/* Storefront stylesheet for public/store.html.
 * Served as an immutable, content-hashed asset under /vendor/store-<hash>.css
 * (see src/store-html.ts getStoreCss + server.ts VENDOR_ASSETS). It used to be
 * inline in store.html; keeping it here lets every navigation reuse the cached
 * file instead of re-downloading ~370 KB of CSS with each page. The clinic CSS
 * sentinel stays INLINE in store.html (it is spliced per tenant at request time).
 */
    /* Smooth cross-document transition to/from the marketplace list (same
       origin). Progressive enhancement — a no-op without the View Transitions API. */
    @view-transition { navigation: auto; }

    /* === Marketplace app-bar === shown only when the customer arrived via a
       marketplace (this.mkt). Makes a vendor open "inside" the marketplace:
       back-to-list + marketplace identity, tinted with the marketplace's OWN
       accent (scoped --mkt-accent), leaving the store's own theme untouched.
       Its measured height is published as --mkt-bar-h so every sticky header
       below offsets under it (default 0px ⇒ direct visitors are unaffected). */
    .mkt-bar {
      --mkt-accent: var(--accent, #d4af37);
      position: sticky; top: 0; z-index: 45;
      display: flex; align-items: center; gap: 8px;
      min-height: 56px; padding: 7px 12px;
      padding-top: max(7px, env(safe-area-inset-top));
      background: color-mix(in srgb, var(--mkt-accent) 13%, var(--bg, #09090b));
      border-bottom: 1px solid color-mix(in srgb, var(--mkt-accent) 34%, transparent);
      -webkit-backdrop-filter: saturate(150%) blur(12px); backdrop-filter: saturate(150%) blur(12px);
      font-family: 'Inter', system-ui, -apple-system, sans-serif;
      /* Shared name with the marketplace shell's .appbar so a cross-document
         View Transition treats both as the SAME element — the bar stays pinned
         (morphs in place) instead of cross-fading when entering/leaving a
         vendor. This is the visual backbone of "one app, vendors inside it". */
      view-transition-name: mkt-appbar;
    }
    .mkt-bar-back, .mkt-bar-id, .mkt-bar-search {
      display: inline-flex; align-items: center;
      background: transparent; border: 0; cursor: pointer;
      color: var(--text, #f0ede8); border-radius: 10px;
    }
    .mkt-bar-back { flex-shrink: 0; width: 40px; height: 40px; justify-content: center; }
    .mkt-bar-back:hover { background: color-mix(in srgb, var(--mkt-accent) 18%, transparent); }
    .mkt-bar-back svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 2.2; }
    /* Marketplace identity: logo chip + name. The logo is the single biggest
       "this is one app" signal — same brand mark as the group shell's app-bar. */
    .mkt-bar-id { gap: 9px; min-width: 0; flex: 1; min-height: 40px; padding: 4px 6px; }
    .mkt-bar-id:hover { background: color-mix(in srgb, var(--mkt-accent) 12%, transparent); }
    .mkt-bar-logo {
      flex-shrink: 0; width: 32px; height: 32px; border-radius: 9px; overflow: hidden;
      display: grid; place-items: center;
      background: color-mix(in srgb, var(--mkt-accent) 20%, transparent);
      color: var(--mkt-accent); font: 800 15px 'Inter', system-ui, sans-serif;
    }
    .mkt-bar-logo img { width: 100%; height: 100%; object-fit: cover; }
    .mkt-bar-name {
      font: 800 16px 'Inter', system-ui, sans-serif; color: var(--text, #f0ede8); letter-spacing: -0.01em;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .mkt-bar-search {
      flex-shrink: 0; width: 40px; height: 40px; justify-content: center;
    }
    .mkt-bar-search:hover { background: color-mix(in srgb, var(--mkt-accent) 18%, transparent); }
    .mkt-bar-search svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 2.2; }
    @media (prefers-reduced-motion: reduce) { .mkt-bar { backdrop-filter: none; -webkit-backdrop-filter: none; } }

    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    /* Global responsive-image guard: no <img> may exceed its container's
       width (prevents large product/hero/logo uploads from overflowing the
       screen). Only caps width — images that set explicit width/height +
       object-fit (cards, gallery slides) keep their sizing because those
       declarations are more specific / set height directly. */
    img { max-width: 100%; }

    :root {
      --bg: #09090b;
      --surface: #111113;
      --surface2: #18181b;
      --surface3: #222225;
      --text: #f0ede8;
      /* Secondary + muted text DERIVE from --text (a faded version of it)
         rather than a fixed cool gray. A hardcoded #71717a looked dull /
         clashed on warm-brand or light themes; mixing toward the background
         keeps them harmonious with whatever --text + --bg the theme uses,
         and auto-flips for light themes (same idea as the borders below).
         Super-admin can still hard-override --text-muted via applyTheme. */
      /* Secondary = readable body-support text (descriptions, menu links,
         meta). Kept high (78%) so it's crisp, not dull. Muted = quiet
         captions / labels / placeholders only (58%). Both derive from
         --text so they harmonize with any theme + auto-flip on light bgs. */
      --text-secondary: color-mix(in srgb, var(--text) 78%, var(--bg));
      --text-muted:     color-mix(in srgb, var(--text) 58%, var(--bg));

      /* Borders derive from text color so they stay visible whether the
         super-admin runs a dark or light theme. Hardcoded white alphas
         were invisible on light backgrounds and too subtle on dark; the
         color-mix pattern auto-flips contrast direction with the theme.
         `--border` is the quiet separator (cards, dividers); `--border-light`
         is the interactive outline (buttons, inputs, pills) and gets a
         stronger contrast so form fields are always discoverable. */
      --border:       color-mix(in srgb, var(--text) 18%, transparent);
      --border-light: color-mix(in srgb, var(--text) 32%, transparent);

      /* Modal panel max-height — defaults to dvh (auto-tracks URL bar). When
         an on-screen keyboard opens, JS overrides these with px values from
         visualViewport so the panel footer stays above the keyboard.
         --kb-gap is the keyboard's height (0 when no keyboard). The panel
         uses it as `bottom: var(--kb-gap)` so it floats above the keyboard
         on iOS Safari, where the layout viewport sits behind the keyboard
         and `bottom: 0` would put the panel out of sight. */
      --modal-h: 92dvh;
      --modal-h-desktop: 88dvh;
      --kb-gap: 0px;

      --accent: var(--theme-accent, #d4af37);
      --accent-light: var(--theme-accent-light, #f0d060);
      --accent-dim: var(--theme-accent-dim, rgba(212,175,55,0.12));
      --accent-glow: var(--theme-accent-glow, rgba(212,175,55,0.03));
      --accent-shadow: var(--theme-accent-shadow, rgba(212,175,55,0.3));
      /* Accent text/fill split + interaction states. Defaults preserve the
         pre-existing look (white text on fills, accent-as-text = accent,
         lighten-on-hover). applyTheme() overrides the --theme-* sources with
         WCAG-aware, mode-aware derivations whenever a tenant theme is set, so
         ANY brand colour stays legible:
           --accent-on   → text/icons placed ON an accent fill (auto #fff/#111)
           --accent-text → the accent used AS text on the page bg (prices,
                           links) — auto-darkened/lightened to clear 4.5:1
           --accent-hover/active → interaction states (darken on light bg,
                           lighten on dark) instead of crude brightness filters */
      --accent-on:     var(--theme-accent-on, #fff);
      --accent-text:   var(--theme-accent-text, var(--accent));
      --accent-hover:  var(--theme-accent-hover, var(--accent-light));
      --accent-active: var(--theme-accent-active, var(--accent));

      --green: #22c55e;
      --green-dim: rgba(34,197,94,0.12);
      --red: #ef4444;
      --red-dim: rgba(239,68,68,0.10);
      --warn: #f59e0b;
      --warn-dim: rgba(245,158,11,0.12);
      --whatsapp: #25D366;
      --whatsapp-dark: #1ebe57;

      --radius: 16px; --radius-sm: 12px; --radius-xs: 8px; --radius-full: 9999px;

      /* ── Design tokens (Phase 3 of the 2026-09 storefront CSS audit) ──
         Spacing scale. Layout-scoped rules read these wherever the literal
         mapped EXACTLY; anything off-scale (10/14/18/26/28px…) deliberately
         stays a literal, so no visible size moved. New rules should pick
         from the scale. */
      --space-1: 4px;  --space-2: 8px;  --space-3: 12px; --space-4: 16px;
      --space-5: 20px; --space-6: 24px; --space-7: 32px; --space-8: 48px;
      /* Fluid type. Steps -1..3 travel LESS than 1px between a 320px phone
         and desktop — they exist so the 0.8 / 1 / 1.2 / 1.5 / 2rem literals
         could adopt them without a visible shift. Steps 4 and 5 are the
         display sizes: the two clamp()s the website heroes already used. */
      --step--1: clamp(0.76rem, 0.74rem + 0.15vw, 0.8rem);
      --step-0:  clamp(0.95rem, 0.92rem + 0.25vw, 1rem);
      --step-1:  clamp(1.14rem, 1.1rem + 0.3vw, 1.2rem);
      --step-2:  clamp(1.44rem, 1.38rem + 0.4vw, 1.5rem);
      --step-3:  clamp(1.94rem, 1.86rem + 0.5vw, 2rem);
      --step-4:  clamp(2rem, 4vw, 3rem);
      --step-5:  clamp(2.2rem, 5vw, 4rem);
      /* Motion durations (the existing --ease is the curve). */
      --motion-fast: 150ms; --motion-base: 250ms;
      /* Shadow ink — derived from --text so elevation flips with the theme
         (near-black on light stores, a faint light rim on dark ones) instead
         of an rgba(0,0,0) that vanishes on a dark canvas. Use as
         color-mix(in srgb, var(--shadow-color) N%, transparent). */
      --shadow-color: var(--text);
      /* Product grid + category-tile geometry. Every grid layout sets ONLY
         these (per breakpoint, on <html>); the grid rules themselves are
         declared once (see the .menu-list and .sf-tiles rules). The values
         here are the neutral shape the loading skeleton paints BEFORE
         data-layout lands (4-up desktop, the pre-existing skeleton). */
      --layout-cols: 4; --layout-gap: 18px;
      /* Loading-skeleton neutrals. Light by default — the tenant theme hasn't
         loaded yet and a light-themed store must not flash black. The dark
         blocks at the end of the file swap them onto the surface tokens. */
      --sk-bg: #f4f5f7; --sk-surface: #fff; --sk-border: #e7e8ec;
      --sk-bone: #e9eaee; --sk-bone-hi: #f1f2f5;
      /* Default display face is Fraunces — modern variable serif, the
         current world-class editorial pick (Aesop / Le Labo / SMB D2C
         brands have moved on from Playfair-class faces). Per-tenant
         override via theme.font_preset (set by applyTheme) switches
         to Playfair Display ('classic') or Bricolage Grotesque
         ('modern') without reloading. */
      --font-display: 'Fraunces', Georgia, serif;
      --font-body: 'Inter', system-ui, -apple-system, sans-serif;
      --ease: cubic-bezier(0.22, 1, 0.36, 1);

      /* Image aspect ratios per surface. Overridden per-vertical
         further down (fashion → 3/4 portrait, matching Myntra / Ajio /
         Bewakoof / Zalando). Card aspect drives both the menu-card
         thumb and the offer-card hero; modal aspect drives the
         detail-modal carousel + the legacy single-image div on mobile
         (desktop modal still uses auto + min-height 460px so the
         2-column layout doesn't break). */
      --card-aspect: 1 / 1;
      --modal-aspect: 4 / 3;
      /* Where to anchor a cover-cropped image. Fashion overrides this
         to center 25% so head + torso stay visible when the source
         photo is more portrait than the card aspect ratio. */
      --img-anchor: center;
    }

    html[data-vertical="grocery"]     { --theme-accent: #84cc16; --theme-accent-light: #bef264; --theme-accent-dim: rgba(132,204,22,0.12); --theme-accent-glow: rgba(132,204,22,0.03); --theme-accent-shadow: rgba(132,204,22,0.3); }
    html[data-vertical="fnb"]         { --theme-accent: #f59e0b; --theme-accent-light: #fbbf24; --theme-accent-dim: rgba(245,158,11,0.12); --theme-accent-glow: rgba(245,158,11,0.03); --theme-accent-shadow: rgba(245,158,11,0.3); }
    /* Fashion: portrait cards (3:4) — Myntra / Ajio / Bewakoof / ASOS
       standard. Anchor cover-crop to upper-third so model's face/torso
       stays visible when the source image is ultra-portrait (9:16). */
    html[data-vertical="fashion"]     { --theme-accent: #f43f5e; --theme-accent-light: #fb7185; --theme-accent-dim: rgba(244,63,94,0.12); --theme-accent-glow: rgba(244,63,94,0.03); --theme-accent-shadow: rgba(244,63,94,0.3); --card-aspect: 3/4; --modal-aspect: 3/4; --img-anchor: center 25%; }
    html[data-vertical="beauty"]      { --theme-accent: #ec4899; --theme-accent-light: #f472b6; --theme-accent-dim: rgba(236,72,153,0.12); --theme-accent-glow: rgba(236,72,153,0.03); --theme-accent-shadow: rgba(236,72,153,0.3); }
    html[data-vertical="electronics"] { --theme-accent: #38bdf8; --theme-accent-light: #7dd3fc; --theme-accent-dim: rgba(56,189,248,0.12); --theme-accent-glow: rgba(56,189,248,0.03); --theme-accent-shadow: rgba(56,189,248,0.3); }
    html[data-vertical="pharmacy"]    { --theme-accent: #2dd4bf; --theme-accent-light: #5eead4; --theme-accent-dim: rgba(45,212,191,0.12); --theme-accent-glow: rgba(45,212,191,0.03); --theme-accent-shadow: rgba(45,212,191,0.3); }
    html[data-vertical="jewelry"]     { --theme-accent: #d4af37; --theme-accent-light: #f0d060; --theme-accent-dim: rgba(212,175,55,0.12); --theme-accent-glow: rgba(212,175,55,0.03); --theme-accent-shadow: rgba(212,175,55,0.3); }
    /* Healthcare was missing from this block since the clinic build — it fell
       through to the base with no accent at all. Calm trust-blue, distinct
       from electronics' sky and pharmacy's teal. */
    html[data-vertical="healthcare"]  { --theme-accent: #3b82f6; --theme-accent-light: #93c5fd; --theme-accent-dim: rgba(59,130,246,0.12); --theme-accent-glow: rgba(59,130,246,0.03); --theme-accent-shadow: rgba(59,130,246,0.3); }
    /* Furniture: warm oak. Accent only — the wide-card geometry lives on the
       showroom LAYOUT so any vertical running it gets the look. */
    html[data-vertical="furniture"]   { --theme-accent: #b45309; --theme-accent-light: #dd8b3e; --theme-accent-dim: rgba(180,83,9,0.12); --theme-accent-glow: rgba(180,83,9,0.03); --theme-accent-shadow: rgba(180,83,9,0.3); }
    html[data-vertical="general"]     { --theme-accent: #d4af37; --theme-accent-light: #f0d060; --theme-accent-dim: rgba(212,175,55,0.12); --theme-accent-glow: rgba(212,175,55,0.03); --theme-accent-shadow: rgba(212,175,55,0.3); }

    /* ─── Storefront layouts (migration 016) ─────────────────────────
       Four named presentation modes, set per-tenant in super-admin via
       data-layout on <html>. List is today's default — no overrides
       needed. The other three flip the .menu-list container into a CSS
       grid and re-orient each card's image vs. body. Modal, hero,
       category tabs and offers carousel are unchanged across all
       four (out of scope for v1). */

    /* Layout: gallery — 2-col image-first (fashion / jewelry / beauty).
       Image takes the full card width; description hidden so the visual
       does the selling. Bumps to 3 cols at tablet. */
    /* ONE product-grid rule for every grid layout (gallery, grid, storefront,
       editorial, swift, bazaar, booking, showroom). Each layout sets only
       --layout-cols / --layout-gap (+ --layout-pad / --layout-max for the
       website-class ones that centre a padded column) on <html>, per
       breakpoint; the grid itself is declared once. 'list' is block flow
       below 1024px and keeps its own desktop rule further down; magazine is
       never a grid. Bazaar's rails mode overrides display with a
       higher-specificity rule of its own. */
    html[data-layout="gallery"] .menu-list,
    html[data-layout="grid"] .menu-list,
    html[data-layout="storefront"] .menu-list,
    html[data-layout="editorial"] .menu-list,
    html[data-layout="swift"] .menu-list,
    html[data-layout="bazaar"] .menu-list,
    html[data-layout="booking"] .menu-list,
    html[data-layout="showroom"] .menu-list {
      display: grid; grid-template-columns: repeat(var(--layout-cols), 1fr); gap: var(--layout-gap);
      max-width: var(--layout-max, none); margin: 0 auto; padding: 0 var(--layout-pad, 0);
    }
    html[data-layout="gallery"] { --card-aspect: 4 / 5; --layout-cols: 2; --layout-gap: 14px; }
    html[data-layout="gallery"] .menu-card { margin-bottom: 0; }
    html[data-layout="gallery"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="gallery"] .card-img-wrap { width: 100%; }
    html[data-layout="gallery"] .card-body { padding: var(--space-3) var(--space-3) var(--space-3); min-width: 0; }
    html[data-layout="gallery"] .card-desc { display: none; }
    html[data-layout="gallery"] .card-meta { display: none; }
    @media (min-width: 720px) {
      html[data-layout="gallery"] { --layout-cols: 3; }
    }
    @media (min-width: 1024px) {
      html[data-layout="gallery"] { --layout-cols: 4; }
    }

    /* Layout: grid — 3-col compact tiles (pharmacy / quick-commerce).
       Squarer images, smaller name + price, single + button. Density
       prioritized over storytelling. */
    html[data-layout="grid"] { --card-aspect: 1 / 1; --layout-cols: 3; --layout-gap: 8px; }
    html[data-layout="grid"] .menu-card { margin-bottom: 0; }
    html[data-layout="grid"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="grid"] .card-img-wrap { width: 100%; }
    html[data-layout="grid"] .card-body { padding: 10px 10px 10px; min-width: 0; }
    html[data-layout="grid"] .card-name { font-size: 0.82rem; line-height: 1.25; gap: var(--space-1); }
    html[data-layout="grid"] .card-desc { display: none; }
    html[data-layout="grid"] .card-meta { display: none; }
    html[data-layout="grid"] .card-variants { display: none; }
    html[data-layout="grid"] .card-footer { gap: 6px; }
    @media (min-width: 720px) {
      html[data-layout="grid"] { --layout-cols: 4; }
    }
    @media (min-width: 1024px) {
      html[data-layout="grid"] { --layout-cols: 5; }
    }

    /* Layout: magazine — full-width hero cards (artisan / premium).
       Wide 16:9 image, generous whitespace, full description preserved.
       Single column at every breakpoint — narrative reading order. */
    html[data-layout="magazine"] { --card-aspect: 16 / 9; }
    html[data-layout="magazine"] .menu-card { margin-bottom: 28px; }
    html[data-layout="magazine"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="magazine"] .card-img-wrap { width: 100%; }
    html[data-layout="magazine"] .card-body { padding: 18px 16px 14px; min-width: 0; }
    html[data-layout="magazine"] .card-name { font-size: var(--step-1); line-height: 1.3; }
    html[data-layout="magazine"] .card-desc {
      display: block; max-height: none; -webkit-line-clamp: unset;
      font-size: 0.92rem; line-height: 1.55; margin-top: 6px;
    }

    /* ===================== STOREFRONT LAYOUT (Shopify-style) =====================
       Desktop-first (base = desktop; one max-width:719px block collapses to
       mobile). Scoped under html[data-layout="storefront"] so the other four
       (mobile-first) layouts are untouched. Honors theme vars only. */
    html[data-layout="storefront"] { --layout-cols: 4; --layout-gap: 18px; }
    html[data-layout="storefront"] .main { max-width: 1280px; }
    html[data-layout="storefront"] .menu-card { margin-bottom: 0; }
    html[data-layout="storefront"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="storefront"] .card-img-wrap { width: 100%; }
    html[data-layout="storefront"] .card-body { padding: var(--space-3) var(--space-3) var(--space-3); min-width: 0; }
    html[data-layout="storefront"] .card-desc { display: none; }

    /* === Equal-height product cards (all three website layouts) ===
       In a CSS grid every card in a row already stretches to the tallest
       sibling; the problem is the CONTENT not aligning — a 1-line vs 2-line
       name pushes the price/button row to different heights. Fix: make the
       card a full-height flex column, clamp the name to exactly 2 lines with
       a fixed min-height, and let the footer sit at the bottom (margin-top
       auto) so the price + Add button line up across the whole row. */
    html[data-layout="storefront"] .menu-card,
    html[data-layout="editorial"] .menu-card,
    html[data-layout="swift"] .menu-card,
    html[data-layout="clinic"] .menu-card,
    html[data-layout="showroom"] .menu-card { height: 100%; }
    /* ...but NOT in a rail. height:100% resolves against the rail's own auto
       height, which is indeterminate, and an explicit height disables
       align-items: stretch — so rail cards sized themselves independently and
       a two-line name left its card taller than its neighbours. Grid cards
       still want the 100% to fill their row. Needs .menu-card.sf-rail-card to
       outrank the rule above. */
    html[data-layout="showroom"] .menu-card.sf-rail-card { height: auto; }
    html[data-layout="storefront"] .menu-card-inner,
    html[data-layout="editorial"] .menu-card-inner,
    html[data-layout="swift"] .menu-card-inner,
    html[data-layout="clinic"] .menu-card-inner,
    html[data-layout="showroom"] .menu-card-inner { height: 100%; }
    html[data-layout="storefront"] .card-body,
    html[data-layout="editorial"] .card-body,
    html[data-layout="swift"] .card-body,
    html[data-layout="clinic"] .card-body,
    html[data-layout="showroom"] .card-body { flex: 1; }
    /* Name: clamp to 2 lines, reserve both lines so short names don't
       shorten the card body and misalign the footer. */
    html[data-layout="storefront"] .card-name,
    html[data-layout="editorial"] .card-name,
    html[data-layout="swift"] .card-name,
    html[data-layout="clinic"] .card-name,
    html[data-layout="showroom"] .card-name {
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
      overflow: hidden;
      /* Clamped to two lines, but NOT padded out to two. min-height: 2.6em
         used to reserve the second line even for a one-line name, which left
         a blank line between the title and the unit ("Murrah Royale" then a
         gap then "1 L") and made every grocery card taller than it needed to
         be. It was never needed for alignment: the very next rule pins the
         footer with margin-top: auto, so prices line up across a row whatever
         the names do, and a card whose neighbours are taller still absorbs the
         slack above the footer rather than under the title — which is exactly
         how the reference cards behave. */
    }
    /* Footer (price + button) pinned to the bottom of every card. */
    html[data-layout="storefront"] .card-footer,
    html[data-layout="editorial"] .card-footer,
    html[data-layout="swift"] .card-footer,
    html[data-layout="clinic"] .card-footer,
    html[data-layout="showroom"] .card-footer { margin-top: auto; }

    /* ===================================================================
       EDITORIAL THEME — a boutique/luxe skin over the SAME website chrome
       as 'storefront' (header, hero, tiles, mega-menu, footer, product
       grid all reused). Everything here is scoped under
       html[data-layout="editorial"] so it never touches the other layouts.
       Design language: big photography, serif display headlines, generous
       whitespace, hairline rules, quiet uppercase labels, hover-reveal
       product cards. Think Aesop / SSENSE / premium DTC. The product grid
       inherits the storefront column rules above, restyled below.
       =================================================================== */
    html[data-layout="editorial"] {
      --layout-cols: 3; --layout-gap: 8px 28px; --layout-pad: 32px; --layout-max: 1320px;
      --tiles-gap: 24px;
      /* Editorial's crisp corners, scaled off the theme's --radius-xs so the
         'sharp' preset still reads sharp and 'soft' softens them:
         tile = 4px / cta = 2px on the default scale (pixel-identical to the
         former literals), 2px / 1px on sharp, 5.5px / 2.75px on soft. */
      --ed-radius-tile: calc(var(--radius-xs) / 2);
      --ed-radius-cta: calc(var(--radius-xs) / 4);
    }
    html[data-layout="editorial"] .main { max-width: 1320px; }
    html[data-layout="editorial"] .menu-card { margin-bottom: 0; }
    html[data-layout="editorial"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="editorial"] .card-img-wrap { width: 100%; }
    html[data-layout="editorial"] .card-desc { display: none; }

    /* --- Editorial: airy, borderless product cards with hover-zoom --- */
    html[data-layout="editorial"] .menu-card { background: none; border: none; border-radius: 0; box-shadow: none; overflow: visible; }
    html[data-layout="editorial"] .menu-card:hover { transform: none; box-shadow: none; }
    html[data-layout="editorial"] .menu-card-inner { background: none; border: none; gap: 14px; padding: 14px 0 28px; }
    html[data-layout="editorial"] .card-img-wrap { aspect-ratio: 3/4; border-radius: var(--ed-radius-tile); background: var(--surface2); box-shadow: 0 1px 0 var(--border); }
    html[data-layout="editorial"] .card-img { transition: transform 0.9s cubic-bezier(.2,.8,.2,1); }
    html[data-layout="editorial"] .menu-card:hover .card-img { transform: scale(1.05); }
    html[data-layout="editorial"] .card-body { padding: 0; gap: 5px; text-align: center; align-items: center; }
    html[data-layout="editorial"] .card-name { font-family: var(--font-display); font-weight: 600; font-size: 1.04rem; letter-spacing: 0.01em; line-height: 1.35; }
    html[data-layout="editorial"] .card-price { font-family: var(--font-body); font-weight: 500; font-size: 0.92rem; letter-spacing: 0.04em; color: var(--text-secondary); justify-content: center; }
    html[data-layout="editorial"] .card-footer { flex-direction: column; gap: var(--space-3); margin-top: var(--space-1); padding-top: var(--space-1); align-items: center; }
    /* Quiet outline CTA that fills on hover — the boutique "ADD" button. */
    html[data-layout="editorial"] .add-btn { margin: 0; width: 100%; max-width: 230px; padding: 11px 18px; border-radius: var(--ed-radius-cta); border: 1px solid var(--text); color: var(--text); background: transparent; letter-spacing: 0.14em; font-size: 0.72rem; opacity: 0; transform: translateY(4px); transition: opacity .25s, transform .25s, background .2s, color .2s; }
    html[data-layout="editorial"] .menu-card:hover .add-btn,
    html[data-layout="editorial"] .add-btn:focus-visible { opacity: 1; transform: translateY(0); }
    html[data-layout="editorial"] .add-btn:hover { background: var(--text); color: var(--surface); }
    html[data-layout="editorial"] .qty-ctrl { margin: 0; }
    /* Touch devices can't hover — keep the CTA visible there. */
    @media (hover: none) { html[data-layout="editorial"] .add-btn { opacity: 1; transform: none; } }

    /* --- Editorial: typography + chrome restyle --- */
    /* Header: flat, no blur/translucency — a calm stationery-white bar. */
    html[data-layout="editorial"] .sf-header { background: var(--surface); -webkit-backdrop-filter: none; backdrop-filter: none; border-bottom: 1px solid var(--border); }
    html[data-layout="editorial"] .sf-header-inner { padding: var(--space-4) var(--space-7); }
    html[data-layout="editorial"] .sf-nav-link { font-family: var(--font-body); font-weight: 500; font-size: var(--step--1); letter-spacing: 0.13em; text-transform: uppercase; }
    html[data-layout="editorial"] .sf-nav-link::after { left: 13px; right: 13px; height: 1px; }
    /* Softly rounded search pill + cart (the old 2px read as hard/square
       against the rest of the bar). Pill search + gently-rounded cart suit
       the boutique look without going fully sharp-edged. */
    html[data-layout="editorial"] .sf-search { border-radius: var(--radius-full); }
    html[data-layout="editorial"] .sf-cart { border-radius: var(--radius-sm); }

    /* Hero: taller, cinematic; serif headline, letter-spaced uppercase CTA. */
    /* Match Modern's hero frame (8:3 = the 1600×600 banner spec) so a
       contained banner fills it edge-to-edge with no top/bottom letterbox.
       A taller frame (16:9) left blank bands above/below the 1600×600 art. */
    html[data-layout="editorial"] .sf-hero { aspect-ratio: 1600 / 600; max-height: 600px; }
    html[data-layout="editorial"] .sf-hero-overlay { align-items: flex-end; }
    html[data-layout="editorial"] .sf-hero-overlay.has-text { background: linear-gradient(0deg, rgba(0,0,0,0.5), transparent 55%); }
    html[data-layout="editorial"] .sf-hero-text { text-align: center; padding: 0 24px 64px; }
    html[data-layout="editorial"] .sf-hero-text h1 { font-weight: 500; font-size: var(--step-5); letter-spacing: 0.01em; margin: 0 auto 18px; max-width: 16ch; }
    html[data-layout="editorial"] .sf-hero-text p { margin: 0 auto 26px; font-size: 1.05rem; letter-spacing: 0.02em; }
    html[data-layout="editorial"] .sf-hero-cta { background: color-mix(in srgb, var(--surface) 95%, transparent); color: var(--text); border-radius: var(--ed-radius-cta); padding: 14px 38px; letter-spacing: 0.16em; text-transform: uppercase; font-size: 0.78rem; box-shadow: none; }
    html[data-layout="editorial"] .sf-hero-cta:hover { background: var(--surface); }

    /* Section headers: centered, serif, with a thin rule + uppercase kicker. */
    html[data-layout="editorial"] .sf-tiles-wrap { padding-top: 60px; }
    html[data-layout="editorial"] .sf-section-title,
    html[data-layout="editorial"] .sf-products-title .sf-section-title {
      text-align: center; font-weight: 500; font-size: clamp(1.5rem, 3vw, 2.1rem);
      letter-spacing: 0.01em; margin: 0 auto 30px; position: relative; padding-bottom: var(--space-4);
    }
    html[data-layout="editorial"] .sf-section-title::after { content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 40px; height: 1px; background: var(--accent); }
    html[data-layout="editorial"] .sf-products-title { margin-top: 64px; }

    /* Category tiles: portrait, sharp corners, dark caption overlay. */
    html[data-layout="editorial"] .sf-tile { position: relative; gap: 0; }
    html[data-layout="editorial"] .sf-tile-img { aspect-ratio: 4/5; border-radius: var(--ed-radius-tile); }
    html[data-layout="editorial"] .sf-tile:hover .sf-tile-img { transform: none; box-shadow: 0 18px 40px color-mix(in srgb, var(--shadow-color) 22%, transparent); }
    html[data-layout="editorial"] .sf-tile-name {
      position: absolute; left: 0; right: 0; bottom: 0; padding: 40px 12px 16px; color: #fff;
      font-family: var(--font-display); font-weight: 500; font-size: 1.1rem; letter-spacing: 0.02em;
      background: linear-gradient(0deg, rgba(0,0,0,0.6), transparent); border-radius: 0 0 var(--ed-radius-tile) var(--ed-radius-tile); pointer-events: none;
    }
    html[data-layout="editorial"] .sf-tiles--sub .sf-tile-img { aspect-ratio: 4/5; }
    html[data-layout="editorial"] .sf-cat-head-inner { padding: 56px 32px; text-align: center; }
    html[data-layout="editorial"] .sf-cat-head h1 { font-weight: 500; font-size: var(--step-4); letter-spacing: 0.01em; }
    html[data-layout="editorial"] .sf-crumb { justify-content: center; letter-spacing: 0.08em; text-transform: uppercase; font-size: 0.72rem; }

    /* Footer kept structurally identical; just calmer type. */
    html[data-layout="editorial"] .sf-footer { background: var(--surface); border-top: 1px solid var(--border); }

    /* Responsive: editorial product grid + tiles collapse gracefully. */
    @media (max-width: 1023px) {
      html[data-layout="editorial"] { --layout-cols: 2; --layout-gap: 6px 20px; --layout-pad: 20px; --layout-tiles: 3; }
    }
    @media (max-width: 719px) {
      html[data-layout="editorial"] { --layout-cols: 2; --layout-gap: 4px 12px; --layout-pad: 12px; --layout-tiles: 2; --tiles-gap: 12px; }
      html[data-layout="editorial"] .sf-header-inner { padding: 12px 14px; }
      html[data-layout="editorial"] .add-btn { opacity: 1; transform: none; }   /* always visible on phones */
      /* Keep the 8:3 banner frame on mobile (same as Modern) so the wide
         1600×600 banner isn't letterboxed into a tall portrait box. */
      html[data-layout="editorial"] .sf-hero-text { padding-bottom: 28px; }
    }

    /* ===================================================================
       SWIFT THEME (ZSite Swift) — dense quick-commerce skin over the SAME
       website chrome (header, hero, tiles, mega-menu, footer, product grid,
       product pages all reused). Scoped under html[data-layout="swift"] so
       it never touches other layouts. Design language: information-dense,
       speed-to-cart — tight 5-up grid, compact bordered cards with the
       price + an ALWAYS-VISIBLE inline ADD / qty stepper (no hover needed),
       loud discount badges, a slim compact header, shorter hero. Think
       Blinkit / Zepto / Amazon grid.
       =================================================================== */
    /* Bazaar (ZSite Bazaar) reuses Swift's dense product-card skin verbatim —
       the selectors below are grouped so the two themes stay byte-for-byte in
       sync. Bazaar deliberately does NOT inherit Swift's slim-header / flyout /
       two-pane rules further down (those stay swift-only), so it keeps the
       plain base header. */
    html[data-layout="swift"],
    html[data-layout="bazaar"] { --layout-cols: 5; --layout-gap: 12px; --layout-pad: 20px; --layout-max: 1320px; }
    html[data-layout="swift"] .main,
    html[data-layout="bazaar"] .main { max-width: 1320px; }
    html[data-layout="swift"] .menu-card,
    html[data-layout="bazaar"] .menu-card,
    html[data-layout="clinic"] .menu-card { margin-bottom: 0; }
    html[data-layout="swift"] .menu-card-inner,
    html[data-layout="bazaar"] .menu-card-inner,
    html[data-layout="clinic"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="swift"] .card-img-wrap,
    html[data-layout="bazaar"] .card-img-wrap,
    html[data-layout="clinic"] .card-img-wrap { width: 100%; }
    html[data-layout="swift"] .card-desc,
    html[data-layout="bazaar"] .card-desc,
    html[data-layout="clinic"] .card-desc { display: none; }

    /* === Blinkit/Zepto-style product card (Swift) === a clean tile: a padded
       "floating" product image, a 2-line name, the pack/unit, then a price +
       a prominent action row. ADD reads as a white pill with a green border;
       tapping it swaps to a SOLID-green qty stepper of the same footprint (no
       layout jump) — the quick-commerce signature. */
    html[data-layout="swift"] .menu-card,
    html[data-layout="bazaar"] .menu-card,
    html[data-layout="clinic"] .menu-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: none; overflow: hidden; transition: border-color .15s, box-shadow .15s, transform .15s; }
    html[data-layout="swift"] .menu-card:hover,
    html[data-layout="bazaar"] .menu-card:hover,
    html[data-layout="clinic"] .menu-card:hover { transform: translateY(-2px); box-shadow: 0 10px 26px color-mix(in srgb, var(--shadow-color) 16%, transparent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
    html[data-layout="swift"] .menu-card-inner,
    html[data-layout="bazaar"] .menu-card-inner,
    html[data-layout="clinic"] .menu-card-inner { gap: 0; padding: 0; }
    /* Product floats on a soft tile (image contained + padded). Slightly less
       padding than before so the product reads bigger on the tile. */
    html[data-layout="swift"] .card-img-wrap,
    html[data-layout="bazaar"] .card-img-wrap,
    html[data-layout="clinic"] .card-img-wrap { aspect-ratio: 1/1; background: var(--surface2); padding: var(--space-2); }
    /* display:block kills the inline-image baseline descender gap, which
       otherwise nudges the contained product up and makes the TOP padding look
       smaller than the bottom (most visible on the tinted packshot tile). */
    html[data-layout="swift"] .card-img,
    html[data-layout="bazaar"] .card-img,
    html[data-layout="clinic"] .card-img { object-fit: contain; display: block; }
    /* Packaged-goods packshots read best on white (Blinkit/Zepto/1mg), the same
       canvas the detail popup already uses for these verticals. */
    html[data-layout="swift"][data-vertical="grocery"] .card-img-wrap,
    html[data-layout="swift"][data-vertical="pharmacy"] .card-img-wrap { background: #fff; }
    /* Lifestyle verticals (fashion / food): full-bleed cover at the vertical's
       own aspect, so photographic images fill the frame instead of sitting boxed.
       Other verticals stay packshot (contain) so packaged photos never crop. */
    html[data-layout="swift"][data-vertical="fashion"] .card-img-wrap,
    html[data-layout="swift"][data-vertical="fnb"] .card-img-wrap { aspect-ratio: var(--card-aspect, 1/1); padding: 0; }
    html[data-layout="swift"][data-vertical="fashion"] .card-img,
    html[data-layout="swift"][data-vertical="fnb"] .card-img { object-fit: cover; object-position: var(--img-anchor, center); }
    /* "Fill product images" store toggle (migration 075, body.pimg-fill): for
       stores with full-bleed lifestyle photography, fill the tile edge-to-edge
       (cover) instead of fitting the whole photo with letterbox padding. */
    html[data-layout="swift"] body.pimg-fill .card-img-wrap,
    html[data-layout="bazaar"] body.pimg-fill .card-img-wrap,
    html[data-layout="clinic"] body.pimg-fill .card-img-wrap { padding: 0; }
    html[data-layout="swift"] body.pimg-fill .card-img,
    html[data-layout="bazaar"] body.pimg-fill .card-img,
    html[data-layout="clinic"] body.pimg-fill .card-img { object-fit: cover; object-position: var(--img-anchor, center); }
    /* Gentle zoom on hover (pointer devices only) for a premium feel. */
    @media (hover: hover) {
      html[data-layout="swift"] .menu-card .card-img { transition: transform .45s var(--ease, ease); }
      html[data-layout="swift"] .menu-card:hover .card-img { transform: scale(1.05); }
    }
    html[data-layout="swift"] .card-body,
    html[data-layout="bazaar"] .card-body,
    html[data-layout="clinic"] .card-body { padding: 9px 11px 12px; gap: 3px; }
    html[data-layout="swift"] .card-name,
    html[data-layout="bazaar"] .card-name,
    html[data-layout="clinic"] .card-name { font-family: var(--font-body); font-weight: 600; font-size: 0.86rem; line-height: 1.3; max-height: 2.6em; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
    html[data-layout="swift"] .card-meta,
    html[data-layout="bazaar"] .card-meta,
    html[data-layout="clinic"] .card-meta { font-size: 0.74rem; color: var(--text-muted); margin-top: 1px; }
    /* Price + action on one baseline row; the action is ALWAYS visible. No
       divider line above it (Blinkit cards are clean). */
    html[data-layout="swift"] .card-footer,
    html[data-layout="bazaar"] .card-footer,
    html[data-layout="clinic"] .card-footer { margin-top: 9px; padding-top: 0; border-top: none; gap: var(--space-2); align-items: center; }
    html[data-layout="swift"] .card-price,
    html[data-layout="bazaar"] .card-price,
    html[data-layout="clinic"] .card-price { font-size: 0.96rem; font-weight: 800; gap: var(--space-1); }
    html[data-layout="swift"] .card-price small,
    html[data-layout="bazaar"] .card-price small,
    html[data-layout="clinic"] .card-price small { font-size: 0.6rem; }
    html[data-layout="swift"] .price-old,
    html[data-layout="bazaar"] .price-old,
    html[data-layout="clinic"] .price-old { font-size: 0.72rem; }
    /* ADD — white pill, green border + label, soft shadow. */
    html[data-layout="swift"] .add-btn,
    html[data-layout="bazaar"] .add-btn,
    html[data-layout="clinic"] .add-btn { margin-left: auto; min-width: 74px; min-height: 34px; padding: 6px 16px; border-radius: 9px; border: 1.5px solid color-mix(in srgb, var(--accent) 55%, transparent); background: var(--surface); color: var(--accent); font-weight: 800; font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; box-shadow: 0 1px 3px color-mix(in srgb, var(--shadow-color) 10%, transparent); }
    html[data-layout="swift"] .add-btn:hover,
    html[data-layout="bazaar"] .add-btn:hover,
    html[data-layout="clinic"] .add-btn:hover { background: var(--accent); color: var(--theme-accent-fg, #fff); border-color: var(--accent); }
    /* Qty stepper — SOLID green, same footprint as ADD so the card never jumps. */
    html[data-layout="swift"] .qty-ctrl,
    html[data-layout="bazaar"] .qty-ctrl,
    html[data-layout="clinic"] .qty-ctrl { margin-left: auto; min-width: 74px; height: 34px; border: none; background: var(--accent); border-radius: 9px; box-shadow: 0 1px 3px color-mix(in srgb, var(--shadow-color) 12%, transparent); justify-content: space-between; }
    html[data-layout="swift"] .qty-btn,
    html[data-layout="bazaar"] .qty-btn,
    html[data-layout="clinic"] .qty-btn { width: 28px; height: 34px; color: var(--theme-accent-fg, #fff); font-size: 1.15rem; }
    html[data-layout="swift"] .qty-btn:hover,
    html[data-layout="bazaar"] .qty-btn:hover,
    html[data-layout="clinic"] .qty-btn:hover { background: color-mix(in srgb, var(--accent-on) 16%, transparent); }
    html[data-layout="swift"] .qty-val,
    html[data-layout="bazaar"] .qty-val,
    html[data-layout="clinic"] .qty-val { color: var(--theme-accent-fg, #fff); font-size: 0.9rem; }
    /* Discount tag on the tile — q-commerce shoppers scan for it. */
    html[data-layout="swift"] .sf-disc-badge,
    html[data-layout="bazaar"] .sf-disc-badge,
    html[data-layout="clinic"] .sf-disc-badge { position: absolute; top: 8px; left: 8px; z-index: 2; background: var(--accent); color: var(--theme-accent-fg, #fff); font: 800 0.62rem var(--font-body); letter-spacing: 0.02em; padding: 3px 7px; border-radius: 6px; box-shadow: 0 2px 6px color-mix(in srgb, var(--shadow-color) 22%, transparent); }
    /* Featured star moves right so it never collides with the discount tag. */
    html[data-layout="swift"] .bestseller,
    html[data-layout="bazaar"] .bestseller,
    html[data-layout="clinic"] .bestseller { left: auto; right: 0; border-radius: 4px 0 0 4px; font-size: 0.62rem; padding: 4px 9px 4px 7px; }
    /* Swift wants square logos — the shared .sf-logo / .sf-footer-logo carry an
       8px radius for the other website-class themes; drop it for Swift only. */
    html[data-layout="swift"] .sf-logo,
    html[data-layout="swift"] .sf-footer-logo { border-radius: 0; }

    /* === Bazaar: per-category product carousels ===
       Bazaar is a SIMPLE (non-website) layout — it keeps the hero header,
       category-pill tabs and search bar. The twist is the product area: in
       rails mode (no active filter) the .layout-with-sidebar wrapper carries
       .is-bazaar-rails, and each category section's .menu-list flips from the
       dense Swift grid (above) to a horizontal scroll-snap strip of the same
       Swift cards — "category name + product carousel". When the shopper
       searches or taps a category pill, the class drops and .menu-list falls
       back to the Swift grid so results read as a normal grid. */
    html[data-layout="bazaar"] .is-bazaar-rails .menu-list {
      display: flex; gap: 14px; overflow-x: auto; overflow-y: hidden;
      scroll-snap-type: x proximity; padding: 2px 2px 10px;
      scrollbar-width: none; -ms-overflow-style: none;
    }
    html[data-layout="bazaar"] .is-bazaar-rails .menu-list::-webkit-scrollbar { display: none; }
    html[data-layout="bazaar"] .is-bazaar-rails .menu-card { flex: 0 0 172px; scroll-snap-align: start; margin-bottom: 0; }
    html[data-layout="bazaar"] .is-bazaar-rails .menu-list.is-draggable { cursor: grab; }
    html[data-layout="bazaar"] .is-bazaar-rails .menu-list.is-dragging { cursor: grabbing; user-select: none; }
    @media (max-width: 719px) {
      html[data-layout="bazaar"] .is-bazaar-rails .menu-card { flex-basis: 150px; }
    }
    /* Bazaar renders category section heads on EVERY viewport (its home
       rails), but the generic .cat-section-head styling lives in the
       desktop-only (≥1024) block — so style Bazaar's heads here, unscoped to
       width. On desktop these (more specific) simply restate the same look. */
    /* Inter-section spacing: Bazaar stacks several category sections on every
       viewport; the generic gap rule is is-sectioned (desktop) only, so add it
       here for mobile/tablet too. */
    html[data-layout="bazaar"] .cat-section + .cat-section { margin-top: 28px; }
    html[data-layout="bazaar"] .cat-section-head {
      display: flex; align-items: baseline; gap: var(--space-3);
      margin: 0 0 var(--space-3); padding-bottom: var(--space-2);
      border-bottom: 1px solid var(--border-light);
    }
    html[data-layout="bazaar"] .cat-section-head h2 {
      font: 700 1.25rem var(--font-display); color: var(--text);
      letter-spacing: -0.01em; margin: 0;
    }
    html[data-layout="bazaar"] .cat-section-head--sub { border-bottom-color: transparent; margin-bottom: 10px; padding-bottom: 2px; }
    html[data-layout="bazaar"] .cat-section-head--sub h3 {
      font: 700 1.05rem var(--font-display); color: var(--text);
      letter-spacing: -0.005em; margin: 0;
    }
    html[data-layout="bazaar"] .cat-super-head {
      font: 700 1.25rem var(--font-display); color: var(--text); letter-spacing: -0.01em;
      margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--border-light);
    }
    /* "View all →" pushed to the right edge of the head, opening the full
       category grid (Swift rail pattern). */
    html[data-layout="bazaar"] .cat-section-viewall {
      margin-left: auto; flex: 0 0 auto;
      display: inline-flex; align-items: center; gap: 5px;
      color: var(--accent); text-decoration: none;
      font: 700 0.84rem var(--font-body); white-space: nowrap;
    }
    html[data-layout="bazaar"] .cat-section-viewall svg { transition: transform .15s; }
    html[data-layout="bazaar"] .cat-section-viewall:hover svg { transform: translateX(3px); }

    /* Slim compact header + shorter hero (less chrome, more catalog). */
    html[data-layout="swift"] .sf-header-inner { padding: 9px 20px; gap: 10px 16px; }
    html[data-layout="swift"] .sf-nav { padding-top: 5px; gap: 6px; }
    html[data-layout="swift"] .sf-nav-link { font-size: 0.86rem; padding: 7px 11px; }
    /* "Shop by Category" trigger sits flush at the nav's left edge and reads
       as the primary anchor; Home/Offers are quieter utility links after it.
       The Offers link gets the accent so deals stand out. */
    html[data-layout="swift"] .sf-shopcat { margin-right: var(--space-1); }
    html[data-layout="swift"] .sf-shopcat-trigger { padding: var(--space-2) var(--space-4); font-weight: 700; }
    html[data-layout="swift"] .sf-nav-offers { color: var(--accent); font-weight: 700; }
    html[data-layout="swift"] .sf-nav-offers svg { fill: var(--accent-dim); }
    /* Match Modern/Luxe's 8:3 frame (the 1600×600 banner spec). The old
       420px cap made the box shorter than 8:3 on wide screens, so a
       contained banner letterboxed on the sides ("not fitting"). 600px cap
       keeps the same proportion as the other themes. */
    html[data-layout="swift"] .sf-hero { aspect-ratio: 1600 / 600; max-height: 600px; }
    html[data-layout="swift"] .sf-section-title { font-size: 1.25rem; margin-bottom: var(--space-3); }
    html[data-layout="swift"] .sf-products-title { margin-top: 28px; }
    html[data-layout="swift"] .sf-tiles-wrap { padding-top: var(--space-6); }
    /* Compact circular-ish category tiles (quick category hopping). */
    html[data-layout="swift"] { --layout-tiles: 6; --tiles-gap: 14px; }
    html[data-layout="swift"] .sf-tile-img { aspect-ratio: 1/1; border-radius: 12px; }
    html[data-layout="swift"] .sf-tile-name { font-size: 0.84rem; }

    /* === Swift Blinkit-style two-pane category page === a fixed left rail of
       subcategories + the product grid offset to its right. Rail is fixed
       below the sticky header; .sf-cat2-main (the <main>) gets a left pad to
       clear it. Swift-only; only renders when sfTwoPane() is true. */
    html[data-layout="swift"] .sf-cat2-rail {
      position: fixed; left: 0; top: calc(var(--mkt-bar-h, 0px) + var(--sf-header-h, 62px) + var(--sf-titlebar-h, 0px)); bottom: 0; width: 96px; z-index: 30;
      display: flex; flex-direction: column; overflow-y: auto;
      background: var(--surface2); border-right: 1px solid var(--border);
      -webkit-overflow-scrolling: touch; scrollbar-width: none;
    }
    html[data-layout="swift"] .sf-cat2-rail::-webkit-scrollbar { display: none; }
    html[data-layout="swift"] .sf-cat2-rail-item {
      display: flex; flex-direction: column; align-items: center; gap: 5px;
      padding: 12px 4px 11px; text-decoration: none; color: var(--text-secondary);
      position: relative; border-bottom: 1px solid var(--border);
      transition: background .12s, color .12s;
    }
    html[data-layout="swift"] .sf-cat2-rail-item:hover { background: var(--surface); }
    html[data-layout="swift"] .sf-cat2-rail-item.active { background: var(--surface); color: var(--text); }
    /* The signature green active bar + tinted thumb (Blinkit). */
    html[data-layout="swift"] .sf-cat2-rail-item.active::before {
      content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px;
      border-radius: 0 3px 3px 0; background: var(--accent);
    }
    html[data-layout="swift"] .sf-cat2-rail-img {
      width: 54px; height: 54px; border-radius: 14px; flex: 0 0 auto;
      background: var(--surface3, var(--surface)); background-size: cover; background-position: center;
      display: grid; place-items: center; overflow: hidden;
      font: 700 0.8rem var(--font-display); color: var(--text-muted);
      border: 1px solid var(--border);
    }
    html[data-layout="swift"] .sf-cat2-rail-item.active .sf-cat2-rail-img { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-dim); }
    html[data-layout="swift"] .sf-cat2-rail-name {
      font: 600 0.7rem var(--font-body); line-height: 1.2; text-align: center;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    }
    html[data-layout="swift"] .sf-cat2-rail-item.active .sf-cat2-rail-name { color: var(--accent); }
    /* Right pane: the existing <main> grid, offset past the rail + 2 columns
       (less width beside the rail), denser like the screenshots. */
    html[data-layout="swift"] main.sf-cat2-main { padding-left: 96px; max-width: none; padding-top: 14px; }
    /* Responsive density (Blinkit/Zepto): auto-fill fits as many ~185px cards
       as the pane allows instead of stretching a fixed 3 — wide desktops show
       5-6, laptops 4-5, and it steps down cleanly on smaller screens. The
       grid fills the pane (no centered max-width) so the right pane reads full. */
    html[data-layout="swift"] main.sf-cat2-main .menu-list {
      grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
      gap: 14px; max-width: none; padding-right: var(--space-5);
    }
    /* App-shell: when the two-pane category page is active, lock the document
       to the viewport so only the rail + product pane scroll (Blinkit feel).
       The page itself doesn't scroll; the header is fixed at top, the rail is
       fixed on the left, and <main> is a fixed scroll region filling the rest.
       The slim category head is hidden (the rail is the category context).
       Anchored to --sf-header-h (measured live) so it's exact on any header
       height / condensed state. body.sf-shell is bound only on this page. */
    body.sf-shell { overflow: hidden; }
    html[data-layout="swift"] body.sf-shell { --sf-titlebar-h: 48px; }
    html[data-layout="swift"] body.sf-shell .sf-header { position: fixed; left: 0; right: 0; top: var(--mkt-bar-h, 0px); }
    html[data-layout="swift"] body.sf-shell .sf-cat-head { display: none; }
    /* Slim sticky category title bar: fixed full-width strip directly under the
       header, spanning above the rail AND the product pane (both start below it
       via --sf-titlebar-h). The single category-context line for the two-pane
       page (the breadcrumby .sf-cat-head is hidden in the shell). */
    html[data-layout="swift"] body.sf-shell .sf-cat2-titlebar {
      position: fixed; left: 0; right: 0; top: calc(var(--mkt-bar-h, 0px) + var(--sf-header-h, 62px)); z-index: 29;
      height: var(--sf-titlebar-h, 48px); display: flex; align-items: center;
      padding: 0 var(--space-4); background: var(--surface); border-bottom: 1px solid var(--border);
    }
    /* Titlebar as a breadcrumb: parent (tappable → "All") › the selected
       subcategory, so the current sub's name is always shown up top. When the
       parent itself is active ("All") it renders alone as the heading. */
    html[data-layout="swift"] .sf-cat2-crumb { display: flex; align-items: center; gap: 7px; min-width: 0; overflow: hidden; }
    /* parent shrinks/ellipsizes FIRST (flex-shrink 3) so the current subcategory
       stays readable; nothing spills out of the fixed titlebar. */
    html[data-layout="swift"] .sf-cat2-crumb-parent { font: 800 1.12rem var(--font-display); letter-spacing: -0.01em; color: var(--text); text-decoration: none; white-space: nowrap; flex: 0 3 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
    html[data-layout="swift"] .sf-cat2-crumb:not(.is-root) .sf-cat2-crumb-parent { font-size: 0.92rem; font-weight: 600; color: var(--text-muted); }
    html[data-layout="swift"] .sf-cat2-crumb:not(.is-root) .sf-cat2-crumb-parent:hover { color: var(--accent); }
    html[data-layout="swift"] .sf-cat2-crumb-tail { display: flex; align-items: center; gap: 7px; min-width: 0; flex: 0 1 auto; }
    html[data-layout="swift"] .sf-cat2-crumb-sep { color: var(--text-muted); flex: 0 0 auto; }
    html[data-layout="swift"] .sf-cat2-crumb-cur { font: 800 1.12rem var(--font-display); letter-spacing: -0.01em; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
    html[data-layout="swift"] body.sf-shell main.sf-cat2-main {
      position: fixed; left: 0; right: 0; top: calc(var(--sf-header-h, 62px) + var(--sf-titlebar-h, 0px)); bottom: 0;
      overflow-y: auto; -webkit-overflow-scrolling: touch;
      padding-top: 14px;
      /* clear the floating cart strip (fixed bottom) + safe-area so the last
         row of products is never hidden behind it. */
      padding-bottom: calc(96px + env(safe-area-inset-bottom));
    }
    /* The rail also bottoms out above the cart strip so its last item clears it. */
    html[data-layout="swift"] body.sf-shell .sf-cat2-rail { padding-bottom: calc(80px + env(safe-area-inset-bottom)); }

    /* Responsive: dense grid steps down but stays tight. */
    @media (max-width: 1023px) {
      html[data-layout="swift"],
      html[data-layout="bazaar"] { --layout-cols: 3; --layout-gap: 10px; --layout-pad: 16px; }
      html[data-layout="swift"] { --layout-tiles: 4; }
      html[data-layout="swift"] main.sf-cat2-main .menu-list { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
    }
    @media (max-width: 719px) {
      html[data-layout="swift"],
      html[data-layout="bazaar"] { --layout-cols: 2; --layout-gap: 8px; --layout-pad: 10px; }
      html[data-layout="swift"] { --layout-tiles: 3; --tiles-gap: 10px; }
      html[data-layout="swift"] .sf-header-inner { padding: 9px 12px; }
      html[data-layout="swift"] .add-btn,
      html[data-layout="bazaar"] .add-btn { min-width: 54px; padding: 6px 10px; }
      /* Swift sets its price at 0.96rem/800 with a layout-scoped selector, so
         it outranks the shared phone size no matter where that sits. Bold
         digits are wide: the price alone was enough to push the stepper (the
         wider of the two controls) onto a second row while Add still fitted —
         one theme, two card layouts. Scoped to swift; bazaar and clinic share
         the 0.96rem rule but have the width to carry it. */
      html[data-layout="swift"] .card-price { font-size: 0.92rem; }
      /* Narrower rail + 2-col products beside it (Blinkit on a phone). */
      html[data-layout="swift"] .sf-cat2-rail { width: 78px; }
      html[data-layout="swift"] .sf-cat2-rail-img { width: 46px; height: 46px; border-radius: 12px; }
      html[data-layout="swift"] main.sf-cat2-main { padding-left: 78px; }
      html[data-layout="swift"] main.sf-cat2-main .menu-list { grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); gap: var(--space-2); padding: 0 10px; }
      html[data-layout="swift"] body.sf-shell { --sf-titlebar-h: 42px; }
      html[data-layout="swift"] body.sf-shell .sf-cat2-titlebar { padding: 0 var(--space-3); }
      html[data-layout="swift"] .sf-cat2-crumb-parent,
      html[data-layout="swift"] .sf-cat2-crumb-cur { font-size: var(--step-0); }
      html[data-layout="swift"] .sf-cat2-crumb:not(.is-root) .sf-cat2-crumb-parent { font-size: 0.84rem; }
    }

    /* --- Swift "Shop by Category" two-panel flyout (BigBasket/Amazon) --- */
    .sf-shopcat { position: relative; }
    .sf-shopcat-trigger { background: var(--accent-dim); color: var(--accent); }
    .sf-shopcat-trigger.open, .sf-shopcat-trigger:hover { background: var(--accent); color: var(--theme-accent-fg, #fff); }
    .sf-shopcat-trigger.open .sf-nav-caret { transform: rotate(180deg); }
    .sf-shopcat-panel {
      position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
      display: flex; width: min(760px, 92vw); max-height: 72vh;
      background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.32); overflow: hidden;
      opacity: 0; visibility: hidden; transform: translateY(8px);
      transition: opacity .16s ease, transform .16s ease, visibility .16s;
    }
    .sf-shopcat-panel.open { opacity: 1; visibility: visible; transform: translateY(0); }
    /* Left rail: scrollable list of top categories. */
    .sf-shopcat-list { flex: 0 0 248px; overflow-y: auto; padding: 8px; border-right: 1px solid var(--border); background: var(--surface2); }
    .sf-shopcat-cat { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 9px; color: var(--text); text-decoration: none; cursor: pointer; }
    .sf-shopcat-cat:hover, .sf-shopcat-cat.active { background: var(--surface); }
    .sf-shopcat-cat.active { box-shadow: inset 3px 0 0 var(--accent); }
    .sf-shopcat-cat-img { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 8px; background: var(--surface3, var(--surface)); background-size: cover; background-position: center; display: grid; place-items: center; font: 800 0.9rem var(--font-display); color: var(--text-muted); overflow: hidden; }
    .sf-shopcat-cat-name { flex: 1; min-width: 0; font: 600 0.9rem var(--font-body); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sf-shopcat-arrow { flex: 0 0 auto; color: var(--text-muted); }
    .sf-shopcat-cat.active .sf-shopcat-arrow { color: var(--accent); }
    /* Right panel: subcategories of the hovered category. */
    .sf-shopcat-subs { flex: 1; min-width: 0; padding: 14px 16px; overflow-y: auto; }
    .sf-shopcat-sub-head { display: inline-flex; align-items: center; gap: 6px; font: 800 0.92rem var(--font-body); color: var(--accent); text-decoration: none; margin-bottom: 10px; }
    .sf-shopcat-sub-head svg { transition: transform .15s; }
    .sf-shopcat-sub-head:hover svg { transform: translateX(3px); }
    .sf-shopcat-sub-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 2px 12px; }
    .sf-shopcat-sub { display: block; padding: 7px 8px; border-radius: 7px; color: var(--text-secondary); text-decoration: none; font: 500 0.86rem var(--font-body); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sf-shopcat-sub:hover { background: var(--surface2); color: var(--accent); }
    .sf-shopcat-empty { color: var(--text-muted); font: 500 0.86rem var(--font-body); }
    /* Inline accordion sub-list — DESKTOP: hidden (the right panel shows
       subs on hover). MOBILE: revealed under the tapped category. */

    /* On mobile, the hover flyout is replaced entirely by the full-screen
       category SHEET (.cat-sheet, opened by shopByCategoryTap). Hide the
       inline panel + accordion so there's no duplicate UI in the drawer. */
    @media (max-width: 719px) {
      .sf-shopcat-panel { display: none !important; }
      .sf-shopcat-trigger { justify-content: flex-start; }
      .sf-shopcat-trigger .sf-nav-caret { margin-left: auto; }
    }

    /* === Mobile full-screen "Shop by Category" sheet === a spacious app-
       style drill-down (Blinkit / Amazon / Flipkart). Slides over everything;
       level 1 = categories, tap → level 2 = subcategories with a back arrow. */
    .cat-sheet {
      position: fixed; inset: 0; z-index: 95; display: flex; flex-direction: column;
      background: var(--surface); animation: catSheetIn .22s var(--ease, ease);
    }
    @keyframes catSheetIn { from { opacity: 0; transform: translateY(0.5%); } to { opacity: 1; transform: none; } }
    .cat-sheet-head {
      display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
      padding: 14px 14px; padding-top: max(14px, env(safe-area-inset-top));
      border-bottom: 1px solid var(--border); background: var(--surface); position: sticky; top: 0;
    }
    .cat-sheet-back, .cat-sheet-close { flex: 0 0 auto; width: 40px; height: 40px; display: grid; place-items: center; border: none; background: var(--surface2); color: var(--text); border-radius: 10px; cursor: pointer; }
    .cat-sheet-title { flex: 1; min-width: 0; font: 700 1.05rem var(--font-display); letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .cat-sheet-close { margin-left: auto; }
    .cat-sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 8px 12px calc(20px + env(safe-area-inset-bottom)); }
    /* Home / Offers quick links at the top of the sheet (level 1). Two
       equal pills above the category list. */
    .cat-sheet-quick { display: flex; gap: 10px; margin-bottom: 14px; }
    .cat-sheet-quick-link { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; padding: 0 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface2); color: var(--text); font: 700 0.92rem var(--font-body); cursor: pointer; }
    .cat-sheet-quick-link:active { background: var(--surface); border-color: var(--accent); }
    .cat-sheet-quick-link svg { flex: 0 0 auto; color: var(--accent); }
    .cat-sheet-list { display: flex; flex-direction: column; gap: 6px; }
    .cat-sheet-row {
      display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
      padding: 12px 12px; min-height: 60px; border: 1px solid var(--border);
      border-radius: 12px; background: var(--surface); color: var(--text); cursor: pointer;
      transition: background .12s, border-color .12s;
    }
    .cat-sheet-row:active { background: var(--surface2); border-color: var(--accent); }
    .cat-sheet-row-img { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 10px; background: var(--surface2); background-size: cover; background-position: center; display: grid; place-items: center; font: 800 1.1rem var(--font-display); color: var(--text-muted); overflow: hidden; }
    .cat-sheet-row-name { flex: 1; min-width: 0; font: 600 0.98rem var(--font-body); line-height: 1.3; }
    .cat-sheet-row-arrow { flex: 0 0 auto; color: var(--text-muted); }
    .cat-sheet-row--all { background: var(--accent-dim); border-color: transparent; }
    .cat-sheet-row--all .cat-sheet-row-name { color: var(--accent); font-weight: 700; }
    .cat-sheet-row--all .cat-sheet-row-arrow { color: var(--accent); }
    /* Custom-page links in the mobile menu (parity with the desktop top
       nav). A labelled section under the categories with slimmer rows so
       they read as secondary nav, not catalog. */
    .cat-sheet-pages { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
    .cat-sheet-pages-label { font: 700 0.7rem var(--font-body); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin: 0 4px 10px; }
    a.cat-sheet-row { text-decoration: none; }
    .cat-sheet-row--page { min-height: 52px; background: var(--surface2); }
    .cat-sheet-row--page .cat-sheet-row-name { font-weight: 600; }
    .cat-sheet-row--page .cat-sheet-row-arrow { color: var(--accent); }

    /* === Booking sheet (migration 049) === bottom sheet on mobile, centered
       modal on desktop. Reuses modal-label + the storefront accent. */
    .booking-sheet { position: fixed; inset: 0; z-index: 1200; display: flex; flex-direction: column; justify-content: flex-end; }
    .booking-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); }
    .booking-panel { position: relative; background: var(--surface); border-radius: 18px 18px 0 0; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 -8px 40px rgba(0,0,0,0.3); }
    @media (min-width: 720px) { .booking-sheet { justify-content: center; align-items: center; padding: 24px; } .booking-panel { border-radius: 16px; width: 100%; max-width: 460px; } }
    .booking-head { display: flex; align-items: center; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--border); }
    .booking-head h2 { font: 700 1.05rem var(--font-display); margin: 0; flex: 1; min-width: 0; letter-spacing: -0.01em; }
    .booking-head .icon-x { flex: 0 0 auto; width: 36px; height: 36px; border: none; background: var(--surface2); color: var(--text); border-radius: 9px; font-size: 1rem; cursor: pointer; }
    .booking-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 16px 18px calc(16px + env(safe-area-inset-bottom)); }
    .booking-block { margin-bottom: 18px; }
    .booking-block .modal-label { display: block; margin-bottom: 9px; }
    .booking-chips { display: flex; flex-wrap: wrap; gap: 8px; }
    .booking-chip { padding: 9px 14px; border: 1.5px solid var(--border-light); border-radius: 10px; background: var(--surface); color: var(--text); font: 600 0.86rem var(--font-body); cursor: pointer; transition: border-color .15s, background .15s; }
    .booking-chip:hover { border-color: var(--accent); }
    .booking-chip.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
    .booking-chip--cal { border-style: dashed; }
    /* Additive month calendar — opens under the quick chips. Themed via vars. */
    .bk-cal { margin-top: 10px; border: 1.5px solid var(--border-light); border-radius: 12px; padding: 10px; background: var(--surface); }
    .bk-cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
    .bk-cal-title { font: 700 0.9rem var(--font-body); color: var(--text); }
    .bk-cal-nav { width: 32px; height: 32px; border: 1px solid var(--border-light); border-radius: 8px; background: var(--surface); color: var(--text); font-size: 1.1rem; line-height: 1; cursor: pointer; }
    .bk-cal-nav:disabled { opacity: 0.35; cursor: default; }
    .bk-cal-dow, .bk-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
    .bk-cal-dow { margin-bottom: 4px; }
    .bk-cal-dow span { text-align: center; font: 600 0.66rem var(--font-body); color: var(--text-muted); }
    .bk-cal-day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border: 1.5px solid transparent; border-radius: 9px; background: var(--surface2, var(--surface)); color: var(--text-muted); font: 600 0.82rem var(--font-body); cursor: default; }
    .bk-cal-day.is-empty { background: transparent; }
    .bk-cal-day.is-avail { color: var(--text); cursor: pointer; border-color: var(--border-light); }
    .bk-cal-day.is-avail:hover { border-color: var(--accent); }
    .bk-cal-day.is-today.is-avail { box-shadow: inset 0 0 0 1.5px var(--accent); }
    .bk-cal-day.is-sel { background: var(--accent); border-color: var(--accent); color: var(--theme-accent-fg, #111); }
    .bk-cal-day:disabled { opacity: 0.4; }
    .booking-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; }
    .booking-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 9px 12px; border: 1.5px solid var(--border-light); border-radius: 10px; background: var(--surface); color: var(--text); font: 700 0.86rem var(--font-body); cursor: pointer; text-align: left; }
    .booking-slot small { font-weight: 500; font-size: 0.68rem; color: var(--text-muted); }
    .booking-slot:hover:not(.oos) { border-color: var(--accent); }
    .booking-slot.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
    .booking-slot.oos { opacity: 0.5; cursor: not-allowed; }
    .booking-duration { display: inline-flex; align-items: center; gap: 14px; border: 1.5px solid var(--border-light); border-radius: 10px; padding: 6px 12px; }
    .booking-dur-btn { width: 30px; height: 30px; border: none; background: var(--accent-dim); color: var(--accent); border-radius: 8px; font-size: 1.1rem; font-weight: 700; cursor: pointer; }
    .booking-dur-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    .booking-dur-val { font: 600 0.9rem var(--font-body); color: var(--text); }
    .booking-input { width: 100%; margin-bottom: 8px; padding: 11px 13px; border: 1.5px solid var(--border-light); border-radius: 10px; background: var(--surface); color: var(--text); font: 500 0.92rem var(--font-body); }
    .booking-input:focus { outline: none; border-color: var(--accent); }
    .booking-hint { color: var(--text-muted); font: 500 0.88rem var(--font-body); padding: 6px 0; }
    .booking-error { background: color-mix(in srgb, var(--red, #dc2626) 12%, var(--surface)); color: var(--red, #dc2626); border-radius: 9px; padding: 9px 12px; font: 600 0.84rem var(--font-body); margin-bottom: 14px; }
    .booking-foot { display: flex; align-items: center; gap: 12px; padding: 14px 18px calc(14px + env(safe-area-inset-bottom)); border-top: 1px solid var(--border); }
    .booking-foot-price { font: 800 1.05rem var(--font-body); color: var(--text); flex: 0 0 auto; }
    /* One consistent CTA component — inline-flex so the label (and any icon)
       is centered both in the footer flex row and as a standalone full-width
       button in the confirmation state. */
    .booking-cta { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; padding: 0 16px; border: none; border-radius: 12px; background: var(--accent); color: var(--theme-accent-fg, #fff); font: 800 0.95rem var(--font-body); text-decoration: none; cursor: pointer; }
    .booking-cta:disabled { opacity: 0.5; cursor: not-allowed; }
    .booking-cta svg { flex: 0 0 auto; }
    .booking-cta--wa { background: #25d366; color: #fff; }
    .booking-cta--ghost { background: var(--surface2); color: var(--text); }
    .booking-done { text-align: center; padding: 16px 8px; }
    .booking-done-mark { width: 56px; height: 56px; margin: 0 auto 14px; border-radius: 50%; background: var(--accent-dim); color: var(--accent); display: grid; place-items: center; font-size: 1.8rem; font-weight: 800; }
    .booking-done ul { list-style: none; padding: 0; margin: 14px 0; color: var(--text-secondary); font: 500 0.92rem var(--font-body); }
    .booking-done li { padding: 3px 0; }
    /* UPI scan-to-pay block in the booking confirmation (when the store uses a
       UPI QR instead of / alongside a gateway). Mirrors the cart UPI block. */
    .booking-upi { margin: 14px 0 6px; padding: 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface2); }
    .booking-upi-title { font: 700 0.95rem var(--font-body); color: var(--text); }
    .booking-upi-img { width: 200px; max-width: 70%; aspect-ratio: 1; margin: 12px auto; padding: 10px; background: #fff; border-radius: 12px; display: block; object-fit: contain; box-shadow: inset 0 0 0 1px var(--border); }
    .booking-upi-help { font: 500 0.78rem var(--font-body); color: var(--text-muted); line-height: 1.5; }
    /* Confirmation buttons stack full-width. */
    .booking-done .booking-cta { width: 100%; margin-top: 8px; }

    .sf { width: 100%; }

    /* ===================== PRODUCT DETAIL PAGE (PDP) =====================
       Full-page product view for website-class layouts (storefront +
       editorial). Two columns on desktop — large gallery + sticky details;
       single column on mobile. Reuses shared modal state/helpers. */
    .sf-pdp { max-width: 1280px; margin: 0 auto; padding: 18px 24px 56px; }
    .sf-pdp-crumb { margin-bottom: 20px; }
    .sf-pdp-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 48px; align-items: start; }
    /* Gallery */
    .sf-pdp-gallery { position: relative; }
    .sf-pdp-track { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 10px; border-radius: 12px; scrollbar-width: none; }
    .sf-pdp-track::-webkit-scrollbar { display: none; }
    /* contain: the full product image is always visible on its detail page
       (industry standard for a PDP). The surface2 fill backs any margin. */
    .sf-pdp-slide { flex: 0 0 100%; scroll-snap-align: center; aspect-ratio: 3/4; background-size: contain; background-repeat: no-repeat; background-position: center; background-color: var(--surface2); border-radius: 12px; cursor: zoom-in; overflow: hidden; }
    /* Real <img> fills the slide; contain mirrors the prior background-size so
       the full product photo stays visible (PDP standard). object-position
       centres it; the slide's surface2 backs any letterbox margin. */
    .sf-pdp-img { width: 100%; height: 100%; object-fit: contain; object-position: center; display: block; }
    /* Swift popup gallery: follow the vertical's aspect, and let lifestyle
       verticals (fashion/food) fill the frame (cover) like the Swift cards. */
    html[data-layout="swift"] .sf-pdp-slide { aspect-ratio: var(--modal-aspect, 3/4); }
    html[data-layout="swift"][data-vertical="fashion"] .sf-pdp-slide,
    html[data-layout="swift"][data-vertical="fnb"] .sf-pdp-slide { background-size: cover; }
    html[data-layout="swift"][data-vertical="fashion"] .sf-pdp-img,
    html[data-layout="swift"][data-vertical="fnb"] .sf-pdp-img { object-fit: cover; }
    /* Swift's default product popup is the .modal-gallery (pdpAsModal); give its
       slides the same lifestyle-cover treatment. Zoom is click-to-enlarge only
       (the fullscreen lightbox), on every theme/device — no hover-zoom, which
       was desktop-only, swift-only, and an abrupt full-image scale. The
       cursor: zoom-in on the slide already signals the click affordance. */
    html[data-layout="swift"][data-vertical="fashion"] .modal-gallery-slide,
    html[data-layout="swift"][data-vertical="fnb"] .modal-gallery-slide { background-size: cover; }
    .sf-pdp-thumbs { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
    .sf-pdp-thumb { width: 64px; height: 80px; border-radius: 8px; background-size: cover; background-position: center; border: 1.5px solid var(--border); cursor: pointer; padding: 0; transition: border-color .15s; }
    html[data-layout="swift"] .sf-pdp-thumb { width: 60px; height: 60px; }
    .sf-pdp-thumb.active { border-color: var(--accent); }
    /* Details (sticky on desktop so the buy bar stays in view while the
       gallery scrolls). */
    .sf-pdp-info { position: sticky; top: calc(var(--mkt-bar-h, 0px) + 96px); display: flex; flex-direction: column; gap: 14px; }
    .sf-pdp-cat { font: 600 0.74rem var(--font-body); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
    .sf-pdp-name { font: 700 2rem var(--font-display); letter-spacing: -0.01em; line-height: 1.15; margin: 0; display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
    .sf-pdp-price-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
    .sf-pdp-price { font: 700 1.5rem var(--font-body); color: var(--text); white-space: nowrap; font-variant-numeric: tabular-nums; }
    .sf-pdp-price .rupee { color: var(--accent-text); font-weight: 600; margin-right: 1px; }
    .sf-pdp-price-old { font: 500 1.05rem var(--font-body); color: var(--text-muted); text-decoration: line-through; }
    .sf-pdp-off { font: 700 0.8rem var(--font-body); color: #fff; background: var(--green, #16a34a); padding: 3px 9px; border-radius: 999px; }
    .sf-pdp-desc { font: 400 0.98rem/1.65 var(--font-body); color: var(--text-secondary); margin: 2px 0 0; white-space: pre-line; }
    /* Buy zone — a hairline separates it from the details above so the
       qty + Add bar reads as a distinct "purchase" block (Myntra/Nykaa). */
    .sf-pdp-buy { display: flex; align-items: stretch; gap: 12px; margin-top: 6px; padding-top: 18px; border-top: 1px solid var(--border); }
    .sf-pdp-qty { flex: 0 0 auto; }
    .sf-pdp-add { flex: 1; min-height: 52px; border: none; border-radius: 10px; background: var(--accent); color: var(--theme-accent-fg, #fff); font: 700 1rem var(--font-body); cursor: pointer; box-shadow: 0 6px 20px var(--accent-shadow); transition: filter .15s; padding: 0 22px; }
    .sf-pdp-add:hover { filter: brightness(1.06); }
    .sf-pdp-add:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
    /* Savings line + stock pill — quiet PDP reassurance under the price. */
    .sf-pdp-save { font: 600 0.86rem var(--font-body); color: var(--green, #16a34a); margin: -4px 0 0; }
    /* Ships-in / made-to-order on the PDP — informational, not an alarm. */
    .sf-pdp-lead { font: 600 0.84rem var(--font-body); color: var(--text-secondary); margin: 6px 0 0; }
    /* Display-only EMI note (store.emi_note, migration 212) under the price. */
    .sf-pdp-emi { font: 500 0.8rem var(--font-body); color: var(--text-muted); margin: 4px 0 0; }
    .sf-pdp-stock { display: inline-flex; align-items: center; gap: 7px; font: 600 0.82rem var(--font-body); margin-top: 2px; }
    .sf-pdp-stock-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
    .sf-pdp-stock.in { color: var(--green, #16a34a); }
    .sf-pdp-stock.in .sf-pdp-stock-dot { background: var(--green, #16a34a); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green, #16a34a) 22%, transparent); }
    .sf-pdp-stock.out { color: var(--text-muted); }
    .sf-pdp-stock.out .sf-pdp-stock-dot { background: var(--text-muted); }
    /* Trust signals row — icon + label, evenly spaced, hairline-topped. */
    .sf-pdp-trust { list-style: none; display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 16px 0 0; padding: 16px 0 0; border-top: 1px solid var(--border); }
    .sf-pdp-trust li { display: inline-flex; align-items: center; gap: 8px; font: 500 0.82rem var(--font-body); color: var(--text-secondary); }
    .sf-pdp-trust svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--accent); }
    .sf-pdp-empty { text-align: center; color: var(--text-muted); padding: 80px 20px; }
    .sf-pdp-back { display: inline-block; margin-top: 12px; color: var(--accent); text-decoration: none; font-weight: 600; }
    /* Editorial skin tweaks for the PDP — serif name, squared buy button. */
    html[data-layout="editorial"] .sf-pdp-name { font-weight: 500; letter-spacing: 0.005em; }
    html[data-layout="editorial"] .sf-pdp-add { border-radius: var(--ed-radius-cta); letter-spacing: 0.08em; text-transform: uppercase; font-size: 0.86rem; box-shadow: none; background: var(--text); color: var(--surface); }
    html[data-layout="editorial"] .sf-pdp-add:hover { filter: none; opacity: 0.88; }
    html[data-layout="editorial"] .sf-pdp-slide { border-radius: var(--ed-radius-tile); }
    @media (max-width: 859px) {
      .sf-pdp-grid { grid-template-columns: 1fr; gap: 24px; }
      .sf-pdp-info { position: static; top: auto; }
      .sf-pdp { padding: 12px 14px 40px; }
      .sf-pdp-name { font-size: 1.5rem; }
      /* Buy bar sticks to the bottom on mobile so "Add" is always reachable. */
      .sf-pdp-buy { position: sticky; bottom: 0; background: var(--surface); padding: 10px 0; margin: 8px -14px 0; padding-left: 14px; padding-right: 14px; border-top: 1px solid var(--border); z-index: 5; }
    }

    /* Header — desktop-first. Translucent, blurred, with a hairline border
       so it reads as a premium website nav. Order: brand · nav · search · cart. */
    .sf-header {
      position: sticky; top: var(--mkt-bar-h, 0px); z-index: 50;
      background: color-mix(in srgb, var(--surface) 86%, transparent);
      -webkit-backdrop-filter: saturate(160%) blur(12px); backdrop-filter: saturate(160%) blur(12px);
      border-bottom: 1px solid var(--border);
      transition: box-shadow .25s ease;
    }
    /* Condensed (scrolled): the category row collapses to 0 height and the
       bar gains a soft shadow — logo·search·cart stay. The category nav
       animates its max-height/opacity so it tucks away smoothly instead of
       snapping. (Desktop only; mobile keeps the drawer.) */
    .sf-header.is-condensed { box-shadow: 0 6px 24px rgba(0,0,0,0.16); }
    .sf-header.is-condensed .sf-nav { max-height: 0; opacity: 0; padding-top: 0; margin-top: 0; border-top-color: transparent; pointer-events: none; overflow: hidden; }
    /* Two-row header (Shopify Dawn pattern): row 1 = logo · search · cart;
       row 2 = full-width category nav. flex-wrap + the nav's flex-basis:100%
       (order:10, so it wraps LAST) put the nav on its own line — it can
       never cross over / hide behind the search box because they're on
       different rows, independent of font load or category count. */
    .sf-header-inner { max-width: 1280px; margin: 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px; padding: 11px 24px; }
    .sf-burger { display: none; order: 0; background: none; border: none; color: var(--text); cursor: pointer; padding: 6px; margin: -6px 0 -6px -6px; border-radius: 8px; }
    .sf-burger:hover { background: var(--surface2); }
    .sf-brand { order: 1; display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); flex: 0 0 auto; }
    /* Logo sizes by HEIGHT with auto width + contain, so square icons AND
       wide wordmarks both show fully (no crop). max-width caps a very wide
       logo so it can't push the nav off-row. The monogram fallback stays a
       square chip (it's a generated icon, not a photo). */
    .sf-logo-wrap { flex: 0 0 auto; display: inline-flex; align-items: center; transition: transform .15s; }
    .sf-brand:hover .sf-logo-wrap { transform: translateY(-1px); }
    /* Aspect-aware: wide logos are width-driven; square + tall (portrait)
       logos get more height so they don't look tiny in the bar. */
    .sf-logo { width: auto; object-fit: contain; display: block; border-radius: 8px; height: 60px; max-width: 280px; }
    .sf-logo.logo--wide { height: 74px; max-width: 440px; }
    .sf-logo.logo--square { height: 64px; max-width: 180px; }
    .sf-logo.logo--tall { height: 70px; max-width: 120px; }
    .sf-logo-mono {
      width: 54px; height: 54px; border-radius: 14px; display: grid; place-items: center;
      background: var(--surface2); border: 1px solid var(--border); box-shadow: 0 2px 12px rgba(0,0,0,0.14);
      font: 800 1.7rem var(--font-display); color: var(--accent);
    }
    /* Category nav: its OWN full-width row (order:10 wraps it below the
       brand/search/cart line; flex-basis:100% claims the whole row). A top
       border separates it as a secondary bar. Items still don't wrap — the
       Priority+ "More" collapses any that don't fit, but with the full row
       width that's now rare. */
    .sf-nav { order: 10; flex: 0 0 100%; display: flex; align-items: center; gap: 2px; position: relative; min-width: 0; flex-wrap: nowrap; overflow: visible; border-top: 1px solid var(--border); padding-top: 7px; margin-top: 2px; max-height: 60px; opacity: 1; transition: max-height .28s ease, opacity .2s ease, padding-top .28s ease, margin-top .28s ease; }
    .sf-nav-item { position: relative; }
    .sf-nav-link { position: relative; display: inline-flex; align-items: center; gap: 4px; padding: 9px 13px; color: var(--text); text-decoration: none; font: 600 0.94rem var(--font-body); border-radius: 9px; white-space: nowrap; transition: background .15s, color .15s; }
    .sf-nav-link:hover { background: var(--surface2); color: var(--accent); }
    .sf-nav-link.active { color: var(--accent); }
    /* Animated active underline — grows from center; premium touch. */
    .sf-nav-link::after { content: ''; position: absolute; left: 13px; right: 13px; bottom: 3px; height: 2px; border-radius: 2px; background: var(--accent); transform: scaleX(0); transform-origin: center; transition: transform .2s ease; }
    .sf-nav-link.active::after { transform: scaleX(1); }
    .sf-nav-caret { opacity: 0.6; transition: transform .15s; }
    .sf-nav-link.open .sf-nav-caret { transform: rotate(180deg); }
    .sf-dropdown { position: absolute; top: calc(100% + 6px); left: 0; min-width: 210px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 16px 40px rgba(0,0,0,0.28); padding: 7px; opacity: 0; visibility: hidden; transform: translateY(6px); transition: all .16s; z-index: 60; }
    .sf-nav-item:hover .sf-dropdown, .sf-nav-item:focus-within .sf-dropdown { opacity: 1; visibility: visible; transform: translateY(0); }
    .sf-dropdown-link { display: block; padding: 9px 12px; color: var(--text); text-decoration: none; font: 500 0.9rem var(--font-body); border-radius: 8px; }
    .sf-dropdown-link:hover { background: var(--surface2); color: var(--accent); }
    /* Priority+ overflow: categories that don't fit collapse into a "More"
       dropdown (BBC/Shopify pattern) instead of spilling behind the search
       box. The dropdown right-aligns so it never overflows the viewport,
       and groups each overflow category with its subcategories. */
    .sf-dropdown--more { right: 0; left: auto; min-width: 240px; max-height: 72vh; overflow-y: auto; }
    .sf-more-group + .sf-more-group { border-top: 1px solid var(--border); margin-top: 5px; padding-top: 5px; }
    .sf-more-head { font-weight: 700; }
    .sf-more-sub { padding-left: 24px; font-weight: 500; color: var(--text-secondary); }
    /* Hidden, out-of-flow mirror of the FULL category list. Rendered at the
       real link size so we can measure how many fit, then collapse the rest.
       visibility:hidden (not display:none) so children keep real widths. */
    .sf-nav-measure { position: absolute; left: 0; top: 0; display: flex; align-items: center; gap: 2px; white-space: nowrap; visibility: hidden; pointer-events: none; z-index: -1; }
    /* === Mega-menu === full-width subcategory panel anchored to the nav
       row. Opens on hover/focus of a category that has subcategories. The
       left side is a multi-column subcategory grid; the right is an optional
       feature card using the category image. */
    .sf-mega {
      position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
      width: max-content; max-width: min(720px, 92vw);
      background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.30); padding: 16px;
      opacity: 0; visibility: hidden; transform: translateY(8px);
      transition: opacity .18s ease, transform .18s ease, visibility .18s;
    }
    .sf-mega.open { opacity: 1; visibility: visible; transform: translateY(0); }
    /* Right-aligned variant for triggers near the row's right edge so the
       panel never overflows the viewport. */
    .sf-mega.align-right { left: auto; right: 0; }
    .sf-mega-inner { display: flex; gap: 20px; align-items: stretch; }
    .sf-mega-cols { flex: 1 1 auto; min-width: 280px; display: grid; grid-template-columns: repeat(2, minmax(150px, 1fr)); gap: 2px 10px; align-content: start; }
    .sf-mega-link { display: block; padding: 9px 12px; border-radius: 9px; color: var(--text); text-decoration: none; font: 500 0.92rem var(--font-body); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background .14s, color .14s; }
    .sf-mega-link:hover { background: var(--surface2); color: var(--accent); }
    .sf-mega-all { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--accent); grid-column: 1 / -1; }
    .sf-mega-all svg { transition: transform .15s; }
    .sf-mega-all:hover svg { transform: translateX(3px); }
    .sf-mega-feature { flex: 0 0 240px; position: relative; border-radius: 12px; overflow: hidden; background-size: cover; background-position: center; min-height: 150px; text-decoration: none; }
    .sf-mega-feature::after { content: ''; position: absolute; inset: 0; background: linear-gradient(0deg, rgba(0,0,0,0.55), rgba(0,0,0,0.05)); }
    .sf-mega-feature-label { position: absolute; left: 14px; bottom: 12px; z-index: 1; color: #fff; font: 700 1.05rem var(--font-display); text-shadow: 0 2px 10px rgba(0,0,0,0.5); }
    /* Search fills row 1 between the logo and cart (the nav moved to its
       own row), capped so it doesn't get absurdly wide on large screens. */
    .sf-search-wrap { order: 3; position: relative; flex: 1 1 auto; min-width: 0; max-width: 460px; margin-left: auto; }
    .sf-search { display: flex; align-items: center; gap: 8px; width: 100%; background: var(--surface2); border: 1px solid var(--border); border-radius: 999px; padding: 9px 16px; color: var(--text-muted); transition: border-color .15s, box-shadow .15s; }
    .sf-search-wrap.open .sf-search, .sf-search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
    .sf-search input { background: none; border: none; outline: none; color: var(--text); font: 500 0.92rem var(--font-body); width: 100%; flex: 1; min-width: 0; }
    .sf-search input::-webkit-search-cancel-button { display: none; }
    .sf-search-clear { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 18px; line-height: 1; padding: 0 2px; }
    /* Live results panel — desktop dropdown */
    .sf-search-panel {
      position: absolute; top: calc(100% + 8px); right: 0; width: 420px; max-width: 90vw; max-height: 72vh; overflow-y: auto;
      background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
      box-shadow: 0 20px 50px rgba(0,0,0,0.32); padding: 8px; z-index: 70;
    }
    .sf-sp-close { display: none; }
    .sf-sp-section { padding: 6px 4px; }
    .sf-sp-section + .sf-sp-section { border-top: 1px solid var(--border); margin-top: 4px; }
    .sf-sp-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; font: 700 0.72rem var(--font-body); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
    .sf-sp-clear { background: none; border: none; color: var(--accent); font: 600 0.74rem var(--font-body); cursor: pointer; text-transform: none; letter-spacing: 0; }
    .sf-sp-chips { display: flex; flex-wrap: wrap; gap: 7px; padding: 4px 8px 6px; }
    .sf-sp-chip { background: var(--surface2); border: 1px solid var(--border); border-radius: 999px; padding: 7px 13px; color: var(--text); font: 500 0.86rem var(--font-body); cursor: pointer; }
    .sf-sp-chip:hover { border-color: var(--accent); color: var(--accent); }
    .sf-sp-cat { display: flex; align-items: center; gap: 10px; width: 100%; background: none; border: none; padding: 9px 8px; border-radius: 9px; color: var(--text); font: 600 0.92rem var(--font-body); cursor: pointer; text-align: left; }
    .sf-sp-cat svg { color: var(--text-muted); flex: 0 0 auto; }
    .sf-sp-cat:hover { background: var(--surface2); }
    .sf-sp-item { display: flex; align-items: center; gap: 12px; width: 100%; background: none; border: none; padding: 8px; border-radius: 10px; cursor: pointer; text-align: left; }
    .sf-sp-item.active, .sf-sp-item:hover { background: var(--surface2); }
    .sf-sp-thumb { flex: 0 0 auto; width: 48px; height: 48px; border-radius: 9px; background: var(--surface3, var(--surface2)); background-size: cover; background-position: center; display: grid; place-items: center; color: var(--text-muted); font: 700 1.1rem var(--font-display); overflow: hidden; }
    .sf-sp-info { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
    .sf-sp-name { font: 600 0.94rem var(--font-body); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sf-sp-meta { display: flex; align-items: center; gap: 8px; }
    .sf-sp-price { font: 700 0.88rem var(--font-body); color: var(--accent-text); white-space: nowrap; font-variant-numeric: tabular-nums; }
    .sf-sp-cat-tag { font: 500 0.76rem var(--font-body); color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sf-sp-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 30px 16px; color: var(--text-muted); text-align: center; font: 500 0.92rem var(--font-body); }
    .sf-sp-all { display: flex; align-items: center; justify-content: center; gap: 7px; width: 100%; margin-top: 4px; padding: 12px; border: none; border-top: 1px solid var(--border); background: none; color: var(--accent); font: 700 0.9rem var(--font-body); cursor: pointer; border-radius: 0 0 8px 8px; }
    .sf-sp-all:hover { background: var(--accent-dim); }
    /* === Search: a header icon + full-screen overlay, identical across ALL
       advanced themes (storefront / editorial / swift / booking). Tap the
       magnifier → focused full-screen search; reuses the shared input + results
       panel. .is-full styles the teleported layer (see below). */
    .sf-search-btn {
      order: 3; flex: 0 0 auto; cursor: pointer;
      width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
      background: var(--surface2); border: 1px solid var(--border); color: var(--text);
      transition: border-color .15s, color .15s;
    }
    .sf-search-btn:hover { border-color: var(--accent); color: var(--accent); }

    /* Install-app button — accent-tinted so it reads as a soft CTA, keeping
       the 44px target + 12px radius so the header row stays in rhythm. Two
       copies live in the DOM and swap by viewport (a single node can't sit in
       both flex zones): DESKTOP shows the labelled copy on the right of the
       action cluster (.sf-install--act); MOBILE shows an icon-only copy in the
       lead zone, right beside the hamburger (.sf-install--lead). */
    .sf-install {
      flex: 0 0 auto; cursor: pointer;
      height: 44px; min-width: 44px; border-radius: 12px;
      align-items: center; justify-content: center; gap: 7px;
      background: var(--accent-dim); border: 1px solid var(--accent); color: var(--accent);
      font: 700 12.5px var(--font-body); white-space: nowrap;
      transition: filter .15s, box-shadow .15s, transform .15s;
    }
    .sf-install:hover { filter: brightness(1.08); box-shadow: 0 4px 14px var(--accent-shadow); }
    .sf-install:active { transform: translateY(1px); }
    /* Desktop default: labelled copy on the right; lead copy hidden. */
    .sf-install--act  { display: inline-flex; order: 2; padding: 0 15px 0 13px; }
    .sf-install--lead { display: none; }

    /* === Search suggestions: a TELEPORTED overlay (portal) at <body> level ===
       Rendering the suggestions inside the blurred header let a browser
       backdrop-filter compositing quirk paint them BEHIND the hero/banner —
       z-index/isolation can't fix that. Teleporting them out of the header's
       stacking context entirely is the robust, industry-standard fix. */
    /* Search overlay, shared by ALL advanced themes (teleported to <body>).
       Mobile: edge-to-edge full-screen sheet — surface input bar on top, results
       panel fills the rest. Desktop: a dropdown card anchored directly under the
       header search (top-right), so suggestions read as "below the search box".
       NOTE: the obar + panel must be position:static here — a global
       .sf-search-panel rule sets position:absolute, which would otherwise drop
       the panel a full viewport-height below the fold (off-screen). */
    .sf-search-layer { position: fixed; inset: 0; z-index: 1000; }
    .sf-search-layer.is-full {
      display: flex; flex-direction: column; background: var(--bg);
      animation: catSheetIn .2s var(--ease, ease);
    }
    .sf-search-layer.is-full .sf-search-obar {
      flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
      padding: calc(10px + env(safe-area-inset-top)) 14px 10px;
      background: var(--surface); border-bottom: 1px solid var(--border);
    }
    .sf-search-layer.is-full .sf-search-obar input {
      flex: 1; min-width: 0; background: none; border: none; outline: none;
      color: var(--text); font: 500 1.02rem var(--font-body);
    }
    .sf-search-layer.is-full .sf-search-obar input::-webkit-search-cancel-button { display: none; }
    .sf-search-layer.is-full .sf-search-panel {
      position: static; width: auto; max-width: none;
      flex: 1 1 auto; max-height: none; border: none; border-radius: 0;
      box-shadow: none; background: var(--bg);
      padding: 8px 8px calc(16px + env(safe-area-inset-bottom));
    }
    .sf-search-back { background: none; border: none; color: var(--text); cursor: pointer; padding: 2px; display: grid; place-items: center; flex: 0 0 auto; }
    @media (min-width: 720px) {
      /* Centered command palette: a dim, blurred backdrop with a big search box
         in the upper-centre (Algolia / Vercel / Linear cmd-K pattern). The
         full-screen layer is the click-catcher — tap the faded background (or
         press Esc) to close. obar + panel stack as one centred, elevated card. */
      .sf-search-layer.is-full {
        background: rgba(0,0,0,0.5);
        -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
        align-items: center; justify-content: flex-start;
        padding: 12vh 16px 24px; overflow-y: auto;
      }
      .sf-search-layer.is-full .sf-search-obar {
        width: 100%; max-width: 640px; padding: 18px 20px; gap: 14px;
        border: 1px solid var(--border);
        border-radius: var(--radius) var(--radius) 0 0;
        box-shadow: 0 30px 80px rgba(0,0,0,0.45);
      }
      .sf-search-layer.is-full .sf-search-obar input { font-size: 1.2rem; }
      .sf-search-layer.is-full .sf-search-panel {
        width: 100%; max-width: 640px; flex: 0 0 auto;
        max-height: min(60vh, 560px); overflow-y: auto;
        background: var(--surface); border: 1px solid var(--border); border-top: none;
        border-radius: 0 0 var(--radius) var(--radius);
        box-shadow: 0 30px 80px rgba(0,0,0,0.45); padding: 10px;
      }
    }
    .sf-cart { order: 4; position: relative; background: var(--surface2); border: 1px solid var(--border); color: var(--text); cursor: pointer; width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; flex: 0 0 auto; transition: border-color .15s, color .15s; }
    .sf-cart:hover { border-color: var(--accent); color: var(--accent); }
    .sf-cart-n { position: absolute; top: -5px; right: -5px; background: var(--accent); color: #fff; border-radius: 999px; font: 700 10.5px var(--font-body); min-width: 18px; height: 18px; display: grid; place-items: center; padding: 0 5px; box-shadow: 0 2px 6px var(--accent-shadow); }
    /* Header right-side spacer — desktop hidden; on mobile it balances the
       burger (equal width) so the centred logo stays dead-centre even when the
       cart button is hidden. Toggled by x-show (only when cart is hidden). */
    .sf-head-spacer { display: none; }
    /* Header side-zone wrappers. Desktop: transparent (display:contents) so the
       burger / search / cart participate in .sf-header-inner directly, exactly
       as before. Mobile (≤719px) turns them into equal-weight flex zones that
       keep the logo centred — see the mobile block below. */
    .sf-head-lead { display: contents; }
    /* Search + cart as ONE tight cluster, pinned to the right end, at every
       breakpoint (the mobile block below only tweaks sizes / zone weights).
       order:2 keeps it after the logo (.sf-brand order 1) and before the
       wrapping nav row (.sf-nav order 10). */
    .sf-head-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: 0 0 auto; order: 2; }
    /* Hero carousel. The box matches the recommended banner ratio
       (1600×600 = 8:3), so `cover` fills it with NO crop at any width and
       it scales responsively. max-height keeps it sane on ultra-wide
       screens (only there does a sliver crop). */
    /* isolation:isolate — contain the hero's composite layers (the slides
       animate opacity → GPU layers) in their OWN stacking context below the
       header. Without this, Chromium/WebKit's backdrop-filter bug paints the
       blurred header (and its overflowing search dropdown) BENEATH the
       composited banner, so the search suggestions vanish behind the hero. */
    .sf-hero { position: relative; isolation: isolate; width: 100%; aspect-ratio: 1600 / 600; max-height: 600px; background: var(--surface2); overflow: hidden; }
    /* True crossfade: ALL slides stay mounted and stacked; visibility is
       driven by opacity + z-index (NOT display/x-show), so stacking order
       is controlled by z-index, never by DOM paint order. This fixes both
       "last slide shows first" (absolute + x-show let the last DOM node
       paint on top during the transition) and "image doesn't change" (the
       display none/block toggle racing the fade). The active slide is
       opaque and on top; the rest are transparent and behind, but still
       laid out — so order is always correct. */
    /* contain (not cover): show the WHOLE uploaded banner, never cropped —
       merchants upload arbitrary ratios, so cover was clipping their art.
       The --surface2 hero background fills any letterbox margin. */
    .sf-hero-slide { position: absolute; inset: 0; background-size: contain; background-repeat: no-repeat; background-position: center; opacity: 0; z-index: 0; transition: opacity 0.5s ease; pointer-events: none; }
    .sf-hero-slide.is-active { opacity: 1; z-index: 1; pointer-events: auto; }
    /* No scrim by default — an image-only slide shows at full brightness.
       The legibility gradient is applied ONLY when the slide has overlay
       text (.has-text), and only over the text side. */
    .sf-hero-overlay { position: absolute; inset: 0; display: flex; align-items: center; background: none; }
    .sf-hero-overlay.has-text { background: linear-gradient(90deg, rgba(0,0,0,0.52) 0%, rgba(0,0,0,0.12) 48%, transparent 72%); }
    .sf-hero-text { max-width: 1280px; width: 100%; margin: 0 auto; padding: 0 48px; color: #fff; }
    .sf-hero-text h1 { font: 800 2.6rem var(--font-display); letter-spacing: -0.02em; margin: 0 0 12px; max-width: 620px; text-shadow: 0 2px 18px rgba(0,0,0,0.4); }
    .sf-hero-text p { font: 500 1.1rem var(--font-body); margin: 0 0 22px; max-width: 520px; opacity: 0.95; text-shadow: 0 2px 12px rgba(0,0,0,0.4); }
    .sf-hero-cta { display: inline-block; background: var(--accent); color: #fff; text-decoration: none; padding: 13px 30px; border-radius: 999px; font: 700 0.98rem var(--font-body); box-shadow: 0 8px 24px var(--accent-shadow); }
    .sf-hero-cta:hover { filter: brightness(1.06); }
    .sf-hero-dots { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 5; }
    .sf-hero-dot { width: 9px; height: 9px; border-radius: 50%; border: none; background: rgba(255,255,255,0.5); cursor: pointer; padding: 0; }
    .sf-hero-dot.active { background: #fff; width: 26px; border-radius: 999px; }
    /* Category-page head */
    .sf-cat-head { background: var(--surface2); background-size: cover; background-position: center; position: relative; isolation: isolate; }
    .sf-cat-head.has-img::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.25), rgba(0,0,0,0.55)); }
    .sf-cat-head-inner { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; padding: 36px 24px; }
    .sf-cat-head.has-img .sf-cat-head-inner, .sf-cat-head.has-img .sf-crumb a, .sf-cat-head.has-img .sf-crumb span { color: #fff; }
    .sf-crumb { font: 500 0.85rem var(--font-body); color: var(--text-muted); margin-bottom: 8px; display: flex; gap: 8px; }
    .sf-crumb a { color: inherit; text-decoration: none; } .sf-crumb a:hover { color: var(--accent); }
    .sf-cat-head h1 { font: 800 2rem var(--font-display); letter-spacing: -0.02em; margin: 0; }
    /* Tiles */
    .sf-tiles-wrap { max-width: 1280px; margin: 0 auto; padding: 34px 24px 0; }
    .sf-section-title { font: 800 1.5rem var(--font-display); letter-spacing: -0.01em; margin: 0 0 18px; }
    .sf-products-title { max-width: 1280px; margin: 34px auto 0; padding: 0 24px; }
    /* Category tiles: ONE grid rule. The base column counts live on <html>
       (a bare html {} so any html[data-layout] override outranks them
       regardless of order); a layout that used to force its own column count
       sets --layout-tiles, which — exactly like the old scoped .sf-tiles rule
       it replaces — applies to the sub-category grid too. */
    html { --tiles-cols: 4; --tiles-sub-cols: 6; --tiles-gap: 18px; }
    .sf-tiles { display: grid; grid-template-columns: repeat(var(--layout-tiles, var(--tiles-cols)), 1fr); gap: var(--tiles-gap); }
    .sf-tiles--sub { grid-template-columns: repeat(var(--layout-tiles, var(--tiles-sub-cols)), 1fr); }
    .sf-tile { text-decoration: none; color: var(--text); display: flex; flex-direction: column; gap: 10px; }
    .sf-tile-img { aspect-ratio: 1/1; border-radius: 14px; background: var(--surface2); background-size: cover; background-position: center; display: grid; place-items: center; font: 800 2rem var(--font-display); color: var(--text-muted); overflow: hidden; transition: transform .2s, box-shadow .2s; }
    .sf-tile:hover .sf-tile-img { transform: translateY(-4px); box-shadow: 0 14px 30px rgba(0,0,0,0.25); }
    .sf-tile-name { font: 600 0.98rem var(--font-body); text-align: center; }
    .sf-tiles--sub .sf-tile-img { aspect-ratio: 4/3; border-radius: 12px; }
    .sf-tiles--sub .sf-tile-name { font-size: 0.88rem; }

    /* === Curated homepage: category carousels (rails) === shared by all
       three website layouts (Modern/Luxe/Swift). Each rail is a category
       heading + "View all" and a horizontal swipe strip of product cards. */
    .sf-rails { max-width: 1280px; margin: 0 auto; padding: 8px 24px 0; }
    .sf-rail { margin-top: 34px; }
    .sf-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
    .sf-rail-title { font: 800 1.4rem var(--font-display); letter-spacing: -0.01em; margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .sf-rail-all { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; color: var(--accent); text-decoration: none; font: 700 0.86rem var(--font-body); }
    .sf-rail-all svg { transition: transform .15s; }
    .sf-rail-all:hover svg { transform: translateX(3px); }
    /* The track: horizontal scroll-snap strip. Cards are fixed-width so the
       strip reads as a rail, not a wrapping grid. Reuses .no-scrollbar +
       _attachHorizontalScroll (drag / wheel). */
    .sf-rail-frame { position: relative; }
    .sf-rail-track { display: flex; gap: 16px; overflow-x: auto; padding: 2px 2px 10px; scroll-snap-type: x proximity; }
    .sf-rail-track.is-draggable { cursor: grab; }
    .sf-rail-track.is-dragging { cursor: grabbing; user-select: none; }
    /* Product-card images/links → suppress native drag so grab-to-scroll owns it
       (belt-and-suspenders alongside the dragstart guard in _attachHorizontalScroll). */
    .sf-rail-track img, .sf-rail-track a { -webkit-user-drag: none; }
    .sf-rail-card { flex: 0 0 200px; scroll-snap-align: start; margin-bottom: 0; }
    /* Editorial: wider, borderless rail cards to match its airy product grid. */
    html[data-layout="editorial"] .sf-rail-title { font-weight: 500; }
    html[data-layout="editorial"] .sf-rail-card { flex-basis: 220px; }
    /* Swift: tighter rail cards, smaller heading — dense quick-commerce. */
    html[data-layout="swift"] .sf-rail { margin-top: var(--space-6); }
    html[data-layout="swift"] .sf-rail-title { font-size: 1.15rem; }
    html[data-layout="swift"] .sf-rail-card { flex-basis: 168px; }
    @media (max-width: 719px) {
      .sf-rails { padding: 4px 14px 0; }
      .sf-rail-track { gap: 12px; }
      .sf-rail-card { flex-basis: 156px; }
      /* 144px left a 120px footer — too narrow for a price and a control
         side by side, so swift alone kept wrapping. 168px matches its own
         desktop basis and clears the whole realistic price range. */
      html[data-layout="swift"] .sf-rail-card { flex-basis: 176px; }
      .sf-rail-title { font-size: 1.15rem; }
    }

    /* === "Shop by category" carousel (migration 157) === when the merchant
       sets store.category_layout='carousel', the .sf-tiles grid above is
       swapped for this single-line scroll rail (drag/wheel/touch via
       _attachHorizontalScroll + CSS scroll-snap). The inner .sf-tile markup is
       shared with the grid; only the track wrapper and a style modifier class
       (--cards | --circles | --hero | --pills) change. */
    .sf-catrail { display: flex; gap: var(--cat-gap); overflow-x: auto; padding: 2px 2px 12px;
                  scroll-snap-type: x proximity; scroll-padding-left: 2px;
                  --cat-gap: 18px; }
    .sf-catrail > .sf-tile { scroll-snap-align: start; margin-bottom: 0; }
    .sf-catrail.is-draggable { cursor: grab; }
    .sf-catrail.is-dragging { cursor: grabbing; user-select: none; }
    /* Tiles are <a> links → the browser natively drag-and-drops them on
       mouse-drag, which would preempt the grab-to-scroll. Disable native drag
       so _attachHorizontalScroll owns the gesture. */
    .sf-catrail .sf-tile, .sf-catrail .sf-tile-img { -webkit-user-drag: none; user-select: none; }
    .sf-catrail.is-dragging .sf-tile { pointer-events: none; }
    /* Frame: layout-transparent in grid mode; positioned anchor for the desktop
       prev/next arrow buttons in carousel mode. */
    .sf-catrail-frame { display: contents; }
    .sf-catrail-frame.is-carousel { display: block; position: relative; }
    /* Desktop arrow buttons (Amazon / Netflix pattern): floating circular
       controls that scroll ~a page and auto-hide at each end. Hidden on touch /
       phones, where native swipe is the affordance. */
    .sf-catrail-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 4;
      width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center;
      cursor: pointer; padding: 0; color: var(--text);
      background: var(--surface, #fff); border: 1px solid var(--border);
      box-shadow: 0 6px 18px rgba(0,0,0,0.18);
      transition: background .15s, color .15s, transform .15s, box-shadow .15s, opacity .15s; }
    .sf-catrail-nav:hover { background: var(--brand, var(--accent)); color: #fff; border-color: transparent;
      box-shadow: 0 8px 22px rgba(0,0,0,0.28); }
    .sf-catrail-nav:active { transform: translateY(-50%) scale(0.93); }
    .sf-catrail-nav:focus-visible { outline: 2px solid var(--brand, var(--accent)); outline-offset: 3px; }
    .sf-catrail-prev { left: -8px; }
    .sf-catrail-next { right: -8px; }
    /* Pills are a compact single row — scale the arrows down to stay proportional. */
    .sf-catrail-frame:has(.sf-catrail--pills) .sf-catrail-nav { width: 36px; height: 36px; }
    .sf-catrail-frame:has(.sf-catrail--pills) .sf-catrail-nav svg { width: 18px; height: 18px; }
    /* Arrows are a pointer affordance only — touch devices and phones swipe. */
    @media (hover: none), (max-width: 719px) { .sf-catrail-nav { display: none !important; } }
    @media (prefers-reduced-motion: reduce) { .sf-catrail-nav { transition: none; } }
    /* === Card width: cards-per-view, not vw === the shopper must always see a
       PART of the next card — that half-card is the only affordance a phone
       gives that the row scrolls (Blinkit / Swiggy / Amazon all size their rails
       this way). So the width is derived from --cat-per-view, a FRACTIONAL card
       count (2.4 = "two cards and a fat sliver"), against the track's own
       measured width — page padding and gaps are subtracted for us:

         width = (track - (N - 1) * gap) / N

       The old clamp(148px, 42vw, 176px) guessed from the VIEWPORT instead, so
       once the 14px page padding and the 12px gap were taken out only ~7px of
       card three survived on a 390px phone — a sliver that reads as a rendering
       flaw, not as "more". Swift looked right only because its own theme
       override happened to land near 34vw. Per-view numbers live in the
       breakpoint blocks below (mobile-first, unlike the rest of this file). */
    .sf-catrail--cards > .sf-tile,
    .sf-catrail--hero > .sf-tile {
      flex: 0 0 calc((100% - (var(--cat-per-view) - 1) * var(--cat-gap)) / var(--cat-per-view)); }
    .sf-catrail--cards { --cat-per-view: 2.4; }
    .sf-catrail--hero { --cat-per-view: 1.25; }
    @media (min-width: 720px) {
      .sf-catrail--cards { --cat-per-view: 4.5; }
      .sf-catrail--hero { --cat-per-view: 2.4; }
    }
    @media (min-width: 1024px) {
      .sf-catrail--cards { --cat-per-view: 6.5; }
      .sf-catrail--hero { --cat-per-view: 4.4; }
    }
    /* The style modifiers double the .sf-catrail class (specificity 0,0,3,0) so
       the chosen shape ALWAYS wins over per-theme .sf-tile-img overrides like
       html[data-layout="editorial"|"swift"|"booking"] .sf-tile-img (0,0,2,1) —
       otherwise circles render as rounded squares / ovals on those themes. */
    .sf-catrail.sf-catrail--cards .sf-tile-img { aspect-ratio: 1/1; }
    /* circles: round avatars, many visible — smallest vertical footprint. */
    .sf-catrail--circles > .sf-tile { flex: 0 0 112px; }
    .sf-catrail.sf-catrail--circles .sf-tile-img { border-radius: 50%; aspect-ratio: 1/1; }
    .sf-catrail--circles .sf-tile-name { font-size: 0.9rem; }
    /* hero: larger landscape cards with the name overlaid on a gradient. */
    .sf-catrail--hero > .sf-tile { position: relative; }
    .sf-catrail.sf-catrail--hero .sf-tile-img { aspect-ratio: 4/3; border-radius: 16px; }
    .sf-catrail.sf-catrail--hero .sf-tile-name { position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
        padding: 16px 16px 14px; color: #fff; text-align: left; font-size: 1.05rem; font-weight: 700;
        letter-spacing: -0.01em; background: linear-gradient(transparent, rgba(0,0,0,0.68)); }
    /* pills: compact text-forward chips with a small round thumbnail. */
    .sf-catrail--pills > .sf-tile { flex: 0 0 auto; flex-direction: row; align-items: center; gap: 11px;
        padding: 8px 18px 8px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface2);
        transition: border-color .18s, transform .18s; }
    .sf-catrail--pills > .sf-tile:hover { border-color: var(--accent); transform: translateY(-2px); }
    .sf-catrail.sf-catrail--pills .sf-tile-img { width: 42px; height: 42px; border-radius: 50%; aspect-ratio: 1/1; font-size: 1.05rem; }
    .sf-catrail--pills .sf-tile-name { font-size: 0.95rem; white-space: nowrap; }
    /* Some themes (editorial) render .sf-tile-name as a caption OVERLAID on the
       image; in the carousel that turns cards/circles/pills into "tombstones".
       Force the label BELOW the image for every style except hero (which overlays
       by design). Specificity 0,0,3,0 beats the theme's html[data-layout] rule. */
    .sf-catrail:not(.sf-catrail--hero) .sf-tile { gap: 10px; }
    .sf-catrail:not(.sf-catrail--hero) .sf-tile-name {
      position: static; padding: 0; background: none; color: var(--text);
      text-align: center; border-radius: 0; pointer-events: auto;
      /* Give the name real presence: full-contrast --text at 1rem/700, not the
         muted 0.84rem caption some themes set for the GRID. In a rail the label
         is the only wayfinding a shopper gets — the image is a mood, the word is
         the destination — and it is read while the thumb is already moving.
         Themes that overlay the caption (editorial, showroom) keep their display
         font here; only the weight and contrast are normalised. */
      font-size: 1rem; font-weight: 700; letter-spacing: -0.005em; line-height: 1.25; }
    /* Two lines, then ellipsis. Cards sit in a flex row, so a three-line name
       would leave one card taller than its neighbours (ragged baselines) and eat
       the vertical room the peek is buying us. Ellipsis on the PEEKING card is a
       feature: truncated text reads as "more ahead" even harder than a cut image. */
    .sf-catrail:not(.sf-catrail--hero):not(.sf-catrail--pills) .sf-tile-name {
      display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
    /* Circles are only ~88px wide — 1rem/700 wraps every two-word category. */
    .sf-catrail.sf-catrail--circles .sf-tile-name { font-size: 0.92rem; }
    /* Per-theme tweaks mirroring the .sf-rail overrides. */
    /* Per-theme tweaks set --cat-per-view, never flex-basis: a theme selector
       (0,0,2,1) outranks the breakpoint rules above (0,0,1,0), and a media query
       adds no specificity — a basis set here would win at EVERY width and undo
       the phone sizing. That is exactly how these themes drifted apart from
       Swift. Confined to >=1024px so the phone/tablet counts stay universal. */
    html[data-layout="editorial"] .sf-catrail--cards .sf-tile-img { border-radius: var(--ed-radius-tile); }
    html[data-layout="swift"] .sf-catrail { --cat-gap: 12px; }
    html[data-layout="swift"] .sf-catrail--circles > .sf-tile { flex-basis: 96px; }
    @media (min-width: 1024px) {
      html[data-layout="editorial"] .sf-catrail--cards { --cat-per-view: 5.7; }
      html[data-layout="swift"] .sf-catrail--cards { --cat-per-view: 7.6; }
    }
    @media (max-width: 719px) {
      .sf-catrail { --cat-gap: 12px; }
      .sf-catrail--circles > .sf-tile { flex-basis: 88px; }
    }

    /* Screen-reader-only utility: keeps the server-rendered <h1>/intro in the
       DOM for crawlers + assistive tech without visually duplicating the
       header brand. */
    .sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

    /* Server-rendered FAQ block (matches the FAQPage JSON-LD). */
    .sf-faq { max-width: 760px; margin: 48px auto 0; padding: 0 24px; }
    .sf-faq-title { font: 700 1.5rem var(--font-display); margin: 0 0 16px; color: var(--text); }
    .sf-faq-item { border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; background: var(--surface); }
    .sf-faq-item summary { cursor: pointer; font: 600 1rem var(--font-body); color: var(--text); list-style: none; }
    .sf-faq-item summary::-webkit-details-marker { display: none; }
    .sf-faq-item summary::after { content: '+'; float: right; color: var(--text-muted); font-weight: 400; }
    .sf-faq-item[open] summary::after { content: '–'; }
    .sf-faq-item p { margin: 10px 0 0; color: var(--text-secondary); line-height: 1.6; }

    /* Footer. NOTE: must override the generic base `footer { max-width:480px;
       margin:32px auto; padding; text-align:center }` rule — without these
       resets the storefront footer renders as a narrow 480px centered
       column. Full-bleed band; inner cols/bar cap + center at 1280. */
    .sf-footer { max-width: none; width: 100%; margin: 56px 0 0; padding: 0; text-align: left; color: var(--text); font-size: 1rem; background: var(--surface); border-top: 1px solid var(--border); }
    .sf-footer-cols { max-width: 1280px; margin: 0 auto; padding: 52px 24px 40px; display: grid; grid-template-columns: 1.7fr 1fr 1fr; gap: 48px; }
    .sf-footer-col { min-width: 0; }
    .sf-footer-col h4 { font: 700 0.74rem var(--font-body); text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); margin: 0 0 16px; }
    .sf-footer-col a { display: block; color: var(--text-secondary); text-decoration: none; font: 500 0.92rem var(--font-body); padding: 6px 0; transition: color .15s; }
    .sf-footer-col a:hover { color: var(--accent); }
    .sf-footer-brand { display: flex; align-items: center; gap: 13px; margin-bottom: 16px; }
    /* Footer logo: same height-based, contain sizing so wide logos aren't
       cropped. The wrap only frames the monogram fallback. */
    .sf-footer-logo-wrap { flex: 0 0 auto; display: inline-flex; align-items: center; }
    .sf-footer-logo { width: auto; object-fit: contain; display: block; border-radius: 8px; height: 56px; max-width: 260px; }
    .sf-footer-logo.logo--wide { height: 64px; max-width: 340px; }
    .sf-footer-logo.logo--square { height: 58px; max-width: 160px; }
    .sf-footer-logo.logo--tall { height: 64px; max-width: 110px; }
    .sf-footer-mono { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; background: var(--surface2); border: 1px solid var(--border); font: 800 1.4rem var(--font-display); color: var(--accent); }
    .sf-footer-name { font: 700 1.3rem var(--font-display); letter-spacing: -0.015em; color: var(--text); line-height: 1.15; }
    .sf-footer-tag { color: var(--text-secondary); font: 500 0.92rem var(--font-body); margin: 0 0 14px; max-width: 340px; line-height: 1.55; }
    .sf-footer-addr { color: var(--text-muted); font: 500 0.86rem var(--font-body); margin: 3px 0; line-height: 1.5; }
    /* "Follow us" social row (store social links, migration 092). Monochrome
       icons that inherit the footer text colour and adopt the accent on hover —
       one cohesive set rather than brand-coloured blobs. */
    .sf-social { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 18px; }
    /* Scoped past ".sf-footer-col a" (which sets display:block + padding) so the
       grid-centring actually applies — otherwise the icons sit off-centre. */
    .sf-social .sf-social-link {
      display: inline-grid; place-items: center; box-sizing: border-box; flex: 0 0 auto;
      width: 38px; height: 38px; padding: 0; line-height: 0;
      border: 1px solid var(--border); border-radius: 10px; color: var(--text-secondary);
      transition: color .15s, border-color .15s, background .15s, transform .15s;
    }
    .sf-social .sf-social-link:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-2px); }
    .sf-social .sf-social-link svg { width: 18px; height: 18px; display: block; }
    .sf-footer-bar { max-width: 1280px; margin: 0 auto; padding: 18px 24px; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; font: 500 0.82rem var(--font-body); color: var(--text-muted); }
    .sf-copy { font: 500 0.82rem var(--font-body); color: var(--text-muted); }
    .sf-footer-powered { display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap; }
    /* "Built with ZSite" — a theme-aware pill: the lightning mark sits in
       an accent gradient disc, the wordmark accents on hover with a soft
       accent glow. Reads well on dark or light themes (all vars). */
    .sf-built {
      display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px 6px 7px;
      border-radius: 999px; text-decoration: none; color: var(--text-secondary);
      background: var(--surface2); border: 1px solid var(--border);
      font: 600 0.8rem var(--font-body); transition: color .18s, border-color .18s, box-shadow .18s, transform .18s;
    }
    .sf-built:hover { color: var(--text); border-color: var(--accent); box-shadow: 0 6px 20px var(--accent-shadow); transform: translateY(-1px); }
    .sf-built-mark {
      width: 24px; height: 24px; border-radius: 50%; flex: 0 0 auto; display: grid; place-items: center;
      color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent-light));
      box-shadow: 0 2px 8px var(--accent-shadow);
    }
    .sf-built-mark svg { width: 13px; height: 13px; }
    .sf-built-text b { color: var(--accent); font-weight: 800; letter-spacing: -0.01em; }
    .sf-built:hover .sf-built-mark { transform: rotate(-8deg) scale(1.05); transition: transform .18s; }
    .sf-managed { font: 500 0.8rem var(--font-body); color: var(--text-muted); }
    .sf-managed b { color: var(--text-secondary); font-weight: 700; }
    /* Accepted-payments strip. Badges sit on white chips (like real
       payment marks) so brand colors read on any theme background. */
    .sf-pay { max-width: 1280px; margin: 0 auto; padding: 16px 24px; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; }
    .sf-pay-label { font: 600 0.74rem var(--font-body); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
    .sf-pay-badges { display: flex; gap: 8px; flex-wrap: wrap; }
    .sf-pay-badge { height: 30px; min-width: 46px; padding: 0 9px; background: #fff; border: 1px solid rgba(0,0,0,0.1); border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.14); }
    .pb-visa { font: 800 italic 14px Arial, Helvetica, sans-serif; color: #1a1f71; letter-spacing: 0.5px; }
    .pb-mc { display: block; }
    .pb-rupay { font: 800 12.5px Arial, sans-serif; letter-spacing: -0.2px; } .pb-rupay b { color: #11385b; } .pb-rupay em { color: #ee7f25; font-style: normal; }
    .pb-upi { font: 800 13px Arial, sans-serif; letter-spacing: 0.3px; } .pb-upi i { color: #ed752e; font-style: normal; } .pb-upi b { color: #0c8540; }
    .pb-cod { font: 700 11px var(--font-body); color: #1f7a3d; }
    /* --- Mobile collapse (storefront is desktop-first) --- */
    @media (max-width: 719px) {
      html[data-layout="storefront"] { --layout-cols: 2; --layout-gap: 12px; }
      /* Mobile header: two rows — [☰  brand  cart] then a full-width
         search bar. The nav becomes a tap-to-open drawer below. */
      .sf-header-inner { padding: 10px 14px; gap: 12px; flex-wrap: wrap; row-gap: 10px; }
      /* THE LOGO IS DEAD-CENTRE ON PHONES, and stays there by construction.
         Two equal-weight side zones (flex: 1 1 0 → identical widths whatever
         they hold) flank an auto-width brand. The brand is therefore centred in
         the viewport whether the right zone holds three icons, one, or none.
         min-width: 0 lets a zone shrink rather than grow past its share, which
         is what keeps the two equal.

         HISTORY, because this flipped twice. It was centred; then the delivery
         chip plus the account button pushed the right zone past its share, the
         zone grew, and the logo drifted into the burger — so it was anchored
         left. But the bottom navigation then took search, cart AND account out
         of the header entirely, leaving the right zone EMPTY, and a left-anchored
         logo stranded the whole right half of the bar. Centring is correct again
         because the cause of the drift is gone. The invariant to hold is the one
         thing that broke it: nothing goes in .sf-head-actions that can outgrow
         half the bar minus the brand. There is a browser check on this. */
      .sf-head-lead { display: flex; align-items: center; gap: 8px; flex: 1 1 0; min-width: 0; justify-content: flex-start; order: 0; }
      .sf-head-actions { display: flex; align-items: center; gap: 8px; flex: 1 1 0; min-width: 0; justify-content: flex-end; order: 2; }
      .sf-burger { display: grid; place-items: center; width: 40px; height: 40px; padding: 0; margin: 0; flex: 0 0 40px; }
      /* Logo only (no title), dead-centre between the two equal side zones. */
      .sf-brand { order: 1; flex: 0 1 auto; min-width: 0; justify-content: center; }
      /* The phantom spacer that used to balance the burger when the cart was
         hidden is no longer needed: equal-weight zones balance themselves, and
         with the bottom nav carrying the actions the right zone is often empty
         anyway. A 40px void would now bias the logo left rather than centre it. */
      .sf-head-spacer { display: none; }
      .sf-logo.logo--wide { height: 52px; max-width: 240px; }
      .sf-logo.logo--square { height: 48px; max-width: 130px; }
      .sf-logo.logo--tall { height: 52px; max-width: 90px; }
      .sf-logo-mono { width: 44px; height: 44px; border-radius: 12px; font-size: 1.4rem; }
      .sf-cart { width: 40px; height: 40px; }
      /* Swap copies: hide the right-side labelled button, show the icon-only
         copy beside the burger at the slimmer 40px mobile target. */
      .sf-install--act  { display: none; }
      .sf-install--lead { display: inline-flex; order: 0; height: 40px; min-width: 40px; padding: 0 9px; }
      .sf-search-wrap { order: 3; flex: 1 1 100%; flex-basis: 100%; width: 100%; max-width: none; margin-left: 0; }
      .sf-search { width: 100%; padding: 11px 16px; }
      .sf-search input { width: 100%; flex: 1; }
      /* Mobile: full-width dropdown under the (sticky) search bar — the
         header input stays visible above it. A "Done" bar lets the
         customer dismiss without picking. */
      .sf-search-panel { left: 0; right: 0; width: 100%; max-width: none; top: calc(100% + 8px); max-height: 74vh; border-radius: 14px; }
      .sf-sp-close { display: flex; align-items: center; justify-content: flex-end; width: 100%; background: none; border: none; color: var(--accent); font: 700 0.86rem var(--font-body); cursor: pointer; padding: 4px 8px 6px; }
      .sf-sp-item { padding: 10px 8px; }
      .sf-sp-thumb { width: 54px; height: 54px; }
      /* Mobile drawer nav: reset the desktop condense/transition props
         (max-height/opacity) so the open drawer is never clipped, and the
         condensed-on-scroll behavior is desktop-only. */
      .sf-nav { order: 4; flex-basis: 100%; display: none; flex-direction: column; align-items: stretch; gap: 2px; padding-top: 4px; border-top: 1px solid var(--border); margin-top: 2px; max-height: none; opacity: 1; overflow: visible; transition: none; }
      .sf-header.is-condensed .sf-nav { max-height: none; opacity: 1; padding-top: 4px; margin-top: 2px; }
      .sf-nav.open { display: flex; }
      .sf-nav-link { padding: 11px 8px; font-size: 1rem; justify-content: space-between; }
      .sf-nav-link::after { display: none; }   /* no underline in the drawer */
      .sf-dropdown { position: static; opacity: 1; visibility: visible; transform: none; box-shadow: none; border: none; padding: 0 0 6px 14px; background: transparent; min-width: 0; }
      /* Mega panel renders inline inside the drawer (no overlay/feature
         card) — same nested-list treatment the old dropdown had. */
      .sf-mega { position: static; opacity: 1; visibility: visible; transform: none; box-shadow: none; border: none; padding: 0 0 6px 14px; background: transparent; }
      .sf-mega-inner { display: block; }
      .sf-mega-cols { display: block; }
      .sf-mega-feature { display: none; }
      .sf-mega-link { padding: 9px 8px; }
      /* Keep the 8:3 ratio on mobile too (full banner, no crop). A
         1600×600 banner is short on a phone, so the overlay goes compact +
         bottom-anchored: heading + CTA only (subheading hidden) over a
         bottom gradient, clipped so nothing overflows. */
      .sf-hero-overlay { align-items: flex-end; background: none; }
      .sf-hero-overlay.has-text { background: linear-gradient(0deg, rgba(0,0,0,0.6), transparent 62%); }
      .sf-hero-text { padding: 0 16px 14px; }
      .sf-hero-text h1 { font-size: 1.2rem; margin-bottom: 8px; max-width: none; }
      .sf-hero-text p { display: none; }
      .sf-hero-cta { padding: 9px 18px; font-size: 0.85rem; }
      html { --tiles-cols: 2; --tiles-sub-cols: 3; --tiles-gap: 12px; }
      .sf-tiles-wrap, .sf-products-title { padding-left: 14px; padding-right: 14px; }
      .sf-cat-head-inner { padding: 24px 14px; } .sf-cat-head h1 { font-size: 1.5rem; }
      /* Footer: brand full-width on top, then Shop + Contact side-by-side. */
      .sf-footer { margin-top: 36px; }
      .sf-footer-cols { grid-template-columns: 1fr 1fr; gap: 28px 20px; padding: 34px 18px 28px; }
      .sf-footer-col:first-child { grid-column: 1 / -1; }
      .sf-footer-bar { justify-content: center; text-align: center; padding: 16px; }
    }
    @media (min-width: 720px) and (max-width: 1023px) {
      html[data-layout="storefront"] { --layout-cols: 3; }
      html { --tiles-cols: 3; --tiles-sub-cols: 4; }
      .sf-header-inner { gap: 12px 14px; }
    }
    /* Let the nav + search compress before the bar overflows on a store
       with many top-level categories. */
    .sf-nav { min-width: 0; }
    .sf-search { min-width: 0; }

    html { scroll-behavior: smooth; }
    body {
      font-family: var(--font-body);
      background: var(--bg); color: var(--text);
      min-width: 320px;
      -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
      overflow-x: hidden; line-height: 1.6;
    }
    [x-cloak] { display: none !important; }
    a { color: inherit; text-decoration: none; }

    body::before {
      content: ''; position: fixed; top: -50%; left: -30%;
      width: 100%; height: 100%;
      background: radial-gradient(ellipse, var(--accent-glow) 0%, transparent 60%);
      pointer-events: none; z-index: 0;
    }

    /* === Background texture (brand pattern) ===
       The tile is a STENCIL, not a picture: mask-image takes its shape and
       background-color fills it with the store's own --accent. One asset
       therefore serves every palette and both light/dark modes, and a
       merchant can't pick a texture that fights their colours because the
       texture has none of its own.

       body::before is already taken (the accent glow above), so this is
       ::after.

       z-index MUST be -1, not 0. A positioned z-index:0 element paints with
       the positioned descendants — i.e. ABOVE all non-positioned in-flow
       content. The simple layouts would survive that by accident (.main and
       .hero set position:relative; z-index:1), but the website layouts don't,
       and the texture rendered straight over .sf-hero's photo and the
       category tiles. At -1 it paints above body's own background and below
       every descendant, which is what we actually want.

       position:fixed — NOT background-attachment:fixed, which repaints on
       every scroll frame and janks badly on iOS. This is one composited
       layer that never repaints.

       Two independent guards, because the failure mode here is severe — an
       unmasked layer paints a SOLID accent wash over the whole storefront:
         1. display:none unless html[data-pattern] is set.
         2. the ink is var(--pattern-ink, transparent), NOT var(--accent)
            directly. --pattern-ink is set only alongside --pattern-tile and
            cleared with it, so if the tile ever goes missing while the
            attribute lingers, the layer is invisible rather than a wash.
       Guard 1 alone was not enough: previewTheme() clears the vars and then
       calls applyTheme(null), which used to early-return before dropping the
       attribute. */
    body::after {
      content: ''; position: fixed; inset: 0; z-index: -1;
      pointer-events: none; display: none;
      background-color: var(--pattern-ink, transparent);
      -webkit-mask: var(--pattern-tile) 0 0 / var(--pattern-scale, 150px) repeat;
              mask: var(--pattern-tile) 0 0 / var(--pattern-scale, 150px) repeat;
      opacity: var(--pattern-opacity, 0.07);
      /* Composites the texture with the canvas below. The layer's backdrop is
         body's own background (it sits at z-index:-1 in body's stacking
         context), and page content paints above, so this can only ever tint
         the canvas — never the cards or the text on top of them.
         'multiply' is the one that earns its keep: a merchant's tile usually
         has a solid white background, and on a light store white × backdrop =
         backdrop, so the white vanishes and only the motifs darken the page.
         Opacity on the same element is applied first and the result is then
         blended, so the two compose rather than cancel. */
      mix-blend-mode: var(--pattern-blend, normal);
    }
    html[data-pattern] body::after { display: block; }

    /* A merchant's OWN tile is a picture, not a stencil, so it must not go
       through the mask — a mask reads ALPHA, and real artwork (a photo, or
       chillies drawn on a white background) is uniformly opaque. Masking it
       yields a featureless accent rectangle: the texture silently vanishes
       and the merchant sees "nothing happened". Render the artwork itself,
       in its own colours, and skip the accent ink entirely.
       Transparent-background PNGs are what actually look right here; an
       opaque tile tints the whole canvas toward its own background colour. */
    html[data-pattern="custom"] body::after {
      background-color: transparent;
      background-image: var(--pattern-tile);
      background-repeat: repeat;
      background-size: var(--pattern-scale, 400px);
      -webkit-mask: none;
              mask: none;
    }
    /* Browsers without CSS masking would show that solid accent fill. Hide
       rather than wreck the store. (Chrome/Safari/FF all support the -webkit-
       form; this is belt-and-braces for anything older.) */
    @supports not ((-webkit-mask-image: none) or (mask-image: none)) {
      html[data-pattern] body::after { display: none; }
    }

    ::-webkit-scrollbar { width: 4px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: var(--surface3); border-radius: 2px; }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .no-scrollbar::-webkit-scrollbar { display: none; }
    .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }

    .tap { transition: transform 0.1s ease; }
    .tap:active { transform: scale(0.97); }

    @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
    /* Loading skeleton — sits on a calm NEUTRAL surface (independent of the
       tenant theme, which hasn't loaded yet) so there's no harsh black flash
       before a light-themed store paints. Mirrors a real storefront shape:
       a header bar, a hero band, then a product grid. */
    .sk-screen { position: fixed; inset: 0; z-index: 1; background: var(--sk-bg); overflow: hidden; }
    .sk-wrap { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
    .sk-topbar { height: 62px; background: var(--sk-surface); border-bottom: 1px solid var(--sk-border); display: flex; align-items: center; }
    .sk-topbar .sk-wrap { width: 100%; display: flex; align-items: center; gap: var(--space-4); }
    .sk-hero-band { background: var(--sk-surface); border-bottom: 1px solid var(--sk-border); padding: 18px 0 26px; }
    /* The grid mirrors whichever layout <html> already carries (the same
       --layout-cols / --layout-gap / --card-aspect the real grid reads), so
       a gallery store no longer shape-shifts from a 4-up skeleton to 2-up
       portrait cards on first paint. With no data-layout yet, the neutral
       4 / 3 / 2 shape below is the pre-existing skeleton. */
    .sk-grid { display: grid; grid-template-columns: repeat(var(--layout-cols), 1fr); gap: var(--layout-gap); padding-top: 26px; }
    @media (max-width: 1023px) { html:not([data-layout]), html[data-layout="list"] { --layout-cols: 3; } }
    @media (max-width: 719px)  { html:not([data-layout]), html[data-layout="list"] { --layout-cols: 2; } .sk-screen .sk-wrap { padding: 0 14px; } }
    .sk-cell { display: flex; flex-direction: column; gap: 10px; }
    /* The thumb placeholder follows the layout's card aspect. !important is
       needed only because store.html still carries an inline
       aspect-ratio: 1/1 on that element; drop the inline style there and
       this can lose the flag. */
    .sk-cell > .sk:first-child { aspect-ratio: var(--card-aspect) !important; }
    .sk {
      background: linear-gradient(90deg, var(--sk-bone) 25%, var(--sk-bone-hi) 50%, var(--sk-bone) 75%);
      background-size: 200% 100%; animation: shimmer 1.4s ease-in-out infinite; border-radius: 8px;
    }
    .sk-circle { border-radius: 50%; }

    .error-state { max-width: 480px; margin: 80px auto; padding: 32px 24px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); text-align: center; }
    .error-state h3 { font-family: var(--font-display); font-size: 1.4rem; margin-bottom: 8px; color: var(--accent); }
    .error-state p { color: var(--text-muted); margin-bottom: 18px; }
    .error-retry {
      padding: 10px 22px; border-radius: var(--radius-xs);
      background: var(--accent); color: var(--theme-accent-fg, #111); border: 0;
      font: 700 0.88rem var(--font-body); cursor: pointer;
      transition: filter 0.15s ease, transform 0.15s ease;
      min-height: 44px;
    }
    .error-retry:hover:not(:disabled) { filter: brightness(1.08); }
    .error-retry:active:not(:disabled) { transform: scale(0.97); }
    .error-retry:disabled { opacity: 0.6; cursor: progress; }

    .main { max-width: 480px; margin: 0 auto; padding: 4px 16px 120px; position: relative; z-index: 1; }

    /* === Hero === */
    .hero { position: relative; padding: 32px 16px 20px; text-align: center; max-width: 480px; margin: 0 auto; z-index: 1; }
    .hero-content { position: relative; z-index: 2; }
    .logo-wrap { width: 100px; height: 100px; margin: 0 auto 16px; position: relative; display: flex; align-items: center; justify-content: center; }
    .logo-ring {
      position: absolute; inset: 0; border-radius: 50%;
      background: conic-gradient(from 0deg, transparent 0deg, var(--accent) 60deg, transparent 120deg, transparent 360deg);
      animation: logoSpin 4s linear infinite;
      mask: radial-gradient(circle, transparent 46px, #000 47px);
      -webkit-mask: radial-gradient(circle, transparent 46px, #000 47px);
    }
    @keyframes logoSpin { to { transform: rotate(360deg); } }
    .logo-inner {
      position: relative; width: 92px; height: 92px; border-radius: 50%;
      background: var(--surface); border: 1px solid var(--border);
      display: flex; align-items: center; justify-content: center; overflow: hidden;
      font-family: var(--font-display); font-weight: 700; font-size: 1.8rem; color: var(--accent);
    }
    .logo-inner img { width: 100%; height: 100%; object-fit: cover; }
    /* "Square logo" store toggle (migration 076, body.logo-square): the simple-
       layout hero (.logo-inner) and the sticky brand mark (.nav-brand-mark)
       default to a circle that cover-crops wide / wordmark logos. Square mode
       drops the spinning ring, switches to a rounded-square frame, and shows
       the whole logo (object-fit: contain) with a little breathing room. */
    body.logo-square .logo-ring { display: none; }
    body.logo-square .logo-inner,
    body.logo-square .nav-brand-mark { border-radius: 14px; border: none; background: transparent; box-shadow: none; }
    body.logo-square .logo-inner img,
    body.logo-square .nav-brand-mark img { object-fit: contain; box-sizing: border-box; padding: 5px; }
    .hero h1 { font-family: var(--font-display); font-weight: 700; font-size: 2rem; color: var(--accent); line-height: 1.2; margin-bottom: 8px; letter-spacing: -0.01em; }
    .hero .divider { width: 48px; height: 2px; margin: 12px auto; background: linear-gradient(90deg, transparent, var(--accent), transparent); }
    .hero .tagline { font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 2.5px; font-weight: 500; margin-bottom: 16px; }
    /* Lifted verbatim out of an inline style on the span so the declarations
       live with every other hero rule. Nothing overrides it today; keep it a
       class anyway — an inline style outranks any stylesheet rule, so this is
       the only form a theme or a media query could ever reach. */
    .hero-addr { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }
    .hero-meta { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 18px; flex-wrap: wrap; }
    .status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: var(--radius-full); font-size: 0.72rem; font-weight: 600; border: 1px solid; }
    .status-badge.open    { color: var(--green); background: var(--green-dim); border-color: var(--green-dim); }
    .status-badge.busy    { color: var(--warn); background: var(--warn-dim); border-color: var(--warn-dim); }
    .status-badge.closed  { color: var(--red); background: var(--red-dim); border-color: var(--red-dim); }

    /* === Neon open/closed sign (simple layouts) ===
       Replaces the old 6px dot. A stroked SVG ring is the tube, the filled
       core is the gas; the glow is two stacked drop-shadows rather than a
       box-shadow so it follows the STROKE, not a square box.

       Everything is currentColor, so one glyph serves every state: the
       badge's own .open / .busy / .closed rules above already set --green /
       --warn / --red, and applyTheme() deliberately never overrides those
       three. A store with a red brand therefore still gets a green "open"
       sign — the state can never lie about itself.

       THE RULE, and it is easy to get wrong: the RESTING appearance of each
       state lives in these base rules. The animations only add the flicker
       and the breath on top. Reaching "unlit" via animation-fill-mode on
       neonDie would look right until someone sets prefers-reduced-motion,
       which cancels the animation and would light up a closed shop. Strip
       every animation below and the sign must still read correctly. */
    .neon { display: inline-flex; align-items: center; justify-content: center; flex: none; }
    .neon svg { width: 100%; height: 100%; display: block; overflow: visible; }
    .neon .neon-tube { fill: none; stroke: currentColor; stroke-width: 2.4; }
    .neon .neon-core { fill: currentColor; stroke: none; }
    .status-badge .neon { width: 11px; height: 11px; }

    /* Lit: full opacity + halo. */
    .sg-open .neon, .sg-busy .neon, .sg-closing .neon {
      opacity: 1;
      filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor);
    }
    /* Unlit: a cold, dim tube with NO halo — the resting closed state. */
    .sg-closed .neon {
      opacity: 0.45;
      filter: saturate(0.55);
    }

    .sg-open .neon    { animation: neonOn 0.9s steps(1, end) 1 both, neonBreathe 3.4s ease-in-out 0.9s infinite; }
    .sg-closing .neon { animation: neonStutter 2.2s steps(1, end) infinite; }
    .sg-busy .neon    { animation: neonBreathe 1.9s ease-in-out infinite; }
    .sg-closed .neon  { animation: neonDie 0.7s steps(1, end) 1; }

    /* Startup flicker — a real tube strikes two or three times before it
       holds. steps(1, end) keeps each frame hard-edged; a smooth tween here
       reads as a fade, not a strike. */
    @keyframes neonOn {
      0%, 14%  { opacity: 0.12; filter: none; }
      18%, 26% { opacity: 1;    filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
      30%, 38% { opacity: 0.18; filter: none; }
      42%, 52% { opacity: 1;    filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
      56%, 62% { opacity: 0.3;  filter: none; }
      66%, 100%{ opacity: 1;    filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
    }
    /* The idle state once lit — the halo swells and settles. Deliberately
       slow: at 3.4s it reads as a live sign, not as a blinking widget. */
    @keyframes neonBreathe {
      0%, 100% { filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 6px currentColor); }
      50%      { filter: drop-shadow(0 0 3px currentColor) drop-shadow(0 0 11px currentColor); }
    }
    /* Closing soon — a tube on its way out. Irregular on purpose; an evenly
       spaced blink reads as a notification badge demanding a tap. */
    @keyframes neonStutter {
      0%, 58%   { opacity: 1;   filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
      60%, 63%  { opacity: 0.3; filter: none; }
      65%, 77%  { opacity: 1;   filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
      79%, 81%  { opacity: 0.3; filter: none; }
      83%, 100% { opacity: 1;   filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
    }
    /* Two dim gasps, then it rests at whatever .sg-closed says above. */
    @keyframes neonDie {
      0%, 18%   { opacity: 1;    filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 7px currentColor); }
      22%, 40%  { opacity: 0.35; filter: none; }
      44%, 54%  { opacity: 0.8;  filter: drop-shadow(0 0 2px currentColor); }
      58%, 100% { opacity: 0.45; filter: saturate(0.55); }
    }

    /* === Hanging sign ===
       The door sign of a real shop: a plaque on two chains under a bracket.
       It drops in on load, settles with a damped swing, then sways very
       slowly while the shop is open. A closed shop's sign hangs dead still —
       stillness is the signal, so it needs no words to read as "shut".

       There is deliberately NO top banner anywhere. The earlier build put a
       "We're closed" stripe above the header while the pill below said
       "Closed · Opens 9am" — the same sentence twice on one screen. One
       announcement, in one place. */
    /* === Merchant's own sign artwork (migration 162) ===
       Deliberately unstyled beyond a frame: this is THEIR picture, so we add
       no border, background or shadow that would fight their design. The frame
       is a size cap, not a look — object-fit:contain means any aspect ratio a
       merchant uploads is shown whole rather than cropped, so nobody loses the
       edge of their logo to a hard-coded 3:1. */
    .hero-artsign { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 4px auto 18px; }
    .hero-artsign__img {
      display: block;
      max-width: min(78vw, 360px);
      max-height: 132px;
      width: auto; height: auto;
      object-fit: contain;
    }
    /* The live caption. Small and secondary — the artwork is the statement,
       this is the part that stays true when the hours change. */
    .hero-artsign__cap {
      font: 500 0.74rem var(--font-body);
      letter-spacing: 0.03em; color: var(--text-muted);
    }
    /* A shut shop's sign sits back a little, the way an unlit one does. Applied
       to the picture only, never the caption, which must stay legible. */
    .hero-artsign.sg-closed .hero-artsign__img { opacity: 0.72; }
    @media (max-width: 480px) { .hero-artsign__img { max-height: 104px; } }
    .hangsign { display: inline-flex; flex-direction: column; align-items: center; margin: 2px auto 18px; }
    .hangsign__bar {
      width: 62px; height: 3px; border-radius: 2px;
      background: currentColor; opacity: 0.5;
    }
    /* The swinging body. transform-origin at the bracket is the whole
       illusion — rotate about the top edge and the plaque pivots from the
       hook exactly as a hung object does. */
    .hangsign__swing { position: relative; transform-origin: 50% 0; padding-top: 14px; }
    .hangsign__chain {
      position: absolute; top: 0; width: 1.5px; height: 15px;
      background: currentColor; opacity: 0.55;
    }
    .hangsign__chain--l { left: 22%; transform: rotate(7deg); }
    .hangsign__chain--r { right: 22%; transform: rotate(-7deg); }
    .hangsign__plate {
      display: flex; flex-direction: column; align-items: center; gap: 5px;
      padding: 12px 24px;
      border: 1.5px solid currentColor; border-radius: var(--radius, 14px);
      background: var(--surface);
      /* store.status is free text up to 50 chars and lands in __sub via
         statusDetail(). Unbounded, "Closed for Diwali until Monday morning"
         runs off a 320px phone. */
      max-width: min(76vw, 260px); text-align: center;
    }
    .hangsign__word {
      font: 700 0.98rem var(--font-display);
      letter-spacing: 0.24em; text-transform: uppercase; line-height: 1;
    }
    .hangsign__sub { font: 500 0.72rem var(--font-body); letter-spacing: 0.04em; opacity: 0.82; }
    .hangsign__word, .hangsign__sub { overflow-wrap: anywhere; }
    .hangsign.open   { color: var(--green); }
    .hangsign.busy   { color: var(--warn); }
    .hangsign.closed { color: var(--red); }

    /* Lit vs unlit, in the BASE rules — see the neon note above: strip every
       animation and the sign must still say the right thing. */
    .sg-open .hangsign__plate, .sg-closing .hangsign__plate, .sg-busy .hangsign__plate {
      box-shadow: 0 0 16px -3px currentColor, inset 0 0 14px -8px currentColor;
    }
    .sg-closed .hangsign { opacity: 0.55; }
    .sg-closed .hangsign__plate { box-shadow: none; }

    /* Open: drop in, settle, then breathe on the hook forever.
       Closed: it drops and then stops dead. */
    .sg-open .hangsign__swing,
    .sg-closing .hangsign__swing,
    .sg-busy .hangsign__swing { animation: signDrop 1.2s cubic-bezier(0.22,1,0.36,1) 1, signSway 4s ease-in-out 1.2s infinite alternate; }
    .sg-closed .hangsign__swing { animation: signDrop 1.2s cubic-bezier(0.22,1,0.36,1) 1; }

    /* A hung sign does not stop at once — it overshoots and decays. Four
       reversals with shrinking amplitude is what sells the weight. */
    @keyframes signDrop {
      0%   { transform: translateY(-18px) rotate(-11deg); opacity: 0; }
      35%  { transform: translateY(0) rotate(7deg); opacity: 1; }
      55%  { transform: rotate(-4.5deg); }
      72%  { transform: rotate(2.6deg); }
      86%  { transform: rotate(-1.3deg); }
      100% { transform: rotate(0deg); }
    }
    @keyframes signSway {
      from { transform: rotate(-2.2deg); }
      to   { transform: rotate(2.2deg); }
    }
    .hero-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
    .hero-actions a, .hero-actions button {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 9px 18px; border-radius: var(--radius-full);
      border: 1px solid var(--border-light); background: var(--surface);
      color: var(--text); font: 600 0.82rem var(--font-body);
      cursor: pointer; transition: all 0.2s ease;
    }
    .hero-actions a:hover, .hero-actions button:hover {
      border-color: var(--accent); color: var(--accent); background: var(--accent-dim);
    }
    .hero-actions svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
    /* Install-app pill — pre-tinted with the store's accent so it draws
       the eye without screaming. Same shape / padding as Call and
       Directions so the row stays visually consistent. */
    .hero-actions .install-pill {
      background: var(--accent-dim);
      color: var(--accent);
      border-color: var(--accent);
      font-weight: 700;
    }
    .hero-actions .install-pill:hover { filter: brightness(1.1); background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }

    /* === Cover-photo band — PHONES ONLY, simple layouts ===
       When the merchant has set a Cover photo (store.cover_photo, migration
       112 — the same wide photo the marketplace cards use), a full-bleed 16:9
       band of it sits at the top of the page with the logo straddling its
       bottom edge. The band is the ONLY thing this adds: the store name,
       tagline, status badge and action pills below the logo keep exactly the
       styling a store with no cover photo gets. Nothing here recolours them.

       Deliberately phone-only. The whole block lives inside
       @media (max-width: 719px) — the exact complement of the min-width:720px
       tablet-up query — so the ≥720 / ≥1024 blocks (including the desktop
       3-column hero grid) never see it and need no defensive overrides. The
       element still renders into the DOM at every width; `display:none` below
       is what gates it, NOT a JS viewport flag.

       No scroll parallax, deliberately. background-attachment:fixed is the
       obvious "premium" move here and it repaints every scroll frame and janks
       on iOS — the same reason body::after uses position:fixed instead (see
       the pattern-texture note above). The composited scale drift gives the
       depth for none of the cost.

       Stacking: .hero is already position:relative; z-index:1, so .hero-cover
       at z-index:0 lives inside that stacking context — above the body::after
       pattern texture (z-index:-1) and below .hero-content (z-index:2). */
    .hero-cover { display: none; }

    @media (max-width: 719px) {
      /* --cover-h in ONE place so the band's height and the padding that
         makes room for it can never drift apart.
           56.25vw — IS 16:9 measured against the full-bleed width.
           260px   — stops the band ballooning on a foldable or a small
                     tablet sitting just under the 719px edge.
           46vh    — the landscape guard, and the only reason this is a
                     three-way min(). A phone turned sideways is ~375px
                     tall, where 56.25vw would be 375px and the cap 260px:
                     either way the band swallows 69% of the screen and
                     pushes the store's name below the fold. This binds
                     ONLY in landscape — every portrait phone measured
                     (320/390/430/700/719 wide) is decided by the other
                     two terms and is unchanged by it.
         vh, not dvh/svh: vh is the stable large-viewport height, so this
         cannot reflow while the URL bar collapses. */
      body.has-cover-hero .hero {
        --cover-h: min(56.25vw, 260px, 46vh);
        padding: calc(var(--cover-h) - 50px) 16px 20px;
      }
      /* 50px above = half of .logo-wrap's 100px, which lands the logo's
         centre exactly on the band's bottom edge. */

      /* margin-inline: calc(50% - 50vw) bleeds past .hero's max-width:480px
         to the viewport edges without disturbing the hero's own padding.
         overflow:hidden belongs HERE, not on .hero: this element owns the
         radius, and clipping at .hero would cut the bleed off on any phone
         wider than 480px. */
      body.has-cover-hero .hero-cover {
        display: block; position: absolute;
        top: 0; left: 0; right: 0; height: var(--cover-h);
        margin-inline: calc(50% - 50vw);
        overflow: hidden;
        border-radius: 0 0 20px 20px;
        z-index: 0;
      }
      /* `cover`, not `contain`: the band is 16:9 and zadmin asks for
         1600×900, so a merchant who follows the guidance is cropped by
         exactly nothing, and everyone else centre-crops gracefully. */
      body.has-cover-hero .hero-cover-img {
        position: absolute; inset: 0;
        background-position: center; background-size: cover; background-repeat: no-repeat;
        animation: coverDrift 20s ease-in-out infinite alternate;
      }
      /* Two jobs. The var(--bg) stop dissolves the band's foot into the page
         instead of ending it on a hard line — that, plus the logo's punch-out
         ring, is what separates a considered header from a photo with a
         border. The light top tint keeps the sale bar legible over a bright
         photo. */
      body.has-cover-hero .hero-cover::after {
        content: ''; position: absolute; inset: 0; pointer-events: none;
        background: linear-gradient(180deg,
          rgba(0,0,0,0.18) 0%, rgba(0,0,0,0) 24%,
          transparent 76%, var(--bg) 100%);
      }

      /* The logo straddles the band's edge, so it needs to punch OUT of the
         photo: a ring in the page background is what does that on every
         profile header worth copying, and it needs no scrim and no contrast
         maths because it isn't translucent. The spinning accent ring is
         retired here — a conic gradient chasing itself around a photo
         boundary reads as noise. */
      body.has-cover-hero .logo-ring { display: none; }
      body.has-cover-hero .logo-inner,
      body.has-cover-hero.logo-square .logo-inner {
        border: 4px solid var(--bg);
        background: var(--surface);
        box-shadow: 0 6px 20px rgba(0,0,0,0.28);
      }
      /* The square-logo rule above is NOT redundant: body.logo-square sets
         border:none; background:transparent, which would erase the punch-out.
         It keeps its own 14px radius from that rule. */
    }
    @keyframes coverDrift { to { transform: scale(1.04); } }
    /* Same guard the banner strip uses — no ambient motion for anyone who
       asked the OS to stop it. */
    @media (prefers-reduced-motion: reduce) {
      body.has-cover-hero .hero-cover-img { animation: none; }
      /* The neon sign loses its flicker and its breath, NOT its meaning:
         the lit / unlit look is carried by the .sg-* base rules, so a shut
         shop still reads shut here. (The dot this replaced ran `pulse`
         forever with no guard at all — it was the one animation in this
         file that ignored the preference.) */
      .neon { animation: none !important; }
      /* The sign stops swinging and stops dropping in. It must still be
         VISIBLE — signDrop starts at opacity 0, so if this ever gains a
         fill-mode the sign would vanish for reduced-motion users. */
      .hangsign__swing { animation: none !important; }
    }

    /* === Banners (simple layouts) ===
       The same store.hero slides the website layouts render as the
       full-bleed .sf-hero crossfade, but drawn as a swipeable card
       strip. Sits between .hero and .sticky-nav: the pills are a
       FILTER over the product list, so store-level merchandising
       belongs above them (Blinkit / Zepto / BigBasket), not inside
       the filtered result.

       Swipe is native scroll-snap — the browser's own momentum
       scrolling, which tracks the finger exactly. Every other strip
       here does the same (.sf-rail-track, .sf-pdp-track, .promos in
       group.html); no JS carousel library is involved. */
    /* Per-layout content measure. The banner IMAGE goes edge to edge on
       desktop, but its overlay copy stays inside this column so a heading
       never floats against the bare screen edge — the full-bleed-image /
       contained-text pattern. Each value MIRRORS that layout's own .main
       max-width (list 1200 / gallery 1280 / grid 1320 / magazine 720 in
       the ≥1024 desktop block; bazaar 1320 at line ~460), so the banner's
       words line up with the product cards below. Change a .main width,
       change the twin here. list + not-yet-set data-layout fall through
       to the 1200px fallback baked into the .bn-in calc. */
    html[data-layout="gallery"]  { --bn-max: 1280px; }
    html[data-layout="grid"]     { --bn-max: 1320px; }
    html[data-layout="magazine"] { --bn-max: 720px; }
    html[data-layout="bazaar"]   { --bn-max: 1320px; }
    @media (min-width: 720px) and (max-width: 1023px) {
      /* Tablet: the per-layout .main widths above only engage at 1024px —
         until then every simple layout sits in a 560px column, so banner
         copy measured against 1200px would run past the products it sits
         over. Bazaar is the one exception: its 1320px .main is a base rule
         that applies here too. */
      html:not([data-layout="bazaar"]) .bn-strip { --bn-max: 560px; }
    }

    .bn-strip { padding: 4px 0 2px; }

    .bn-rail {
      display: flex; gap: 10px; overflow-x: auto;
      scroll-snap-type: x mandatory;
      scroll-padding-left: 16px;
      padding: 2px 16px 10px;
      scrollbar-width: none; -ms-overflow-style: none;
      -webkit-overflow-scrolling: touch;
      /* Stop a horizontal fling from chaining into the browser's
         back-navigation gesture on iOS / Chrome Android. */
      overscroll-behavior-x: contain;
    }
    .bn-rail::-webkit-scrollbar { display: none; }

    .bn {
      flex: 0 0 86%; max-width: 460px;
      scroll-snap-align: start; scroll-snap-stop: always;
      position: relative;
      /* App-tall on phones. The 8:3 box the website hero uses collapses
         to a ~125px sliver at 390px wide; 16:9 gives it ~188px. Desktop
         widens back to 8:3 below, where the wide box belongs.
         `cover` (not the .sf-hero `contain`) because a letterboxed card
         with grey bars inside a rounded corner looks broken — and the
         crop is safe here: heading/subheading/CTA are live DOM text,
         not pixels baked into the upload, so they reflow instead of
         being sliced. Merchants who DO bake text into their art can
         set image_mobile. */
      aspect-ratio: 16 / 9;
      border-radius: var(--radius-sm);
      overflow: hidden;
      background-color: var(--surface2);
      background-size: cover; background-position: center; background-repeat: no-repeat;
      box-shadow: 0 6px 18px rgba(0,0,0,0.28);
      border: 0; padding: 0; margin: 0;
      text-align: left; color: inherit; font: inherit;
      cursor: default; -webkit-tap-highlight-color: transparent;
      transition: transform 0.16s var(--ease), box-shadow 0.16s var(--ease);
    }
    /* ── Desktop / tablet: ONE full-bleed banner, not a row of them ───
       A 440px card on a 1440px screen showed 3-4 slides at once, which
       reads as an ad row: the slides compete, the headings shrink to
       noise, and the dots page through something already fully visible.
       From 720px up the strip is a one-up carousel running edge to edge —
       square corners, no side gutters, the artwork touching both screen
       edges the way a campaign hero should.

       720px is deliberate, not new: it's the same line bnWide uses to
       swap image_mobile out for the wide image, and the same one seo.ts
       preloads against. Art direction and layout flip together.

       gap:0 + zero side padding makes one slide exactly fill the viewport
       with no sliver of the next bleeding in. _bnStep() measures the real
       pitch off the DOM, so dots and autoplay need no JS change. */
    @media (min-width: 720px) {
      .bn-frame { position: relative; }
      .bn-rail { gap: 0; padding: 0 0 12px; scroll-padding-left: 0; }
      .bn {
        flex: 0 0 100%; max-width: none;
        /* 8:3 and NOTHING that overrides it. A max-height here does not
           shorten the box, it makes it WIDER than 8:3, and
           background-size:cover then eats the difference — silently
           trimming the top and bottom off artwork uploaded at exactly the
           ratio zadmin asks for. Cropping a correct upload is a worse
           failure than a tall banner, so the height simply follows the
           viewport: 1920 => 720px tall, 2560 => 960px. */
        aspect-ratio: 8 / 3;
        border-radius: 0;
        /* Edge to edge means no side shadow to cast; keep only the drop
           that separates the banner from the page below it. */
        box-shadow: 0 8px 20px -8px rgba(0,0,0,0.45);
      }
    }
    /* A lone banner earns no chrome. The 86% peek exists to say "there's
       another one" — with a single banner that's a false promise, and the
       gap beside it just reads as a layout bug. Fill the width instead.
       (Set from JS, not :only-child: Alpine leaves the x-for <template> in
       the DOM as a sibling, so the slide is never actually an only child.) */
    .bn-rail.is-single { scroll-snap-type: none; }
    .bn-rail.is-single .bn { flex: 0 0 100%; max-width: none; }

    .bn.is-link { cursor: pointer; }
    .bn.is-link:active { transform: scale(0.985); }
    .bn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

    /* Scrim ONLY under text — self-contained artwork renders at full
       brightness. Left-anchored so the photo's subject stays bright on
       the right. Same rule as .promo.has-text in group.html. */
    .bn.has-text::after {
      content: ''; position: absolute; inset: 0;
      background: linear-gradient(90deg, rgba(0,0,0,0.60) 0%, rgba(0,0,0,0.20) 56%, rgba(0,0,0,0) 88%);
    }
    .bn-in {
      position: absolute; inset: 0; z-index: 1;
      padding: 14px 16px;
      display: flex; flex-direction: column; justify-content: center;
      color: #fff;
    }
    .bn-h {
      font: 800 1.15rem/1.08 var(--font-body); letter-spacing: -0.02em;
      max-width: 64%; text-shadow: 0 2px 12px rgba(0,0,0,0.45);
    }
    .bn-s {
      margin-top: 5px; font: 600 0.74rem var(--font-body); opacity: 0.94;
      max-width: 64%; text-shadow: 0 1px 8px rgba(0,0,0,0.45);
    }
    .bn-cta {
      margin-top: 9px; align-self: flex-start;
      background: #fff; color: #111;
      font: 800 0.66rem var(--font-body);
      padding: 6px 12px; border-radius: var(--radius-full);
      text-decoration: none;
    }
    /* Type was scaled for a 440px card; on a 1200px banner the same
       sizes read undersized and the copy floats in the corner. Scale
       with the box and pull the text column in to ~half the width so
       it never runs into the photo's subject. The scrim widens with
       it — enough contrast under the words, no grey wash over the art. */
    @media (min-width: 720px) {
      .bn.has-text::after {
        background: linear-gradient(90deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.18) 52%, rgba(0,0,0,0) 78%);
      }
      /* Full-bleed image, CONTAINED text. The inset resolves to whatever
         gutter centres a --bn-max content column in the viewport, so the
         heading starts on the same line as the product cards below rather
         than floating against the bare screen edge. max() keeps a sane
         44px minimum once the viewport is narrower than that column. */
      .bn-in {
        padding-block: 36px;
        padding-inline: max(44px, calc((100% - (var(--bn-max, 1200px) - 32px)) / 2));
      }
      /* NB: these percentages resolve against .bn-in's CONTENT box (the
         box minus the padding above) — i.e. the content column, not the
         full-bleed banner. Held loose here for the ~528px tablet column
         and tightened at ≥1024 below, where the column is 1100px+ and
         copy running half its width would collide with the artwork. */
      .bn-h { font-size: clamp(1.6rem, 2.4vw, 2.3rem); max-width: 78%; }
      .bn-s { font-size: 1rem; margin-top: 8px; max-width: 76%; }
      .bn-cta { font-size: 0.82rem; padding: 10px 20px; margin-top: 16px; }
    }
    @media (min-width: 1024px) {
      .bn-h { max-width: 48%; }
      .bn-s { max-width: 46%; }
    }

    /* ── Desktop carousel arrows ──────────────────────────────────────
       Same visual language as .modal-gallery-arrow on the PDP gallery.
       Mouse users get no swipe gesture and the dots are a destination,
       not a step control — these are the step control. Hidden entirely
       on touch (the finger is the arrow) and below 720px, where the
       peek carousel already says "swipe me". */
    .bn-nav {
      display: none;
      position: absolute; top: 50%; transform: translateY(-50%);
      width: 40px; height: 40px; border-radius: var(--radius-full);
      place-items: center;
      background: rgba(0,0,0,0.55); color: #fff;
      border: 0; padding: 0; cursor: pointer;
      backdrop-filter: blur(8px);
      opacity: 0; transition: opacity 0.18s, background 0.18s;
      z-index: 2;
    }
    .bn-nav svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.2; }
    .bn-nav--prev { left: 14px; }
    .bn-nav--next { right: 14px; }
    /* A little more air once the banner runs to the screen edges. */
    @media (min-width: 1024px) {
      .bn-nav--prev { left: 26px; }
      .bn-nav--next { right: 26px; }
    }
    @media (min-width: 720px) and (hover: hover) and (pointer: fine) {
      .bn-nav { display: grid; }
      .bn-frame:hover .bn-nav { opacity: 0.92; }
      /* Clear the arrow lane. Both arrows are vertically centred and the
         copy is too, so at the 44px floor above the prev arrow would sit
         on top of the heading. Applied only in this block — where the
         arrows actually exist — so a touch tablet keeps the tighter
         inset. Above the floor the content-column gutter already clears
         them by a mile. */
      .bn-in { padding-left: max(78px, calc((100% - (var(--bn-max, 1200px) - 32px)) / 2)); }
      .bn-nav:hover { opacity: 1; background: rgba(0,0,0,0.75); }
      /* Keyboard users never trigger the hover reveal. */
      .bn-nav:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 3px; }
      /* :active scale is a thumb gesture; a mouse expects a lift.
         Matches .menu-card:hover in the desktop block below.
         :not(:active) because .bn.is-link:active is declared EARLIER in this
         file at the same specificity — without the guard this rule wins the
         source-order tie and the press feedback silently disappears on
         desktop (a mouse-down is always also a hover). */
      .bn.is-link:hover:not(:active) { transform: translateY(-2px); box-shadow: 0 12px 34px rgba(0,0,0,0.30); }
    }

    .bn-dots { display: flex; justify-content: center; gap: 6px; padding: 0 0 6px; }
    .bn-dot {
      width: 6px; height: 6px; border-radius: var(--radius-full);
      background: color-mix(in srgb, var(--text) 26%, transparent);
      border: 0; padding: 0; cursor: pointer;
      transition: width 0.22s var(--ease), background 0.22s;
    }
    .bn-dot.active { width: 18px; background: var(--accent); }
    .bn-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

    /* An auto-advancing carousel is exactly the case this query exists
       for. _bnStart() also checks it in JS so autoplay never arms. */
    @media (prefers-reduced-motion: reduce) {
      .bn-rail { scroll-behavior: auto; }
      .bn, .bn-dot, .bn-nav { transition: none; }
    }

    /* === Sticky nav === */
    .sticky-nav {
      position: sticky; top: var(--mkt-bar-h, 0px); z-index: 30;
      /* Translucent version of the theme bg so the nav blurs over content
         in both dark and light palettes. Falls back to a solid bg in
         browsers without color-mix() support (Safari < 16.2). */
      background: var(--bg);
      background: color-mix(in srgb, var(--bg) 88%, transparent);
      backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
      border-bottom: 1px solid var(--border);
    }
    .nav-inner { max-width: 480px; margin: 0 auto; padding: 12px 16px; display: flex; flex-direction: column; gap: 10px; }
    /* Brand pair (logo + name) that slides into the sticky nav on
       desktop ONCE the hero scrolls out of view. Mirrors the pattern
       on Amazon / Flipkart / Myntra / Swiggy desktop: the customer
       loses their reference to "which store am I on?" the moment the
       hero leaves the viewport, so a compact brand identifier appears
       on the left side of the sticky bar. Hidden on mobile (the hero
       compacts more aggressively and the cart-strip already carries
       store context). Hidden initially even on desktop — only renders
       when .sticky-nav has .is-compact class (added by the IO that
       watches the hero element).
       Wrapper for the existing controls so we can lay them out as a
       grid on desktop: brand col 1, controls col 2. */
    .nav-controls { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
    .nav-brand { display: none; }
    .search-wrap { position: relative; }
    .search-wrap svg.search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; stroke: var(--text-muted); fill: none; stroke-width: 2; pointer-events: none; }
    .search-wrap input {
      width: 100%; min-height: 44px; padding: 0 40px 0 38px;
      border-radius: var(--radius-full); border: 1px solid var(--border-light);
      background: var(--surface); color: var(--text); font: 500 0.9rem var(--font-body);
      outline: none; transition: all 0.2s ease;
    }
    .search-wrap input::placeholder { color: var(--text-muted); }
    .search-wrap input:focus { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-dim); }
    /* Clear (×) button — only appears while typing. Single-tap reset
       vs deleting char-by-char; universal pattern (Spotify / Apple
       Music / Tata 1mg / Swiggy / Zomato all have it). */
    .search-clear {
      position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
      width: 28px; height: 28px; border-radius: 50%; border: 0;
      background: var(--surface3); color: var(--text); cursor: pointer;
      display: grid; place-items: center; transition: background 0.15s ease;
    }
    .search-clear:hover { background: var(--accent-dim); color: var(--accent); }
    .search-clear svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2.4; stroke-linecap: round; }

    /* Highlight the matched substring from a live search. Subtle
       accent-tinted background, no border or weight change so it
       doesn't disrupt scan flow. */
    mark.search-hit {
      background: var(--accent-dim); color: var(--accent);
      padding: 0 2px; border-radius: 3px;
    }
    /* min-width: 0 is REQUIRED on a flex item that wants overflow:auto
       to actually scroll. By default a flex child has min-width:auto,
       so it refuses to shrink below its content's intrinsic size — the
       strip would grow wider than nav-inner (560px on desktop), then
       body's overflow-x:hidden clips the off-screen tabs without any
       scroll affordance. min-width:0 lets the strip shrink to its
       container width so overflow:auto kicks in correctly. */
    .cat-tabs { display: flex; gap: 6px; overflow-x: auto; padding: 2px 0; min-width: 0; }
    .cat-tabs::-webkit-scrollbar { display: none; }

    /* Drag-to-scroll affordance on desktop. JS adds .is-draggable when
       the strip actually overflows (no point showing a grab cursor on
       a strip that fits the viewport). Touch devices don't render
       cursors so this is desktop-only by nature. */
    .cat-tabs.is-draggable,
    .diet-pills.is-draggable,
    .brand-pills.is-draggable,
    .offers-carousel.is-draggable { cursor: grab; }
    .cat-tabs.is-dragging,
    .diet-pills.is-dragging,
    .brand-pills.is-dragging,
    .offers-carousel.is-dragging { cursor: grabbing; user-select: none; }

    /* === Order-type switcher ===
       Compact segmented control at the very top of the sticky nav.
       Each pill carries an icon + label; the active pill fills in the
       brand accent so the customer always sees their current mode at
       a glance. The flex layout splits available width evenly so the
       row stays balanced regardless of mode count (2 or 3). */
    .mode-switch {
      display: flex; gap: 6px; padding: 2px 0;
      background: var(--surface); border: 1px solid var(--border-light);
      border-radius: var(--radius-full);
      padding: 4px; margin-bottom: 4px;
    }
    .mode-switch-pill {
      flex: 1 1 0; min-width: 0;
      display: inline-flex; align-items: center; justify-content: center; gap: 6px;
      padding: 7px 10px; border-radius: var(--radius-full);
      background: transparent; border: 0; color: var(--text-secondary);
      font: 600 12.5px Inter, sans-serif; cursor: pointer;
      transition: background 0.18s ease, color 0.18s ease;
      min-height: 34px; white-space: nowrap;
    }
    .mode-switch-pill:hover { color: var(--text); }
    .mode-switch-pill.active {
      background: var(--accent); color: var(--theme-accent-fg, #111);
      box-shadow: 0 2px 8px var(--accent-shadow);
    }
    .mode-switch-icon { display: inline-flex; align-items: center; }
    .mode-switch-icon svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
    .mode-switch-pill.active .mode-switch-icon svg { stroke-width: 2.4; }
    /* Price-context caption under the mode pills when prices vary by order
       type (dual pricing) — keeps browsing prices unambiguous. */
    .mode-price-note {
      display: flex; align-items: center; gap: 5px;
      margin: 1px 2px 6px; font-size: 12px; color: var(--text-muted);
    }
    .mode-price-note svg { width: 13px; height: 13px; flex: none; stroke: currentColor; fill: none; stroke-width: 2; }

    /* === Diet filter pills (F&B only) === */
    .diet-pills { display: flex; gap: 6px; padding: 2px 0; min-width: 0; overflow-x: auto; }
    .diet-pill {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 7px 12px; border-radius: var(--radius-full);
      background: var(--surface); border: 1px solid var(--border-light);
      color: var(--text-secondary); font: 600 12.5px Inter, sans-serif; cursor: pointer;
      transition: all 0.18s ease; min-height: 36px;
    }
    .diet-pill:hover { color: var(--text); border-color: var(--text-muted); }
    .diet-pill.active--all    { background: var(--accent); color: var(--theme-accent-fg, #111); border-color: var(--accent); }
    .diet-pill.active--veg    { background: var(--green); color: #fff; border-color: var(--green); }
    .diet-pill.active--nonveg { background: var(--red);   color: #fff; border-color: var(--red); }

    /* === Brand filter pills (only rendered when catalog has >= 2 brands) ===
       Reuses the diet-pill aesthetic with neutral coloring — no red/green
       semantic, since brand is identity, not category. */
    .brand-pills { display: flex; gap: 6px; padding: 2px 0; overflow-x: auto; min-width: 0; }
    .brand-pill {
      display: inline-flex; align-items: center; gap: 4px;
      padding: 7px 12px; border-radius: var(--radius-full);
      background: var(--surface); border: 1px solid var(--border-light);
      color: var(--text-secondary); font: 600 12.5px Inter, sans-serif;
      cursor: pointer; transition: all 0.18s ease; min-height: 36px;
      white-space: nowrap; flex-shrink: 0;
    }
    .brand-pill:hover { color: var(--text); border-color: var(--text-muted); }
    .brand-pill.active { background: var(--accent); color: var(--theme-accent-fg, #111); border-color: var(--accent); }
    .brand-pill .count { font-weight: 500; opacity: 0.7; }
    .brand-pill.active .count { opacity: 0.85; }

    /* Veg / Non-veg dot indicators (square box + filled circle) */
    .vdot, .nvdot {
      width: 14px; height: 14px; border-radius: 3px; border: 2px solid;
      display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    }
    .vdot  { border-color: var(--green); }
    .nvdot { border-color: var(--red); }
    .vdot::after, .nvdot::after {
      content: ''; width: 7px; height: 7px; border-radius: 50%;
    }
    .vdot::after  { background: var(--green); }
    .nvdot::after { background: var(--red); }
    .vdot--sm, .nvdot--sm { width: 11px; height: 11px; }
    .vdot--sm::after, .nvdot--sm::after { width: 5px; height: 5px; }
    .cat-tab {
      flex-shrink: 0; padding: 7px 14px; border-radius: var(--radius-full);
      background: var(--surface); border: 1px solid var(--border-light);
      color: var(--text-secondary); font: 600 0.78rem var(--font-body); cursor: pointer;
      transition: all 0.2s ease; white-space: nowrap; min-height: 36px;
    }
    .cat-tab:hover { border-color: var(--border-light); color: var(--text); }
    .cat-tab.active { background: var(--accent); color: var(--theme-accent-fg, #111); border-color: var(--accent); box-shadow: 0 4px 16px var(--accent-shadow); }
    .cat-tab .count { opacity: 0.65; font-weight: 500; margin-left: 4px; }

    /* Subcategory chip row (migration 026). A lighter, smaller variant
       of .cat-tabs that appears beneath the category tabs when the
       active top-level category has populated subcategories. Pill
       shape, slightly smaller, with a subtle top divider so it reads
       as a second tier. */
    .subcat-tabs {
      display: flex; gap: 6px; overflow-x: auto;
      padding: 8px 0 2px; min-width: 0;
      margin-top: 6px; border-top: 1px solid var(--border-light);
    }
    .subcat-tab {
      flex-shrink: 0; padding: 5px 12px; border-radius: var(--radius-full);
      background: transparent; border: 1px solid var(--border-light);
      color: var(--text-secondary); font: 600 0.74rem var(--font-body); cursor: pointer;
      transition: all 0.2s ease; white-space: nowrap; min-height: 30px;
    }
    .subcat-tab:hover { color: var(--text); border-color: var(--border); }
    .subcat-tab.active {
      background: var(--accent-dim); color: var(--accent);
      border-color: var(--accent);
    }
    /* Offers tab — accent-tinted dot leading the label. Subtle when
       inactive; the dot inverts to dark when the tab is active so it
       reads against the bright accent background. */
    .cat-tab--offers .cat-tab-dot {
      display: inline-block; width: 6px; height: 6px; border-radius: 50%;
      background: var(--accent); margin-right: 6px; vertical-align: middle;
      box-shadow: 0 0 0 3px var(--accent-dim);
    }
    .cat-tab--offers.active .cat-tab-dot { background: #111; box-shadow: 0 0 0 3px rgba(0,0,0,0.18); }

    /* === Offers carousel === */
    /* Sits between the sticky filter block and the main grid. Edge-to-
       edge horizontal scroller; cards are compact and uniform so the
       customer can scan the strip in one sweep. No card images means
       no big tap target either — keep the strip tight and let the
       detail modal handle the rich view. */
    /* Wrap is full-bleed inside the .main column (which has 16px L/R
       padding); the head + scroll re-apply that padding so labels and
       card edges align with the rest of the page while the strip can
       scroll past the column edges. */
    .offers-carousel-wrap { margin: 12px -16px 4px; padding: 0; }
    .offers-carousel-head {
      display: flex; align-items: baseline; justify-content: space-between;
      padding: 0 16px 8px; gap: 10px;
    }
    .offers-carousel-label {
      font: 700 0.78rem var(--font-body); color: var(--text); letter-spacing: 0.4px;
      text-transform: uppercase; position: relative; padding-left: 14px;
    }
    .offers-carousel-label::before {
      content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
      width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-dim);
    }
    .offers-carousel-count { font: 500 0.72rem var(--font-body); color: var(--text-muted); }
    .offers-carousel {
      display: flex; gap: 10px; overflow-x: auto; padding: 2px 16px 12px;
      scroll-snap-type: x proximity; min-width: 0;
    }
    .offers-carousel::-webkit-scrollbar { display: none; }
    .offers-carousel { scrollbar-width: none; -ms-overflow-style: none; }
    /* drag cursor already covered by the .cat-tabs / .diet-pills /
       .brand-pills group above via the shared selector; this kept
       only for backwards compatibility with previous CSS readers
       scanning for offers-specific class. The helper class is the
       single source of truth, and offers-carousel is now included
       in _attachAllHorizontalScrolls() so it gets .is-draggable /
       .is-dragging just like the other strips. */
    .offer-card {
      flex: 0 0 156px; scroll-snap-align: start;
      background: var(--surface); border: 1px solid var(--border-light); border-radius: var(--radius-md);
      overflow: hidden; cursor: pointer; transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
      display: flex; flex-direction: column;
    }
    .offer-card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 8px 24px var(--accent-shadow); }
    .offer-card-img {
      position: relative; width: 100%; aspect-ratio: var(--card-aspect);
      background: var(--surface2) var(--img-anchor) / cover no-repeat;
    }
    .offer-card-pill {
      position: absolute; top: 8px; left: 8px;
      background: var(--accent); color: var(--theme-accent-fg, #111);
      font: 700 0.7rem var(--font-body); padding: 3px 7px; border-radius: 999px;
      letter-spacing: 0.2px; line-height: 1;
      /* Outline ring + drop shadow lift the pill off the image so a
         same-color photo (e.g. accent-pink shirt) can't camouflage it.
         Subtle white text-shadow under the dark digits gives a halo
         that survives on any background. */
      box-shadow: 0 0 0 1px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.35);
      text-shadow: 0 1px 0 rgba(255,255,255,0.45);
    }
    .offer-card-body { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 4px; }
    .offer-card-name {
      font: 600 0.82rem var(--font-body); color: var(--text); line-height: 1.3;
      display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;
      overflow: hidden; min-height: 2.1em;
    }
    .offer-card-prices { display: flex; align-items: baseline; gap: 6px; font-variant-numeric: tabular-nums; }
    .offer-card-price { font: 700 0.92rem var(--font-body); color: var(--text); white-space: nowrap; font-variant-numeric: tabular-nums; }
    .offer-card-old { font: 500 0.72rem var(--font-body); color: var(--text-muted); text-decoration: line-through; white-space: nowrap; font-variant-numeric: tabular-nums; }

    .section-head { margin: 18px 0 12px; display: flex; align-items: baseline; gap: 10px; }
    .section-head h2 { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; color: var(--text); letter-spacing: -0.01em; }
    .section-head .small { font-size: 0.72rem; color: var(--text-muted); margin-left: auto; }
    /* "← Back to menu" link shown above the filtered results when the
       customer would otherwise be in the desktop sectioned view. Quiet
       button styling so it reads as an affordance, not a primary CTA. */
    .back-to-menu {
      display: inline-flex; align-items: center; gap: 6px;
      margin: 14px 0 8px; padding: 6px 12px 6px 8px;
      background: transparent; border: 1px solid var(--border-light);
      border-radius: var(--radius-full);
      color: var(--text-secondary);
      font: 600 0.78rem var(--font-body); cursor: pointer;
      transition: color 0.15s, border-color 0.15s, background 0.15s;
    }
    .back-to-menu:hover {
      color: var(--accent); border-color: var(--accent);
      background: var(--accent-dim);
    }
    .back-to-menu svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }

    /* === Menu card === */
    .menu-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); transition: transform 0.3s var(--ease), box-shadow 0.3s ease, border-color 0.3s ease; }
    .menu-card:hover { transform: translateY(-2px); border-color: var(--border-light); box-shadow: 0 8px 32px rgba(0,0,0,0.3); }
    .menu-card-inner { display: flex; cursor: pointer; }
    .card-img-wrap { width: 140px; flex-shrink: 0; position: relative; overflow: hidden; background: var(--surface2); aspect-ratio: var(--card-aspect); }
    .card-img { width: 100%; height: 100%; object-fit: cover; object-position: var(--img-anchor); transition: transform 0.5s var(--ease); }
    .menu-card:hover .card-img { transform: scale(1.06); }
    /* Image-less product monogram: a soft accent-tinted surface + accent
       letter (was a flat gray that read as "broken/empty"). Brand-colored
       so a no-photo product still looks intentional in any theme. */
    .card-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, var(--accent-dim) 0%, var(--surface2) 100%); color: color-mix(in srgb, var(--accent) 70%, var(--text)); font-family: var(--font-display); font-weight: 700; font-size: 2.5rem; opacity: 0.9; }
    /* Featured + OOS badges sit ON the card image. Both ship with
       their own background, but a same-color photo can still
       camouflage them (a pink fashion model photo behind the rose
       accent badge, a black backdrop behind the dark OOS badge).
       Defensive halo: thin dark ring + drop shadow on Featured;
       backdrop-blur stays on OOS but gain a thin ring + text-shadow
       so the white text reads on any image. */
    .bestseller { position: absolute; top: 8px; left: 0; z-index: 2; padding: 3px 9px 3px 7px; border-radius: 0 4px 4px 0; background: linear-gradient(135deg, var(--accent), var(--accent-light)); color: var(--theme-accent-fg, #111); font-size: 0.6rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; display: inline-flex; align-items: center; gap: 3px; box-shadow: 0 0 0 1px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.35); }
    .oos-badge { position: absolute; bottom: 8px; left: 8px; z-index: 2; padding: 3px 8px; border-radius: 4px; background: rgba(0,0,0,0.78); color: #fff; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; backdrop-filter: blur(4px); box-shadow: 0 0 0 1px rgba(255,255,255,0.1), 0 2px 6px rgba(0,0,0,0.4); text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
    /* AI-generated cover photos are labelled, so a customer is never misled
       about what will arrive (migrations 176/177). Bottom-RIGHT, deliberately:
       .oos-badge already owns bottom-left, and a sold-out generated dish must
       show both without them colliding.

       Deliberately quiet — lowercase-ish weight, no icon shouting "FAKE". The
       point is disclosure, not a warning. But it still has to be legible over a
       photograph of any brightness, so it borrows the same defence .oos-badge
       uses: translucent dark fill + blur + a thin light ring + text-shadow.
       pointer-events:none so it never eats a tap meant for the card. */
    .ai-badge { position: absolute; bottom: 8px; right: 8px; z-index: 2; padding: 2px 7px; border-radius: 4px; background: rgba(0,0,0,0.55); color: rgba(255,255,255,0.92); font-size: 0.58rem; font-weight: 600; letter-spacing: 0.3px; backdrop-filter: blur(4px); box-shadow: 0 0 0 1px rgba(255,255,255,0.12); text-shadow: 0 1px 2px rgba(0,0,0,0.5); pointer-events: none; }
    /* The same disclosure on a product's own page. A caption rather than an
       overlay badge, for two reasons: there is room for the fuller wording where
       a 200px card had none, and the gallery interleaves the generated cover
       with the merchant's real gallery photos — a badge pinned over the track
       would be claiming something untrue about the ones they shot themselves. */
    .ai-note { margin-top: 8px; font-size: 0.7rem; line-height: 1.4; color: var(--text-muted, var(--text-secondary)); display: flex; align-items: flex-start; gap: 5px; }
    .ai-note::before { content: 'ⓘ'; opacity: 0.7; }
    .card-body { flex: 1; padding: 12px 14px; display: flex; flex-direction: column; min-width: 0; }
    .card-name { font-family: var(--font-display); font-size: 0.95rem; font-weight: 700; color: var(--text); line-height: 1.3; }
    /* Product-name links (crawlable PDP anchors) inherit card text styling,
       not the default underlined link look. Covers both card renderers:
       a.card-name (rail) and .card-name > a (grid). */
    a.card-name, .card-name a { color: inherit; text-decoration: none; }
    a.card-name:hover, .card-name a:hover { color: var(--accent); }
    .card-desc { font-size: 0.73rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
    .card-meta { font-size: 0.68rem; color: var(--text-secondary); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; }
    /* Grocery (migration 041): declared pack size + auto unit price, and the
       low-stock urgency nudge. Render only when the data is present, so
       non-grocery cards look exactly as before. */
    .card-grocery { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin-top: 3px; }
    .card-netqty { font-size: 0.74rem; color: var(--text-muted); font-weight: 500; }
    .card-unitprice { font-size: 0.7rem; font-weight: 700; color: var(--text-secondary); background: var(--surface2); border-radius: 5px; padding: 1px 6px; white-space: nowrap; }
    .card-lowstock { margin-top: 3px; font: 700 0.72rem var(--font-body); color: var(--lowstock, #e0701a); letter-spacing: 0.2px; }
    /* Ships-in / made-to-order — calm informational sibling of the low-stock
       nudge (never urgency-coloured). */
    .card-leadtime { margin-top: 3px; font: 600 0.72rem var(--font-body); color: var(--text-muted); letter-spacing: 0.2px; }
    /* Per-category availability chip (migration 087). open = subtle accent,
       closed = muted/warning, pre-order = accent. Inline so it sits under the
       name without disturbing the card grid. */
    .card-avail { margin-top: 3px; display: inline-flex; align-items: center; gap: 4px; font: 700 0.7rem var(--font-body); letter-spacing: 0.2px; padding: 1px 7px; border-radius: 999px; }
    .card-avail.is-open { color: #0a7d33; background: rgba(16,160,64,0.1); }
    .card-avail.is-closed { color: #b4690e; background: rgba(224,112,26,0.12); }
    .card-avail.is-pre { color: var(--accent, #6d28d9); background: color-mix(in srgb, var(--accent, #6d28d9) 12%, transparent); }
    .card-avail::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
    /* "/kg" suffix on a sell-by-weight item's price — smaller + muted. */
    .card-perunit { font-size: 0.72em; font-weight: 600; color: var(--text-muted); margin-left: 1px; }
    /* Footer holds the price (when shown) + the action button. For F&B
       items with the inline variant row above, the footer carries only
       the Options button — `.add-btn { margin-left: auto }` below
       forces it to the right whether it has a sibling or not. */
    /* WRAPS on purpose. The price and the action share one row only while
       they BOTH fit; the moment they don't, the button drops to its own row
       instead of being painted over. Without flex-wrap the price (which sets
       min-width: 0, so flexbox is free to squeeze it below its own content
       width) simply overflowed its box and ran straight through the button —
       reproduced on every theme at every phone width, and worst on the ones
       with the biggest price type. A price is never truncated or ellipsised:
       a half-shown number is worse than a taller card. */
    .card-footer { display: flex; flex-wrap: wrap; align-items: center; margin-top: auto; padding-top: 8px; gap: 8px 10px; }
    /* WhatsApp-mode card footer: stack so the price is always fully visible
       with a full-width "Order" button below it (never overlaps the price). */
    .card-footer.is-wa { flex-direction: column; align-items: stretch; gap: 9px; }
    .card-footer.is-wa .card-price { width: 100%; }
    .card-price { font-size: 1.05rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap; row-gap: 2px; min-width: 0; flex: 999 1 auto; }
    .card-price .rupee { color: var(--accent-text); font-weight: 600; }
    .card-price small { font-size: 0.65rem; color: var(--text-muted); font-weight: 500; margin-right: 4px; text-transform: uppercase; letter-spacing: 0.3px; }
    /* Atomic group — ₹ + amount stay glued together even when the rest
       of the price stack (MRP strike + % pill) wraps to a second line. */
    .card-price-now { display: inline-flex; align-items: baseline; white-space: nowrap; }
    /* "Free" / "Price on request" tag shown instead of ₹ for free / enquiry-
       priced services. It's a label, not a number: compact, accent-tinted, and
       it WRAPS (no nowrap) so a long phrase never overflows into the Book
       button on a tight card. */
    .price-tag { font-weight: 700; color: var(--accent-text); letter-spacing: 0.01em; line-height: 1.2; }
    .card-price .price-tag { font-size: 0.82rem; }
    .sf-pdp-price.price-tag { font: 700 1.1rem var(--font-body); color: var(--accent-text); }
    /* Strike-through original price (MRP) sits to the right of the live
       price. Smaller, muted, with a line-through. The discount % pill
       follows it — small accent-tinted chip, no animation, no emoji. */
    .price-old { font-size: 0.78rem; font-weight: 500; color: var(--text-muted); text-decoration: line-through; text-decoration-thickness: 1.5px; text-decoration-color: rgba(127,127,127,0.55); font-variant-numeric: tabular-nums; }
    .price-off { font-size: 0.7rem; font-weight: 700; color: var(--accent); background: var(--accent-dim); padding: 2px 6px; border-radius: 999px; letter-spacing: 0.2px; line-height: 1; white-space: nowrap; }
    /* Store-wide sale ribbon (migration 057) — theme-accent banner at the very
       top of the storefront when an automatic sale is active. */
    .storewide-sale-bar { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; padding: 8px 16px; background: var(--accent, #16a34a); color: #fff; font-weight: 700; font-size: 0.82rem; letter-spacing: 0.02em; text-align: center; }
    .storewide-sale-bar__pct { background: rgba(255,255,255,0.22); padding: 2px 9px; border-radius: 999px; font-size: 0.76rem; }
    /* === Closed / paused / pre-order strip (migration 161) ===
       Full-bleed, above the hero, on every layout. Deliberately NOT the solid
       shout of the sale bar above: this is bad news delivered calmly, so it
       reads as a tinted notice with a bottom rule rather than a banner
       competing with the brand. Static in the flow, not sticky — the checkout
       footer bar is the second, unmissable line of defence, and a sticky strip
       would fight the sticky nav the simple layouts already have. */
    .closed-strip {
      /* nowrap on the OUTER row so the dot always stays beside the text; the
         text itself wraps internally. Without this the dot drops onto its own
         line the moment the copy needs two rows on a phone. */
      display: flex; align-items: center; justify-content: center; gap: 9px;
      padding: 9px 16px; text-align: center; flex-wrap: nowrap;
      font-size: 0.8rem; line-height: 1.35;
      background: var(--cs-bg); color: var(--cs-fg);
      border-bottom: 1px solid var(--cs-line);
    }
    .closed-strip__txt { display: inline-flex; align-items: baseline; gap: 7px; flex-wrap: wrap; justify-content: center; }
    .closed-strip__txt strong { font-weight: 680; letter-spacing: 0.01em; }
    .closed-strip__txt span { opacity: 0.82; }
    .closed-strip__dot {
      width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
      background: currentColor; opacity: 0.65;
    }
    /* Shut and paused are the same news to a customer: you can't order yet.
       Amber, not red — the shop isn't broken, it's just not open.
       Uses the theme's own --warn tokens (translucent tints) rather than fixed
       hex, so the strip sits INSIDE a dark or brand-tinted storefront instead
       of punching a white band across the top of it. */
    .cs-closed, .cs-paused {
      --cs-bg: var(--warn-dim); --cs-fg: var(--warn); --cs-line: rgba(245,158,11,0.3);
    }
    /* The sub-line is the same amber at lower emphasis. Against the tint that
       lands around 60% contrast — enough to read as secondary, not so faint it
       fails on the dark themes. */
    .cs-closed .closed-strip__txt span,
    .cs-paused .closed-strip__txt span { opacity: 0.75; }
    /* Pre-order is an invitation, not a warning, so it takes the brand accent
       and a live dot: something IS available here. */
    .cs-closed.is-pre { --cs-bg: var(--accent-dim); --cs-fg: var(--accent-text); --cs-line: var(--accent); }
    .cs-closed.is-pre .closed-strip__dot { animation: csPulse 2.4s ease-in-out infinite; }
    @keyframes csPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.9; } }
    @media (prefers-reduced-motion: reduce) { .closed-strip__dot { animation: none !important; } }
    @media (max-width: 480px) { .closed-strip { font-size: 0.76rem; padding: 8px 12px; } }
    /* Inline F&B variant preview (Half ₹120 / Full ₹220) — when a
       portion is on offer we strike through its MRP just like the
       item-level price, but at variant-row size. */
    .cv-pr .cv-old { font-size: 0.72rem; color: var(--text-muted); text-decoration: line-through; margin-left: 4px; font-weight: 500; }
    /* Variant chip inside the detail modal — strike-through for chips
       that carry their own compare_at_price. Sits under the chip price. */
    .variant-old { display: block; font-size: 0.62rem; color: rgba(255,255,255,0.55); text-decoration: line-through; font-weight: 500; margin-top: 1px; line-height: 1; }
    .variant.sel .variant-old { color: rgba(255,255,255,0.7); }
    /* Modal price row — bigger pill, struck-through MRP next to the
       live price using the same tokens as the card. */
    .modal-price-old { font-size: 0.95rem; font-weight: 500; color: var(--text-muted); text-decoration: line-through; text-decoration-thickness: 1.5px; font-variant-numeric: tabular-nums; }
    .modal-price-off { font-size: 0.78rem; font-weight: 700; color: var(--accent); background: var(--accent-dim); padding: 3px 9px; border-radius: 999px; letter-spacing: 0.2px; line-height: 1; }
    /* Cart "You saved" line — single subtle row, accent text. Sits
       between subtotal and grand total. Horizontal padding MUST match
       .cart-total-row's 16px so the savings line aligns with the
       totals stack instead of bleeding to the container edge. */
    .cart-savings {
      display: flex; justify-content: space-between; align-items: center;
      padding: 8px 16px; background: var(--surface2);
      font-size: 0.85rem; color: var(--accent); font-weight: 600;
    }
    .cart-savings .val { font-variant-numeric: tabular-nums; }
    /* Free-delivery threshold nudge. Green-tinted strip below the
       totals — matches the Swiggy / Zomato "add ₹X more to save"
       pattern. Disappears once the threshold is hit. */
    .delivery-nudge {
      margin: 10px 16px 0;
      padding: 10px 12px;
      background: var(--green-dim); color: var(--green);
      border: 1px solid color-mix(in srgb, var(--green) 35%, transparent);
      border-radius: var(--radius-sm);
      font: 600 0.83rem var(--font-body); text-align: center;
    }
    .delivery-nudge strong { font-weight: 800; }
    /* F&B portion preview (Zomato/Swiggy pattern). Standalone row above
       the action footer, full body width — variants get proper breathing
       room and wrap naturally on any viewport without competing with
       the Options button for horizontal space. In-stock variants only;
       OOS states are surfaced in the options modal where customers can
       actually act on them. */
    .card-variants { display: flex; flex-wrap: wrap; column-gap: 14px; row-gap: 4px; align-items: baseline; min-width: 0; margin-top: 6px; font-size: 0.85rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; line-height: 1.35; }
    .card-variants .cv { display: inline-flex; align-items: baseline; white-space: nowrap; }
    .card-variants .cv-nm { color: var(--text-secondary); font-weight: 500; margin-right: 4px; }
    .card-variants .rupee { color: var(--accent); font-weight: 600; margin-right: 1px; }

    /* inline-flex + center alignment is critical here. Without it,
       the button defaults to inline-block, and when min-height (36px)
       is taller than the natural content height (padding + 1-line
       text ≈ 26px), the extra vertical space DOESN'T center — text
       floats off the geometric middle of the border. The mismatch is
       most visible on offer cards where the adjacent .card-price
       wraps to two rows, making the button look off-axis next to a
       taller sibling. justify-content: center handles the same problem
       for state swaps (Add / … / ✓ on the is-adding / is-added
       variants) where text width changes between renders. */
    .add-btn { display: inline-flex; align-items: center; justify-content: center; margin-left: auto; padding: 7px 18px; border-radius: var(--radius-xs); font: 600 0.78rem var(--font-body); text-transform: uppercase; letter-spacing: 0.3px; border: 1.5px solid var(--border-light); background: transparent; color: var(--text); cursor: pointer; transition: all 0.2s ease; min-height: 36px; min-width: 64px; line-height: 1; white-space: nowrap; flex-shrink: 0; }
    /* Closed window-mode item (migration 087): muted, non-interactive, but
       still legible so the customer reads "Opens 6 AM". normalCase so a time
       like "Opens 6:00 AM" isn't uppercased into noise. */
    /* Full width WHEN IT WRAPS, natural width when it shares the row with the
       price. The price above carries flex-grow: 999, so on a shared row it
       swallows the free space and this button sits at its content width
       exactly where margin-left:auto used to put it; alone on a wrapped row
       there is no competitor, so flex-grow: 1 fills the line. Scoped to the
       sibling selector on purpose — a footer holding ONLY an Options button
       (F&B cards with an inline variant row) keeps margin-left:auto and stays
       right-aligned rather than suddenly stretching. */
    .card-price ~ .add-btn { margin-left: 0; flex: 1 1 auto; }
    /* The qty stepper is a DIFFERENT element from .add-btn — a card swaps one
       for the other once the item is in the cart — so it needs the same
       treatment or the fix only covers half the states a card can be in.
       Full width when it wraps; when it does, the plus/minus grow with it
       instead of stranding the number between two small buttons. */
    .card-price ~ .qty-ctrl { margin-left: 0; flex: 1 1 auto; justify-content: space-between; }
    .card-price ~ .qty-ctrl .qty-btn { flex: 1 0 auto; }
    /* ONE control footprint — on desktop too. The <=719px block further down
       already pins Add and the stepper to the same width for phones; above it
       the stepper stayed 90px against the Add button it REPLACES at 66px, so a
       card that fitted price + Add on one line reflowed the instant you added
       the item. Measured on a 200px rail card (166px footer): Rs 14,000 is 75px,
       so 75 + 10 + 66 = 151 fits, while 75 + 10 + 90 = 175 does not — the
       stepper dropped to its own row and the card grew 37px under the cursor,
       while the Rs 5,000 card beside it (63px) stayed put. Trimming the stepper
       to ~68px makes the two states interchangeable across this range.
       Scoped to a footer that HAS a price: the steppers on the product page and
       in the cart keep their full size, where there is room and the tap target
       matters more. flex stays 1 1 auto, so a price long enough to wrap anyway
       (lakh-rupee) still gets a full-width stepper on its own row. */
    @media (min-width: 720px) {
      .card-price ~ .qty-ctrl .qty-btn { width: 26px; }
      .card-price ~ .qty-ctrl .qty-val { min-width: 14px; padding: 0 3px; }
    }

    .add-btn.is-disabled { cursor: not-allowed; opacity: 0.75; color: var(--text-muted); border-color: var(--border-light); background: transparent; text-transform: none; letter-spacing: 0.1px; font-size: 0.72rem; }
    /* WhatsApp-enquiry mode buttons — themed (use the store accent like the
       normal Add CTA) with the WhatsApp glyph for channel recognition, so
       they stay on-brand on any palette. Filled accent = primary action. */
    .add-btn.wa-btn { gap: 6px; background: var(--accent); border-color: var(--accent); color: var(--theme-accent-fg, #111); box-shadow: 0 2px 10px var(--accent-shadow, transparent); }
    .add-btn.wa-btn:hover { filter: brightness(1.06); background: var(--accent); border-color: var(--accent); color: var(--theme-accent-fg, #111); }
    .modal-add.wa-btn { display: inline-flex; align-items: center; justify-content: center; }
    .add-btn.wa-btn svg, .modal-add.wa-btn svg { flex: 0 0 auto; opacity: 0.92; }
    /* In the stacked WhatsApp footer the button spans the full card width. */
    .card-footer.is-wa .add-btn.wa-btn { width: 100%; margin-left: 0; padding: 9px 16px; min-height: 40px; }
    .add-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
    .add-btn.is-adding { background: var(--accent); color: var(--theme-accent-fg, #111); border-color: var(--accent); pointer-events: none; animation: addPulse 0.4s ease; }
    .add-btn.is-added { background: var(--green); color: #fff; border-color: var(--green); pointer-events: none; }
    @keyframes addPulse { 0% { transform: scale(1); } 50% { transform: scale(1.08); } 100% { transform: scale(1); } }

    .qty-ctrl { margin-left: auto; display: flex; align-items: center; border-radius: var(--radius-xs); border: 1.5px solid var(--accent); overflow: hidden; background: var(--accent-dim); }
    .qty-btn { width: 32px; height: 36px; display: flex; align-items: center; justify-content: center; background: transparent; color: var(--accent); font-size: 1.1rem; font-weight: 700; border: 0; cursor: pointer; transition: background 0.15s; font-family: var(--font-body); }
    .qty-btn:hover { background: var(--accent-dim); }
    .qty-val { min-width: 24px; padding: 0 5px; white-space: nowrap; text-align: center; font-size: 0.85rem; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
    /* Weight readout in the detail-view qty stepper for sell-by-weight goods. */
    .qty-readout { padding: 0 12px; min-width: 56px; text-align: center; white-space: nowrap; font-size: 0.95rem; font-weight: 700; }

    .empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
    .empty-state svg { width: 40px; height: 40px; stroke: var(--text-muted); fill: none; stroke-width: 1.5; margin-bottom: 12px; }
    .empty-state h3 { font-family: var(--font-display); font-size: 1.1rem; color: var(--text-secondary); margin-bottom: 4px; }
    .empty-state p { font-size: 0.85rem; }

    .reveal-ready .reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.5s ease, transform 0.5s var(--ease); }
    .reveal-ready .reveal.visible { opacity: 1; transform: translateY(0); }

    /* === Cart strip (bottom) === */
    .cart-strip { position: fixed; bottom: 0; left: 0; right: 0; z-index: 40; padding: 12px 16px; padding-bottom: max(12px, env(safe-area-inset-bottom)); pointer-events: none; }
    /* When the floating cart bar is showing, reserve space at the page bottom so
       the footer (incl. the "Built with ZSite" credit) clears it instead of
       being covered — keeps the footer reachable + the credit always visible. */
    body.cart-strip-open { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
    .cart-strip-inner {
      max-width: 480px; margin: 0 auto;
      background: linear-gradient(135deg, var(--accent), var(--accent-light));
      border-radius: var(--radius); padding: 14px 16px;
      display: flex; align-items: center; justify-content: space-between;
      cursor: pointer; pointer-events: auto;
      box-shadow: 0 -4px 24px rgba(0,0,0,0.4), 0 4px 20px var(--accent-shadow);
      transition: transform 0.2s ease; color: var(--theme-accent-fg, #111);
    }
    .cart-strip-inner:hover { transform: translateY(-2px); }
    .cart-strip-inner:active { transform: scale(0.98); }
    .cart-strip-left { display: flex; align-items: center; gap: 10px; }
    .cart-strip-count { width: 28px; height: 28px; border-radius: var(--radius-xs); background: rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 0.82rem; font-weight: 700; }
    .cart-strip-label { font-size: 0.85rem; font-weight: 700; }
    .cart-strip-label small { font-weight: 500; font-size: 0.7rem; opacity: 0.7; display: block; margin-top: 1px; }
    .cart-strip-total { font-size: 1.05rem; font-weight: 800; display: flex; align-items: center; gap: 4px; }
    .cart-strip-arrow { font-size: 1.1rem; margin-left: 4px; opacity: 0.7; }

    /* === Modal scroll-lock ===
       Applied to <body> when ANY modal (cart, variant, QR) is open so
       the underlying page can't scroll behind the dim backdrop on
       mobile. We deliberately do NOT set `touch-action: none` here —
       it breaks form input handling on iOS Safari (focus / typing
       events get suppressed), which was causing the checkout drawer
       to misbehave the moment customers tapped the address field. */
    body.modal-open { overflow: hidden; }
    /* Lock page scroll while the full-screen search overlay is open. */
    body.search-open { overflow: hidden; }

    /* === Cart drawer === */
    .cart-overlay { position: fixed; inset: 0; z-index: 50; }
    .cart-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); animation: fadeIn 0.25s ease; }
    @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    .cart-panel {
      position: absolute; bottom: var(--kb-gap, 0px); left: 0; right: 0;
      max-height: var(--modal-h); background: var(--surface);
      border-top: 1px solid var(--border-light);
      border-radius: 20px 20px 0 0;
      display: flex; flex-direction: column; overflow: hidden;
      animation: slideUp 0.35s var(--ease);
    }
    @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
    /* Desktop cart-panel slide that preserves horizontal centering. The
       mobile `slideUp` only animates Y, but on desktop the base style
       has `translateX(-50%)` for centering — without a composed keyframe
       the animation strips that transform and the panel slides up from
       the bottom-LEFT, then snaps to center when the animation ends. */
    @keyframes cartSlideUpDesktop {
      from { transform: translate(-50%, 100%); }
      to   { transform: translate(-50%, 0);    }
    }
    @media (min-width: 720px) {
      .cart-panel {
        left: 50%; transform: translateX(-50%); right: auto;
        width: 92%; max-width: 540px;
        animation: cartSlideUpDesktop 0.35s var(--ease);
      }
    }
    .drawer-handle { width: 40px; height: 4px; border-radius: 2px; background: var(--surface3); margin: 8px auto 0; }
    .cart-header {
      display: flex; align-items: center; justify-content: space-between;
      padding: 12px 16px; padding-right: 88px;  /* room for floating buttons */
      border-bottom: 1px solid var(--border); flex-shrink: 0;
    }
    .cart-header h2 { font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; }

    /* === Floating Close / Clear cluster ===
       Anchored to the cart-panel's top-right corner with position:absolute
       so they are ALWAYS visible — independent of cart body scroll,
       independent of flex reflow, and immune to the position:sticky
       quirks that bite on iOS Safari inside a flex+overflow:hidden parent. */
    .cart-fab-cluster {
      position: absolute; top: 12px; right: 12px; z-index: 10;
      display: flex; gap: 6px; align-items: center;
      pointer-events: none;  /* let the buttons themselves take pointer */
    }
    /* Both the chip background AND the icon derive from the SAME theme tokens
       (--text over --surface), so contrast is guaranteed on every theme —
       light or dark. The old hard-coded near-black fill made the dark
       --text-secondary icon vanish on light themes. A clear border + shadow
       keep the control legible even where its tint matches the panel. */
    .cart-fab-cluster button {
      pointer-events: auto;
      width: 36px; height: 36px;
      border-radius: 50%; border: 1px solid var(--border);
      background: color-mix(in srgb, var(--text) 7%, var(--surface));
      color: var(--text-secondary); cursor: pointer;
      display: grid; place-items: center;
      backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
      transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease;
      box-shadow: 0 2px 10px color-mix(in srgb, var(--text) 22%, transparent);
    }
    .cart-fab-cluster button:hover {
      color: var(--text);
      background: color-mix(in srgb, var(--text) 14%, var(--surface));
      border-color: var(--text-muted);
      box-shadow: 0 4px 16px color-mix(in srgb, var(--text) 28%, transparent);
    }
    /* The clear-cart (delete) chip reads as destructive — a muted red at rest
       that stays visible on any theme, filling solid red on hover. */
    .cart-fab-cluster button.danger { color: var(--red); }
    .cart-fab-cluster button.danger:hover { color: #fff; background: var(--red); border-color: var(--red); }
    .cart-fab-cluster button:active { transform: scale(0.93); }
    .cart-fab-cluster button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .cart-fab-cluster button svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.1; }

    .cart-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
    .cart-line { display: grid; grid-template-columns: 48px 1fr auto; gap: 12px; padding: 10px 16px; align-items: center; border-bottom: 1px solid var(--border); }
    .cart-line:last-of-type { border-bottom: 0; }
    .cart-thumb { width: 48px; height: 48px; border-radius: var(--radius-xs); background: var(--surface2) center/cover no-repeat; border: 1px solid var(--border); }
    .cart-line .name { font-size: 0.85rem; font-weight: 600; line-height: 1.3; }
    .cart-line .sub { font-size: 0.7rem; color: var(--text-muted); margin-top: 2px; }
    .cart-line .ctrl { display: inline-flex; align-items: center; margin-top: 6px; border: 1.5px solid var(--accent); border-radius: var(--radius-xs); background: var(--accent-dim); overflow: hidden; }
    .cart-line .ctrl button { width: 28px; height: 28px; border: 0; background: transparent; color: var(--accent); font-size: 1rem; font-weight: 700; cursor: pointer; transition: background 0.15s; }
    .cart-line .ctrl button:hover { background: var(--accent-dim); }
    .cart-line .ctrl span { padding: 0 8px; font-size: 0.78rem; font-weight: 700; color: var(--accent); }
    .cart-line .price { font-size: 0.9rem; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }

    .cart-empty { padding: 40px 24px; text-align: center; color: var(--text-muted); }
    .cart-empty svg { width: 48px; height: 48px; stroke: var(--text-muted); fill: none; stroke-width: 1.5; margin-bottom: 12px; }
    .cart-empty h3 { font-family: var(--font-display); font-size: 1.1rem; color: var(--text-secondary); margin-bottom: 4px; }

    .cart-total-row {
      display: flex; align-items: baseline; justify-content: space-between;
      padding: 14px 16px; margin: 8px 0 0; background: var(--surface2);
      border-top: 1px solid var(--border);
    }
    .cart-total-row .lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
    .cart-total-row .val { font-family: var(--font-display); font-size: 1.4rem; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
    /* Coupon codes (migration 230) — the field above the totals */
    .cpn-box{ margin:10px 0 12px; border:1px dashed var(--border,#ddd); border-radius:12px; padding:10px 12px; }
    .cpn-box .row{ display:flex; gap:8px; } .cpn-box .row input{ flex:1; min-width:0; height:40px; border:1px solid var(--border,#ddd); border-radius:9px; padding:0 12px; font-family:inherit; font-weight:700; font-size:14px; letter-spacing:.06em; text-transform:uppercase; background:#fff; }
    .cpn-box .row button{ height:40px; padding:0 14px; border-radius:9px; border:none; background:var(--accent,#111); color:var(--accent-ink,#fff); font-family:inherit; font-weight:700; font-size:13px; cursor:pointer; } .cpn-box .row button:disabled{ opacity:.5; }
    .cpn-box .ok{ display:flex; align-items:center; gap:8px; font-size:13px; color:#14532d; font-weight:700; } .cpn-box .ok button{ margin-left:auto; border:none; background:none; color:var(--text-muted,#777); font-family:inherit; font-weight:700; font-size:12.5px; cursor:pointer; }
    .cpn-box .err{ color:#9f1b1b; font-size:12.5px; margin-top:6px; font-weight:600; }
    .cpn-box .chips{ display:flex; gap:8px; overflow-x:auto; scrollbar-width:none; margin-top:8px; } .cpn-box .chips::-webkit-scrollbar{ display:none; }
    .cpn-box .chips button{ flex:0 0 auto; border:1px dashed #15803d; color:#15803d; background:#ecfdf3; border-radius:8px; padding:6px 10px; font-family:inherit; font-weight:800; font-size:11.5px; letter-spacing:.04em; cursor:pointer; white-space:nowrap; }
    .cart-coupon{ display:flex; justify-content:space-between; font-size:13.5px; color:#15803d; font-weight:700; padding:2px 0; }
    /* Subtotal + GST: smaller, neutral color so the Grand Total below
       remains the primary visual anchor. Stacked rows share the same
       padded panel — only the last (Grand Total) gets the accent val. */
    .cart-total-row--sub { padding: 8px 16px 0; margin: 0; background: var(--surface2); border-top: 0; }
    .cart-total-row--sub:first-child { padding-top: 12px; }
    .cart-total-row--sub .val--sub { font-family: var(--font-body); font-size: 0.95rem; font-weight: 600; color: var(--text-secondary); }

    /* === Order section === */
    .order-section { padding: 16px; border-top: 1px solid var(--border); }
    .order-section h3 { font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 10px; }

    .order-types { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
    .order-types.three { grid-template-columns: 1fr 1fr 1fr; }
    .order-type-btn {
      padding: 12px 8px; border-radius: var(--radius-sm);
      border: 1.5px solid var(--border-light); background: var(--surface);
      color: var(--text-secondary); cursor: pointer; transition: all 0.2s ease;
      font: 600 0.82rem var(--font-body); min-height: 56px;
      display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    }
    .order-type-btn:hover { border-color: var(--text-muted); color: var(--text); }
    .order-type-btn.active { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
    .order-type-btn svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }

    .form-group { margin-bottom: 10px; }
    .form-label { display: block; font-size: 0.74rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; }
    /* Out-of-stock substitution radios (migration 041) — stacked tappable rows
       that highlight the chosen option. */
    .subst-opts { display: flex; flex-direction: column; gap: 6px; }
    .subst-opt { display: flex; align-items: center; gap: 9px; padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer; font-size: 0.86rem; color: var(--text); transition: border-color .15s, background .15s; }
    .subst-opt input { accent-color: var(--accent); flex: 0 0 auto; }
    .subst-opt.on { border-color: var(--accent); background: var(--accent-dim); }
    .form-label .req { color: var(--red); }
    .form-input {
      width: 100%; padding: 12px 14px; border-radius: var(--radius-xs);
      border: 1.5px solid var(--border-light); background: var(--surface);
      color: var(--text); font: 500 1rem var(--font-body);  /* 16px prevents iOS focus zoom */
      transition: border-color 0.2s, box-shadow 0.2s; min-height: 48px; outline: none;
    }
    /* Placeholder reads as a faint HINT, never like typed input: a lighter
       theme-derived tint (~40% of text over the field bg) + normal weight
       (typed text is 500). opacity:1 overrides Firefox's default placeholder
       fade so the colour is consistent across browsers. */
    .form-input::placeholder { color: color-mix(in srgb, var(--text) 40%, var(--surface)); font-weight: 400; opacity: 1; }
    .form-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
    textarea.form-input { resize: none; min-height: 60px; }
    /* === Scheduled-delivery slot picker === */
    .slot-picker { margin-bottom: 14px; }
    /* Mixed-schedule pre-order warning (migration 087). */
    .slot-conflict { margin-bottom: 12px; padding: 9px 12px; border-radius: 10px; font: 600 0.82rem var(--font-body); color: #b4690e; background: rgba(224,112,26,0.1); border: 1px solid rgba(224,112,26,0.25); }
    .slot-dates { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; -webkit-overflow-scrolling: touch; }
    .slot-date-chip {
      flex: 0 0 auto; padding: 8px 14px; border-radius: 999px; cursor: pointer;
      border: 1.5px solid var(--border-light); background: var(--surface); color: var(--text);
      font: 600 0.82rem var(--font-body); white-space: nowrap; transition: border-color 0.2s, background 0.2s;
    }
    .slot-date-chip.active { border-color: var(--accent); background: var(--accent); color: #fff; }
    .slot-date-chip.is-disabled { opacity: 0.4; cursor: not-allowed; }
    .slot-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
    .slot-card {
      display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
      padding: 10px 12px; border-radius: var(--radius-xs); cursor: pointer; text-align: left;
      border: 1.5px solid var(--border-light); background: var(--surface); color: var(--text);
      transition: border-color 0.2s, background 0.2s;
    }
    .slot-card.active { border-color: var(--accent); background: var(--accent-dim); }
    .slot-card.is-disabled { opacity: 0.5; cursor: not-allowed; }
    .slot-card-label { font: 700 0.9rem var(--font-body); }
    .slot-card-win { font: 500 0.78rem var(--font-body); color: var(--text-secondary); }
    .slot-card-reason { font: 600 0.68rem var(--font-body); color: var(--red); margin-top: 2px; }
    .slot-card-left { font: 600 0.68rem var(--font-body); color: var(--accent); margin-top: 2px; }
    .slot-summary {
      margin-top: 10px; padding: 8px 12px; border-radius: var(--radius-xs);
      background: var(--accent-dim); color: var(--text); font: 600 0.82rem var(--font-body);
    }
    /* PIN gate: red border when entered PIN doesn't match the
       merchant's allowlist, plus a rejection banner below the input
       with an optional "Switch to Takeaway" escape hatch. */
    .form-input.pin-bad {
      border-color: var(--err, #dc2626);
      box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.12);
    }
    .pin-reject {
      margin-top: 8px; padding: 10px 12px;
      background: rgba(220, 38, 38, 0.08); border: 1px solid rgba(220, 38, 38, 0.3);
      border-radius: var(--radius-xs); color: var(--err, #dc2626);
      font: 500 0.82rem var(--font-body); line-height: 1.4;
      display: flex; flex-direction: column; gap: 8px;
    }
    .pin-switch-btn {
      align-self: flex-start; padding: 6px 12px;
      background: var(--err, #dc2626); color: #fff;
      border: 0; border-radius: var(--radius-xs);
      font: 600 0.78rem var(--font-body); cursor: pointer;
    }
    .pin-switch-btn:hover { filter: brightness(1.1); }
    .hint { margin-top: 6px; font: 400 0.78rem var(--font-body); color: var(--text-muted); line-height: 1.4; }
    /* Merchant-authored delivery note. Info-blue, not red — this is
       context the merchant WANTS to share, not an error. Preserves
       line breaks from the textarea so multi-line notes render
       intact ("Free delivery within 5km.\nCall +91-... for areas
       outside Bangalore." displays as two lines). */
    .delivery-note {
      margin: 12px 0; padding: 10px 14px;
      display: flex; gap: 10px; align-items: flex-start;
      background: var(--accent-dim); border: 1px solid var(--accent);
      border-radius: var(--radius-xs);
      color: var(--text); font: 500 0.82rem var(--font-body);
      line-height: 1.45; white-space: pre-line;
    }
    .delivery-note-icon {
      width: 18px; height: 18px; flex-shrink: 0;
      color: var(--accent); margin-top: 1px;
    }

    /* === Location card (delivery only) ===
       Sits ABOVE the address textarea — pin first, then refine the
       door-level details in text. Industry pattern (Swiggy / Zomato /
       Dunzo): location capture is the high-value action and deserves
       visual priority; the textarea is the fallback for tall buildings,
       weak signal, or customers who decline the permission. */
    .locate-card {
      display: grid; grid-template-columns: 36px 1fr; gap: 12px; align-items: center;
      padding: 14px 16px; border-radius: var(--radius-xs);
      background: var(--accent-dim); border: 1.5px solid var(--accent);
      color: var(--text); cursor: pointer; min-height: 56px;
      transition: filter 0.18s ease, transform 0.18s ease;
      width: 100%; text-align: left; font-family: var(--font-body);
    }
    .locate-card:hover:not(:disabled) { filter: brightness(1.06); }
    .locate-card:active:not(:disabled) { transform: scale(0.99); }
    .locate-card:disabled { opacity: 0.7; cursor: progress; }
    .locate-card .locate-icon {
      width: 36px; height: 36px; border-radius: 50%;
      background: var(--accent); color: var(--theme-accent-fg, #111);
      display: grid; place-items: center; flex-shrink: 0;
    }
    .locate-card .locate-icon svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.2; }
    .locate-card .locate-title { font-size: 0.92rem; font-weight: 700; line-height: 1.25; color: var(--text); }
    .locate-card .locate-sub { font-size: 0.74rem; color: var(--text-secondary); margin-top: 2px; line-height: 1.35; }
    .locate-card .spin { animation: locate-spin 0.9s linear infinite; }
    @keyframes locate-spin { to { transform: rotate(360deg); } }

    /* Compact status strip — used for pinned / error states. Same
       border-radius as .locate-card so the morph between states feels
       coherent. Two-color variants reuse the theme tokens already in
       :root (green for success, red for error). */
    .locate-status {
      display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
      padding: 10px 14px; border-radius: var(--radius-xs);
      font-size: 0.82rem; line-height: 1.4;
    }
    .locate-status.ok   { background: var(--green-dim); border: 1px solid rgba(34,197,94,0.4); color: var(--text); }
    .locate-status.err  { background: var(--red-dim);   border: 1px solid rgba(239,68,68,0.4); color: var(--text); }
    /* Amber variant for "pinned but accuracy is bad" — same shape as
       ok / err but tells the customer the fix is usable yet imprecise.
       Reuses the standard amber tokens. */
    .locate-status.warn { background: rgba(245,158,11,0.12); border: 1px solid rgba(245,158,11,0.4); color: var(--text); }
    .locate-status .locate-mark {
      width: 22px; height: 22px; border-radius: 50%;
      display: grid; place-items: center; flex-shrink: 0;
      font-size: 0.78rem; font-weight: 700;
    }
    .locate-status.ok   .locate-mark { background: var(--green);  color: #fff; }
    .locate-status.err  .locate-mark { background: var(--red);    color: #fff; }
    .locate-status.warn .locate-mark { background: rgb(245,158,11); color: #fff; }
    .locate-status .locate-text { flex: 1; min-width: 0; font-weight: 600; }
    .locate-status .locate-actions { display: inline-flex; gap: 12px; margin-left: auto; }
    .locate-status .locate-link {
      color: var(--text-muted); text-decoration: underline;
      cursor: pointer; font-weight: 500; font-size: 0.78rem;
    }
    .locate-status .locate-link:hover { color: var(--text); }
    /* Sub-rows that hang below the pinned strip. Subnote = low-accuracy
       suggestion (amber). Substrip = re-pin failed warning (red). Both
       are compact so they don't dominate the form. */
    .locate-subnote {
      margin: 6px 0 0; padding: 6px 14px;
      font-size: 0.74rem; color: var(--text-muted); line-height: 1.4;
    }
    .locate-substrip {
      margin: 6px 0 0; padding: 8px 12px;
      display: flex; align-items: center; gap: 8px;
      font-size: 0.78rem; line-height: 1.35;
      border-radius: var(--radius-xs);
    }
    .locate-substrip.err {
      background: var(--red-dim); border: 1px solid rgba(239,68,68,0.4);
      color: var(--text);
    }
    .locate-substrip .locate-mark {
      width: 18px; height: 18px; border-radius: 50%;
      display: grid; place-items: center; flex-shrink: 0;
      font-size: 0.72rem; font-weight: 700;
      background: var(--red); color: #fff;
    }

    /* Blocked-permission card — info-accent (amber-ish), not red.
       Treats the denial as a user choice we're respecting, not an
       error. Stripe / Twilio / Vercel pattern: permission-required
       states are warnings, not errors. */
    .locate-blocked {
      display: flex; flex-direction: column; gap: 12px;
      padding: 14px 16px; border-radius: var(--radius-xs);
      background: var(--warn-dim); border: 1px solid rgba(245,158,11,0.35);
      color: var(--text);
    }
    .locate-blocked-row { display: flex; gap: 12px; align-items: flex-start; }
    .locate-blocked-icon {
      width: 22px; height: 22px; flex-shrink: 0; color: var(--warn);
      margin-top: 2px;
    }
    .locate-blocked-icon svg { width: 100%; height: 100%; }
    .locate-blocked-body { flex: 1; min-width: 0; }
    .locate-blocked-title { font-size: 0.95rem; font-weight: 700; line-height: 1.3; color: var(--text); }
    .locate-blocked-sub { font-size: 0.78rem; color: var(--text-secondary); margin-top: 3px; line-height: 1.5; }

    /* "How to enable location" disclosure toggle. Chevron rotates 180°
       to point up when the inline help is open. Small, non-shouty. */
    .locate-blocked-disclose {
      display: inline-flex; align-items: center; gap: 6px;
      align-self: flex-start;
      padding: 6px 0; border: 0; background: transparent;
      color: var(--warn); font: 600 0.8rem var(--font-body);
      cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
      text-decoration-color: rgba(245,158,11,0.5); text-decoration-thickness: 1px;
    }
    .locate-blocked-disclose:hover { filter: brightness(1.15); }
    .locate-blocked-disclose svg { width: 12px; height: 12px; transition: transform 0.18s ease; }

    /* Inline numbered steps — same idiom as the iOS PWA install sheet
       earlier in this file, so the visual rhythm is consistent. */
    .locate-blocked-help { padding-left: 4px; display: flex; flex-direction: column; gap: 8px; }
    .locate-blocked-platform {
      font: 600 0.7rem var(--font-body); color: var(--warn);
      text-transform: uppercase; letter-spacing: 0.06em;
    }
    .locate-blocked-steps {
      list-style: decimal inside; padding: 0; margin: 0;
      display: flex; flex-direction: column; gap: 6px;
      font-size: 0.85rem; color: var(--text); line-height: 1.5;
    }
    .locate-blocked-steps li {
      padding: 8px 12px; background: rgba(255,255,255,0.03);
      border: 1px solid var(--border); border-radius: var(--radius-xs);
    }

    /* Action row — primary Try Again on the left, ghost "Just type
       address" on the right gives customers a clear out without
       making them feel they HAVE to enable location. */
    .locate-blocked-actions { display: flex; gap: 8px; flex-wrap: wrap; }
    .locate-blocked-btn {
      flex: 1 1 140px;
      padding: 11px 14px; border-radius: var(--radius-xs);
      font: 600 0.85rem var(--font-body); cursor: pointer;
      border: 1.5px solid var(--warn); min-height: 44px;
      transition: filter 0.15s ease, background 0.15s ease, color 0.15s ease;
    }
    .locate-blocked-btn.primary { background: var(--warn); color: #111; }
    .locate-blocked-btn.primary:hover { filter: brightness(1.08); }
    .locate-blocked-btn.ghost { background: transparent; color: var(--warn); }
    .locate-blocked-btn.ghost:hover { background: var(--warn-dim); }

    /* Invalid-field ring. The footer alert says WHAT is wrong; this says
       WHERE. Same geometry as the focus ring (3px halo, no size change) so a
       field never reflows when it turns invalid — only its colour changes.
       One class covers both a bare input and a whole group (slot picker,
       order-type row), which have no border of their own to recolour. */
    .field-invalid {
      box-shadow: 0 0 0 3px var(--red-dim);
      border-radius: var(--radius-xs);
      transition: box-shadow 0.2s ease, border-color 0.2s ease;
    }
    input.field-invalid, textarea.field-invalid, select.field-invalid { border-color: var(--red); }
    input.field-invalid:focus, textarea.field-invalid:focus, select.field-invalid:focus {
      border-color: var(--red); box-shadow: 0 0 0 3px var(--red-dim);
    }

    /* === UPI QR block === */
    .upi-block {
      display: flex; gap: 12px; align-items: center;
      padding: 12px; background: var(--surface2); border-radius: var(--radius-sm);
      border: 1px solid var(--border); margin-top: 12px;
    }
    .upi-qr-wrap {
      width: 96px; height: 96px; flex-shrink: 0;
      display: grid; place-items: center; background: #fff;
      border-radius: var(--radius-xs); padding: 4px; position: relative;
      cursor: zoom-in; border: 1px solid var(--border);
      transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
    }
    .upi-qr-wrap:hover { transform: scale(1.04); border-color: var(--accent); box-shadow: 0 6px 18px var(--accent-shadow); }
    .upi-qr-wrap img { width: 100%; height: 100%; display: block; object-fit: contain; }
    .upi-qr-zoom-hint {
      position: absolute; right: -6px; bottom: -6px; width: 24px; height: 24px;
      border-radius: 50%; background: var(--accent); color: var(--theme-accent-fg, #111);
      display: grid; place-items: center;
      box-shadow: 0 2px 6px var(--accent-shadow); pointer-events: none;
    }
    .upi-info { flex: 1; min-width: 0; }
    .upi-info h4 { font-size: 0.9rem; font-weight: 700; letter-spacing: -0.01em; display: flex; align-items: center; gap: 6px; }
    .upi-info p { font-size: 0.75rem; color: var(--text-secondary); margin-top: 3px; line-height: 1.45; }
    .upi-tap-label { font-size: 0.72rem; font-weight: 600; color: var(--accent); margin-top: 6px; display: inline-flex; align-items: center; gap: 4px; background: none; border: 0; padding: 0; cursor: pointer; font-family: var(--font-body); }
    .upi-disclaimer { font-size: 0.7rem; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }

    /* === Cart footer ===
       Stacked CTA layout. When only WhatsApp checkout is configured,
       the WA button is the single primary CTA at full prominence (green
       brand color, big padding, drop shadow). When the merchant has also
       connected an online payment gateway, Pay Online becomes the
       primary and WhatsApp demotes to a smaller secondary option,
       separated by an "or" divider. Mirror of how Shopify / Stripe
       Checkout / Razorpay's own checkout layer two payment paths. */
    .cart-footer {
      padding: 14px 16px;
      padding-bottom: max(16px, calc(env(safe-area-inset-bottom) + 4px));
      border-top: 1px solid var(--border);
      flex-shrink: 0; background: var(--surface);
    }

    /* === WhatsApp button — primary mode (no gateway) === */
    .wa-btn {
      width: 100%; padding: 14px; border-radius: var(--radius-sm);
      background: var(--whatsapp); color: #fff;
      font: 700 0.95rem var(--font-body); border: 0; cursor: pointer;
      display: flex; align-items: center; justify-content: center; gap: 8px;
      transition: all 0.2s ease; min-height: 52px; letter-spacing: -0.01em;
    }
    .wa-btn:hover:not(:disabled) { background: var(--whatsapp-dark); box-shadow: 0 6px 20px rgba(37,211,102,0.28); }
    .wa-btn:active:not(:disabled) { transform: scale(0.98); }
    .wa-btn:disabled { opacity: 0.65; cursor: not-allowed; }

    /* === WhatsApp button — secondary mode (sits below Pay Online) ===
       Outlined treatment instead of filled. Smaller padding, lighter
       weight. Stays green so the brand cue is intact, but visually
       demoted so customers know Pay Online is the recommended path. */
    .wa-btn.is-secondary {
      background: transparent; color: var(--whatsapp);
      border: 1.5px solid var(--whatsapp);
      min-height: 46px; padding: 10px 14px;
      font-weight: 600; font-size: 0.875rem;
      box-shadow: none;
    }
    .wa-btn.is-secondary:hover:not(:disabled) {
      background: color-mix(in srgb, var(--whatsapp) 8%, transparent);
      color: var(--whatsapp-dark); border-color: var(--whatsapp-dark);
      box-shadow: none;
    }
    .wa-btn.is-secondary svg {
      width: 17px; height: 17px;
    }

    /* === Floating WhatsApp button (FAB) ===
       Merchant-toggled storefront chat affordance (store.whatsapp_float_enabled).
       Brand green, bottom-right, above the mobile cart-strip but below modals
       (z 45) so it tucks behind an open cart/sheet. Circular by default; on
       desktop hover the pill expands to reveal a "Chat with us" label. */
    .wa-fab {
      position: fixed;
      right: max(16px, env(safe-area-inset-right));
      bottom: max(16px, env(safe-area-inset-bottom));
      z-index: 45;
      display: inline-flex; align-items: center;
      height: 56px; padding: 0; border: 0; cursor: pointer;
      border-radius: var(--radius-full);
      background: var(--whatsapp); color: #fff;
      box-shadow: 0 6px 20px rgba(37,211,102,0.45), 0 2px 6px rgba(0,0,0,0.25);
      transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
      animation: waFabIn 0.45s var(--ease) 0.5s both;
    }
    .wa-fab:hover { background: var(--whatsapp-dark); box-shadow: 0 10px 28px rgba(37,211,102,0.5), 0 3px 8px rgba(0,0,0,0.3); }
    .wa-fab:active { transform: scale(0.95); }
    .wa-fab-ico { flex: none; width: 56px; height: 56px; display: grid; place-items: center; }
    .wa-fab-ico svg { width: 30px; height: 30px; }
    .wa-fab-label {
      max-width: 0; overflow: hidden; opacity: 0; white-space: nowrap;
      font: 700 0.95rem var(--font-body); letter-spacing: -0.01em;
      transition: max-width 0.28s var(--ease), opacity 0.2s ease, padding 0.28s var(--ease);
    }
    /* Reveal the label only where real hover exists (desktop) — keeps mobile a
       clean circle. */
    @media (hover: hover) and (pointer: fine) {
      .wa-fab:hover .wa-fab-label { max-width: 160px; opacity: 1; padding-right: 20px; }
    }
    /* Subtle continuous attention ring behind the FAB. */
    .wa-fab::before {
      content: ''; position: absolute; inset: 0; border-radius: inherit;
      animation: waFabPulse 2.8s ease-out infinite; pointer-events: none;
    }
    /* Lift clear of the mobile cart-strip when it's showing (it reserves ~96px). */
    body.cart-strip-open .wa-fab { bottom: calc(96px + env(safe-area-inset-bottom)); }
    @keyframes waFabIn { from { opacity: 0; transform: scale(0.6) translateY(8px); } to { opacity: 1; transform: none; } }
    @keyframes waFabPulse {
      0%   { box-shadow: 0 0 0 0 rgba(37,211,102,0.5); }
      70%  { box-shadow: 0 0 0 14px rgba(37,211,102,0); }
      100% { box-shadow: 0 0 0 0 rgba(37,211,102,0); }
    }
    @media (prefers-reduced-motion: reduce) {
      .wa-fab { animation: none; }
      .wa-fab::before { animation: none; }
    }

    /* === Pay Online — primary CTA when a payment gateway is connected. */
    .pay-online-btn {
      width: 100%; padding: 14px; border-radius: var(--radius-sm);
      background: var(--accent); color: var(--theme-accent-fg, #fff);
      font: 700 0.95rem var(--font-body); border: 0; cursor: pointer;
      display: flex; align-items: center; justify-content: center; gap: 8px;
      transition: all 0.2s ease; min-height: 52px; letter-spacing: -0.01em;
      box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 25%, transparent);
    }
    .pay-online-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 35%, transparent); }
    .pay-online-btn:active:not(:disabled) { transform: scale(0.98); }
    .pay-online-btn:disabled { opacity: 0.65; cursor: not-allowed; transform: none; }

    /* Test-mode banner under the Pay Online button. Same amber as the
       admin form's mode toggle so the visual language stays consistent
       across surfaces. */
    .pay-online-note {
      margin-top: 8px; padding: 6px 10px;
      background: #fef3c7; color: #92400e;
      border-radius: var(--radius-sm);
      font: 600 0.72rem var(--font-body); text-align: center;
      letter-spacing: 0.02em;
    }
    .pay-online-err {
      margin-top: 10px; padding: 8px 12px;
      background: #fee2e2; color: #991b1b;
      border-radius: var(--radius-sm);
      font: 500 0.78rem var(--font-body); line-height: 1.5;
    }
    /* Minimum-order gate notice above the checkout CTAs. */
    .min-order-bar {
      margin-bottom: 12px; padding: 10px 13px;
      background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa;
      border-radius: var(--radius-sm);
      font: 500 0.82rem var(--font-body); line-height: 1.45; text-align: center;
    }
    .min-order-bar strong { font-weight: 700; }
    /* Store closed / paused (migration 161) — same bar, and the same theme
       --warn tokens as the strip at the top of the page, so a customer who
       scrolled past the strip and opened the cart meets one consistent
       message rather than what looks like a second, different problem. */
    .min-order-bar.closed-bar { background: var(--warn-dim); color: var(--warn); border-color: rgba(245,158,11,0.35); }
    /* Pre-order takes the brand accent: this one is a confirmation, not a
       blocker, and the CTA underneath it is live. */
    .min-order-bar.preorder-bar { background: var(--accent-dim); color: var(--accent-text); border-color: var(--accent); }

    /* === Checkout alert === the single place a checkout failure is reported.
       It lives in the sticky footer, NOT in the scrolling form, because the
       form scrolls out of sight the moment the cart holds a few items: the
       old inline banner was being written to a part of the panel the customer
       could not see, so tapping Place Order looked like nothing happened.
       Sits directly above the CTA — where the eye already is after a tap —
       and scrolls the offending field back into view when pressed. */
    .checkout-alert {
      width: 100%; display: flex; align-items: center; gap: 9px;
      margin-bottom: 12px; padding: 10px 12px;
      background: var(--red-dim); color: var(--red);
      border: 1px solid rgba(239,68,68,0.35); border-radius: var(--radius-sm);
      font: 600 0.82rem var(--font-body); line-height: 1.4; text-align: left;
      cursor: pointer; -webkit-appearance: none;
      animation: checkoutAlertIn 0.22s var(--ease);
    }
    .checkout-alert svg { flex: 0 0 auto; stroke: currentColor; fill: none; }
    .checkout-alert > svg { width: 17px; height: 17px; stroke-width: 2.2; }
    .checkout-alert-msg { flex: 1; min-width: 0; }
    /* The affordance that makes the bar look pressable rather than decorative. */
    .checkout-alert-go {
      flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px;
      padding: 4px 8px; border-radius: 999px;
      background: rgba(239,68,68,0.14);
      font: 700 0.68rem var(--font-body); letter-spacing: 0.06em; text-transform: uppercase;
    }
    .checkout-alert-go svg { stroke-width: 2.6; }
    /* Server / network failures have no field to jump to — the bar stays, but
       it stops pretending to be a button. */
    .checkout-alert.is-static { cursor: default; }
    @keyframes checkoutAlertIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
    @media (prefers-reduced-motion: reduce) { .checkout-alert { animation: none; } }
    /* A shut store's CTA must not keep WhatsApp green or the brand accent.
       A big green button reading "Closed · Opens Monday" still looks pressable,
       and a customer who taps it and gets nothing assumes the site is broken
       rather than that the shop is shut. Overrides the method colours, so it
       comes after them in the cascade. */
    .pay-online-btn.cta-shut, .wa-btn.cta-shut {
      background: var(--surface2) !important;
      color: var(--text-muted) !important;
      border: 1px solid var(--border) !important;
      box-shadow: none !important;
    }
    .cta-shut .pay-cta-ic { opacity: 0.5; }

    /* === "or" divider between the two payment CTAs ===
       Industry-standard pattern: two horizontal rules with a small
       lowercase label between. Stripe, PayPal, Shopify all use this
       when stacking payment methods. Centers the eye on the secondary
       option without making it feel like an afterthought. */
    .checkout-or {
      display: flex; align-items: center; gap: 12px;
      margin: 16px 4px 14px;
    }
    .checkout-or::before,
    .checkout-or::after {
      content: ''; flex: 1; height: 1px;
      background: var(--border);
    }
    .checkout-or span {
      font: 600 0.7rem var(--font-body);
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.12em;
      flex-shrink: 0;
    }

    /* Checkout method selector (migration 154). Reuses the .subst-opt stacked
       radio-card idiom; only the extras below are method-specific. */
    .pay-methods { margin-bottom: 12px; }
    .subst-opt.is-disabled { opacity: 0.55; cursor: not-allowed; }
    .pay-method-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
    .pay-method-title { font-weight: 600; }
    .pay-method-sub { font-size: 0.76rem; color: var(--text-muted); }
    .pay-cta-ic { display: inline-flex; align-items: center; }
    .pay-cta-ic svg { display: block; }
    /* COD advance split box (migration 155). */
    .cod-advance-box { margin: 10px 16px 0; padding: 11px 14px; background: var(--accent-dim); border: 1px solid var(--border-light); border-radius: var(--radius-sm); }
    .cod-advance-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 0.86rem; color: var(--text-secondary); }
    .cod-advance-row + .cod-advance-row { margin-top: 4px; }
    .cod-advance-row strong { color: var(--text); font-weight: 700; }

    /* === Compact payment chooser row (Swiggy / Uber / DoorDash pattern) ===
       Replaces the tall stack of method cards in the footer with one short
       line: an icon, a "Pay with · <method>" label, and a Change affordance.
       Tapping it opens the .pay-picker sheet. Keeping the footer to this one
       row (~52px) instead of ~175px of stacked cards is what frees the space
       the mobile keyboard used to steal from the checkout form. */
    .pay-chooser {
      width: 100%; display: flex; align-items: center; gap: 12px;
      padding: 10px 14px; margin-bottom: 12px; min-height: 52px;
      border: 1px solid var(--border); border-radius: var(--radius-sm);
      background: var(--surface2); color: var(--text); cursor: pointer;
      text-align: left; font-family: var(--font-body);
      transition: border-color 0.15s, background 0.15s, transform 0.12s ease;
    }
    .pay-chooser:hover { border-color: var(--text-muted); }
    .pay-chooser:active { transform: scale(0.99); }
    .pay-chooser:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .pay-chooser-ic { flex: 0 0 auto; color: var(--text-secondary); }
    .pay-chooser-ic svg { width: 20px; height: 20px; }
    .pay-chooser-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
    .pay-chooser-cap { font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
    .pay-chooser-val { font-size: 0.92rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .pay-chooser-chev { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; font-size: 0.78rem; font-weight: 600; color: var(--accent); }
    .pay-chooser-chev svg { display: block; }

    /* === Payment-method picker sheet === bottom sheet on mobile, centered on
       desktop. Mirrors the .booking-sheet skeleton so it inherits the same
       slide-up + safe-area behaviour. Sits above the cart drawer (z 50). */
    .pay-picker { position: fixed; inset: 0; z-index: 1300; display: flex; flex-direction: column; justify-content: flex-end; }
    .pay-picker-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); animation: fadeIn 0.2s ease; }
    .pay-picker-panel {
      position: relative; background: var(--surface);
      border-radius: 20px 20px 0 0; max-height: 80vh;
      display: flex; flex-direction: column; overflow: hidden;
      box-shadow: 0 -8px 40px rgba(0,0,0,0.3);
      animation: slideUp 0.3s var(--ease);
    }
    @media (min-width: 720px) {
      .pay-picker { justify-content: center; align-items: center; padding: 24px; }
      .pay-picker-panel { border-radius: 16px; width: 100%; max-width: 440px; animation: fadeIn 0.2s ease; }
    }
    .pay-picker-head { display: flex; align-items: center; gap: 12px; padding: 16px 18px 14px; border-bottom: 1px solid var(--border); }
    .pay-picker-head h2 { flex: 1; min-width: 0; margin: 0; font: 700 1.05rem var(--font-display); letter-spacing: -0.01em; }
    .pay-picker-x { flex: 0 0 auto; width: 36px; height: 36px; border: 1px solid var(--border); border-radius: 50%; background: color-mix(in srgb, var(--text) 7%, var(--surface)); color: var(--text-secondary); display: grid; place-items: center; cursor: pointer; transition: background 0.15s, color 0.15s; }
    .pay-picker-x:hover { color: var(--text); background: color-mix(in srgb, var(--text) 14%, var(--surface)); }
    .pay-picker-body { padding: 16px 18px calc(18px + env(safe-area-inset-bottom)); overflow-y: auto; -webkit-overflow-scrolling: touch; }
    /* The picker's cards are the primary tap target here — give them a touch
       more presence than the inline substitution radios. */
    .pay-picker-body .subst-opt { padding: 13px 14px; }

    .add-more-btn {
      width: 100%; padding: 10px; margin-top: 14px;
      border-radius: var(--radius-sm); background: transparent; color: var(--text-muted);
      font: 500 0.82rem var(--font-body); border: 1px solid var(--border-light);
      cursor: pointer; min-height: 40px;
    }
    .add-more-btn:hover { background: var(--surface2); color: var(--text-secondary); }

    /* === QR zoom modal === */
    .qr-modal { position: fixed; inset: 0; z-index: 1300; display: grid; place-items: center; padding: 20px; }
    .qr-modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.72); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
    .qr-modal-panel {
      position: relative; width: 100%; max-width: min(380px, calc(100vw - 32px));
      background: var(--surface); border-radius: 20px; padding: 24px 20px 20px;
      box-shadow: 0 30px 60px rgba(0,0,0,0.5); text-align: center;
      animation: qrModalIn 0.25s var(--ease);
    }
    @keyframes qrModalIn { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: scale(1); } }
    .qr-modal-close { position: absolute; top: 10px; right: 10px; width: 36px; height: 36px; border-radius: 50%; background: var(--surface2); border: 0; color: var(--text-secondary); cursor: pointer; display: grid; place-items: center; }
    .qr-modal-close:hover { background: var(--surface3); color: var(--text); }
    .qr-modal-close svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
    .qr-modal-title { font-size: 0.78rem; font-weight: 600; color: var(--text-secondary); letter-spacing: 0.06em; text-transform: uppercase; }
    .qr-modal-amount { font-family: var(--font-display); font-size: 2.2rem; font-weight: 700; color: var(--accent); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; margin-top: 2px; }
    .qr-modal-image { width: 100%; aspect-ratio: 1; max-width: 320px; margin: 18px auto 14px; padding: 12px; background: #fff; border-radius: 14px; box-shadow: inset 0 0 0 1px var(--border); }
    .qr-modal-image img { width: 100%; height: 100%; display: block; object-fit: contain; }
    .qr-modal-help { font-size: 0.82rem; color: var(--text-secondary); line-height: 1.5; margin-bottom: 4px; }
    .qr-modal-apps { font-size: 0.72rem; color: var(--text-muted); letter-spacing: 0.02em; }
    .qr-modal-done {
      margin-top: 16px; width: 100%; padding: 12px; border-radius: var(--radius-sm);
      background: var(--accent); color: var(--theme-accent-fg, #111); border: 0;
      font: 700 0.9rem var(--font-body); cursor: pointer; min-height: 48px; letter-spacing: -0.01em;
    }
    .qr-modal-done:hover { filter: brightness(1.08); }

    /* Install help sheet — reuses the qr-modal-panel for layout. The list
       is numbered (ol) so the steps feel like a procedure, not a wall of
       text. The inline icon mirrors what's on the customer's screen
       (Share glyph on iOS, three dots on Android, install/+ glyph on
       desktop) so the instruction is unambiguous regardless of platform. */
    .install-help { text-align: left; padding: 24px 22px 20px; }
    .install-help .qr-modal-title { text-align: center; margin-bottom: 6px; }
    .install-help-name {
      font-family: var(--font-display); font-size: 1.35rem; font-weight: 700;
      color: var(--text); text-align: center; letter-spacing: -0.01em; margin-bottom: 18px;
    }
    .install-help-platform {
      font: 600 0.7rem var(--font-body); color: var(--accent);
      text-transform: uppercase; letter-spacing: 0.06em;
      margin-bottom: 8px;
    }
    .install-help-steps {
      list-style: decimal inside; padding: 0; margin: 0 0 6px;
      display: flex; flex-direction: column; gap: 12px;
    }
    .install-help-steps li {
      font-size: 0.9rem; color: var(--text); line-height: 1.5;
      padding: 12px 14px; background: var(--surface2); border-radius: var(--radius-xs);
      border: 1px solid var(--border);
      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    }
    .install-help-steps li strong { color: var(--accent); font-weight: 700; }
    .install-step-icon {
      width: 22px; height: 22px; stroke: var(--accent); fill: none; stroke-width: 1.8;
      flex-shrink: 0;
    }
    .install-step-hint { color: var(--text-muted); font-size: 0.78rem; font-weight: 500; }
    .install-help-note {
      font-size: 0.9rem; line-height: 1.55; color: var(--text-secondary);
      padding: 12px 14px; background: var(--surface2); border-radius: var(--radius-xs);
      border: 1px solid var(--border); margin-bottom: 6px;
    }
    .install-help-note strong { color: var(--accent); font-weight: 700; }

    /* Multi-number Call sheet. Same qr-modal-panel layout as the
       install help so the visual language stays consistent. Each row
       is a tap target on its own — primary number first, then any
       extras the merchant added. Labels in small caps above the
       number make it scannable at a glance. */
    .call-sheet { text-align: left; padding: 24px 22px 20px; }
    .call-sheet .qr-modal-title { text-align: center; margin-bottom: 6px; }
    .call-sheet-name {
      font-family: var(--font-display); font-size: 1.3rem; font-weight: 700;
      color: var(--text); text-align: center; letter-spacing: -0.01em; margin-bottom: 18px;
    }
    .call-sheet-list {
      display: flex; flex-direction: column; gap: 8px; margin-bottom: 6px;
    }
    .call-sheet-row {
      display: flex; align-items: center; gap: 12px;
      padding: 14px 16px; background: var(--surface2); border-radius: var(--radius-xs);
      border: 1px solid var(--border); color: var(--text); text-decoration: none;
      transition: background 0.15s ease, border-color 0.15s ease;
    }
    .call-sheet-row:hover { background: var(--surface3); border-color: var(--accent); }
    .call-sheet-row:active { transform: scale(0.99); }
    .call-sheet-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
    .call-sheet-row-label {
      font: 600 0.68rem var(--font-body); color: var(--text-muted);
      text-transform: uppercase; letter-spacing: 0.06em;
    }
    .call-sheet-row-num {
      font: 700 1rem var(--font-body); color: var(--text);
      font-variant-numeric: tabular-nums; letter-spacing: -0.005em;
    }
    .call-sheet-row-icon {
      width: 22px; height: 22px; stroke: var(--accent); fill: none; stroke-width: 1.8;
      flex-shrink: 0;
    }

    /* === Variant modal === */
    .modal-overlay { position: fixed; inset: 0; z-index: 60; }
    .modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); animation: fadeIn 0.2s ease; }
    .modal-panel {
      position: absolute; bottom: var(--kb-gap, 0px); left: 0; right: 0;
      max-height: var(--modal-h); background: var(--surface);
      border-top: 1px solid var(--border-light); border-radius: 20px 20px 0 0;
      display: flex; flex-direction: column; overflow: hidden;
      animation: slideUp 0.32s var(--ease);
      /* Collapse driver. --pdp-p runs 0 → 1 as .modal-body scrolls its first
         --pdp-band worth of content; JS writes ONLY this property and every
         rule below reads it. See _onPdpScroll(). */
      --pdp-p: 0;
      --pdp-band: 44dvh;
      --pdp-bar: 56px;
      /* The top bar arrives on its own ramp, not on raw p: it stays out of the
         way while the photo is still big enough to be the subject (p < .25),
         then is fully home by the time the band is two-thirds gone (p = .7).
         The bar's slide and the body's top padding both read THIS, so they can
         never drift apart. */
      --pdp-bar-in: clamp(0, calc(var(--pdp-p) * 2.2 - 0.55), 1);
    }
    /* Variant modal on desktop is centered (bottom: 50% + translate(-50%, 50%)),
       not a bottom sheet — a fade-in feels right and avoids the same
       transform-override jump that bit .cart-panel. Keyframes compose the
       centering transform so the modal stays put during the animation. */
    @keyframes modalFadeInDesktop {
      from { transform: translate(-50%, 50%) scale(0.96); opacity: 0; }
      to   { transform: translate(-50%, 50%) scale(1);    opacity: 1; }
    }
    @media (min-width: 720px) {
      .modal-panel {
        position: absolute; bottom: 50%; left: 50%;
        transform: translate(-50%, 50%);
        max-width: 540px; max-height: var(--modal-h-desktop); border-radius: var(--radius);
        animation: modalFadeInDesktop 0.22s ease-out;
      }
    }
    /* Two-column desktop layout (Amazon / Shopify / Zomato pattern).
       Image fills a fixed-width left column; content fills the right and
       starts at modal-top, so variants + price are visible without scroll
       regardless of description length. Single-column stack stays below
       960px so smaller laptops aren't cramped. */
    @media (min-width: 960px) {
      .modal-panel {
        max-width: 860px;
        display: grid; grid-template-columns: 380px 1fr;
        /* Bound the single grid row to the viewport cap. Without this the
           row sizes to its tallest column's content — a long description
           grows .modal-side past 88dvh, and the panel's max-height just
           clips the overflow, hiding the footer (Add-to-Cart) off the
           bottom. minmax(0, …) lets short modals stay content-height while
           capping tall ones so .modal-side gets a bounded height and its
           .modal-body scrolls internally with the footer pinned. */
        grid-template-rows: minmax(0, var(--modal-h-desktop));
      }
      .modal-img, .modal-gallery { height: 100%; aspect-ratio: auto; min-height: min(460px, 80dvh); }
      .modal-side { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
      .modal-body { padding: 22px 24px 10px; }
      .modal-footer { padding: 16px 24px 20px; }
    }
    /* Detail-modal image uses `contain` GLOBALLY (not cover) — the
       customer is on a product detail page; they want to see the
       whole photo, not a tile-thumb crop. Industry consensus across
       Amazon, Flipkart, Blinkit, Zepto, Myntra, Nykaa: detail-page
       photos always letterbox to show the full image. Cropping is for
       the catalog tile, not for the product page.
       Variant photos and packaged-goods labels both demand full
       visibility, and lifestyle food shots tend to be near-square so
       contain shows them with minimal banding anyway. */
    .modal-img {
      width: 100%; aspect-ratio: var(--modal-aspect);
      background: var(--surface2) center center / contain no-repeat;
      flex-shrink: 0; position: relative;
    }
    .modal-close { position: absolute; top: 14px; right: 14px; width: 36px; height: 36px; border-radius: 50%; border: 0; cursor: pointer; background: rgba(0,0,0,0.6); color: #fff; display: grid; place-items: center; backdrop-filter: blur(8px); z-index: 3; }

    /* modal-side is a flex column at ALL sizes (not just desktop) so
       the footer's Add-to-Cart button stays anchored at the bottom
       of the modal even when the image is tall — without this, on
       portrait-aspect fashion modals the variant chips and CTA could
       get pushed below the 92dvh modal max-height and become
       unreachable. modal-body fills the remaining space and scrolls
       internally; modal-footer never moves. */
    .modal-side {
      display: flex; flex-direction: column;
      min-height: 0; flex: 1; overflow: hidden;
      position: relative;   /* anchors .modal-topbar over the body's top edge */
    }
    /* Single-column image-area cap — applies to EVERY width below the 960px
       two-column grid: the mobile bottom sheet AND the centred tablet /
       landscape-phone modal. The image is a capped top region (definite
       height, not aspect-ratio) so the variant picker + sticky footer
       (Add-to-Cart) are always reachable; the body scrolls between them.
       Trades off two failure modes:
       - too tall → variants + CTA push below the fold (customer doesn't
         realize the modal-body scrolls, reads the half-hidden last variant
         row as "broken"). A portrait 3/4 image is the worst case: a
         single-photo .modal-img would fall back to its aspect-ratio and the
         carousel previously took a rigid 460px min — either overflows a short
         panel (tablet portrait, landscape phone) and pushes the footer off.
       - too short → product photo feels cramped.
       44dvh is the balance: on a 360x740 phone ≈ 325px image, leaving ~325px
       for body+footer. Definite height also dodges the iOS Safari bug where an
       aspect-ratio flex item in a column collapses to ~0. `contain` still
       letterboxes so the whole photo shows.

       The band is now also the OPENING height, not the permanent one: it
       collapses to nothing as the body scrolls (see --pdp-p below), so the
       "too tall" failure mode above costs one gesture instead of the whole
       reading area.

       Given the room, the band shrinks at exactly the rate it would have
       scrolled away at — the photo's bottom edge stays glued to the content it
       started next to. Where the content is too short to pay for that, the
       collapse is spent faster so it still finishes (see _measurePdpCollapse).
       The cost either way: while collapsing, the content rises by the scroll
       PLUS the band it inherits, so it travels ~1.8x the finger until the
       photo is gone, then exactly 1:1 for the rest. That is inherent to the
       photo living OUTSIDE the scroller (which is what keeps this jitter-free)
       and it is what Android's CollapsingToolbarLayout does too. */
    @media (max-width: 959px) {
      .modal-img, .modal-gallery {
        height:     calc(var(--pdp-band) * (1 - var(--pdp-p)));
        max-height: calc(var(--pdp-band) * (1 - var(--pdp-p)));
        aspect-ratio: auto; flex: 0 0 auto; min-height: 0;
      }
      /* The photo shrinks with the band all the way; it only fades over the
         last stretch, where what's left is a smear of colour too short to read
         as a photograph. The chrome goes much earlier — dots on a 20px-tall
         band are noise. */
      .modal-gallery-track { opacity: clamp(0, calc((0.95 - var(--pdp-p)) / 0.4), 1); }
      .modal-gallery-dots, .modal-gallery-arrow { opacity: clamp(0, calc(1 - var(--pdp-p) * 3), 1); }
    }

    /* === Collapsed-state top bar ===
       Takes over from the photo once it has scrolled away, so the customer
       never loses the answer to "what am I configuring?" while they work
       through variants and add-ons. Absolutely positioned — it must NOT sit in
       the body's flow, or it would consume 56px at the top of a modal that
       hasn't been scrolled yet.

       It SLIDES in from behind the shrinking photo; it never cross-fades. An
       opaque bar dissolving over live content gives you a long muddy window
       where a half-cut chip row reads straight through the product name. Fully
       opaque and clipped by .modal-side's overflow, it always masks whatever
       is behind it, and it looks like it is being uncovered as the photo
       lifts away.

       Out of flow means it occludes, so .modal-body grows a matching top
       padding on the SAME ramp (below): the content is pushed down exactly as
       fast as the bar arrives, so the bar never swallows a line the customer
       was reading. The ramp is a pure function of --pdp-p, which is a pure
       function of scrollTop, so it can't feed back into the scroll position. */
    .modal-topbar {
      position: absolute; top: 0; left: 0; right: 0; z-index: 2;
      height: var(--pdp-bar);
      display: flex; align-items: center; gap: 10px;
      padding: 0 56px 0 20px;    /* right gap clears the floating close button */
      background: var(--surface);
      border-bottom: 1px solid var(--border);
      transform: translateY(calc(-100% * (1 - var(--pdp-bar-in))));
      /* Deliberately NOT pointer-events:none. The bar covers live content —
         scrolled past, but present — and letting taps fall through means a
         tap on the product name can toggle an add-on the customer cannot see
         and silently change what they pay. The bar has nothing tappable of
         its own, so absorbing the tap costs nothing. (While it is parked off
         the top edge, .modal-side's overflow clips it out of hit-testing
         entirely, so it can never block the photo.) */
    }
    .modal-topbar-thumb {
      flex: 0 0 auto; width: 34px; height: 34px; border-radius: 8px;
      background: var(--surface2) center center / cover no-repeat;
      border: 1px solid var(--border);
    }
    .modal-topbar-name {
      flex: 1; min-width: 0;
      font: 700 0.95rem var(--font-display); letter-spacing: -0.01em;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    /* Desktop keeps the image as its own grid column — nothing collapses, so
       there is nothing for the bar to stand in for. And with nothing
       collapsing, Close belongs back over the photo: anchored to the panel it
       would land on the content column's first row, on top of whatever label
       happens to start there. 380px is the image track from the grid above;
       50px backs off the button's own 36px plus its 14px gap. */
    @media (min-width: 960px) {
      .modal-topbar { display: none; }
      .modal-close { left: calc(380px - 50px); right: auto; }
    }
    /* No prefers-reduced-motion override here on purpose: the slide is not
       decoration, it IS how the bar is positioned. Every pixel of it tracks
       the finger — there is no self-running animation to suppress. */

    /* === Modal photo carousel ===
       Replaces the single .modal-img background-image when an item
       has 2+ photos. Native scroll-snap handles touch swipe; JS only
       tracks the active slide index for the dot indicators and
       handles variant-pick → slide auto-scroll. Single-photo items
       render exactly the same as today (no carousel chrome). */
    .modal-gallery {
      position: relative;
      width: 100%; aspect-ratio: var(--modal-aspect);
      flex-shrink: 0;
      background: var(--surface2);
    }
    .modal-gallery-track {
      width: 100%; height: 100%;
      display: flex;
      overflow-x: auto;
      scroll-snap-type: x mandatory;
      scroll-behavior: smooth;
      scrollbar-width: none;
      -webkit-overflow-scrolling: touch;
    }
    .modal-gallery-track::-webkit-scrollbar { display: none; }
    .modal-gallery-slide {
      flex: 0 0 100%; width: 100%; height: 100%;
      scroll-snap-align: start;
      background-color: var(--surface2);
      /* `contain` (not cover) so the entire photo is visible — same
         rationale as .modal-img above. Background-position center so
         letterbox bands are symmetric. cursor: zoom-in invites tap-to-
         enlarge; the slide handler opens the fullscreen lightbox. */
      background-size: contain;
      background-position: center center;
      background-repeat: no-repeat;
      cursor: zoom-in;
    }

    /* === Grocery / pharmacy: white-paper canvas ===
       Packaged-goods photos (Blinkit / Zepto / Instamart / 1mg)
       universally render on pure white so labels read clearly. Other
       verticals inherit var(--surface2) which adapts to the theme. */
    html[data-vertical="grocery"] .modal-img,
    html[data-vertical="grocery"] .modal-gallery,
    html[data-vertical="grocery"] .modal-gallery-slide,
    html[data-vertical="pharmacy"] .modal-img,
    html[data-vertical="pharmacy"] .modal-gallery,
    html[data-vertical="pharmacy"] .modal-gallery-slide {
      background-color: #ffffff;
    }

    /* === Tap-to-zoom photo lightbox ===
       The detail modal letterboxes photos to keep variants + CTA in
       view. Customers who want a really close look tap the photo to
       expand it to a fullscreen overlay. Amazon / Flipkart / Myntra
       pattern. Backdrop click + Escape both dismiss. */
    .photo-zoom {
      position: fixed; inset: 0; z-index: 80;
      background: rgba(0,0,0,0.94); backdrop-filter: blur(8px);
      display: grid; place-items: center; padding: 24px;
      cursor: zoom-out;
      animation: photoZoomIn 0.18s ease;
    }
    @keyframes photoZoomIn { from { opacity: 0; } to { opacity: 1; } }
    .photo-zoom-img {
      /* Size against the VIEWPORT, not the (auto-sized) grid track. With
         max-width:100% inside a place-items:center grid, the track sizes to
         the image's natural size first, so a large photo overflows the
         screen instead of fitting. Viewport units (minus the .photo-zoom
         padding) guarantee the whole image is always visible. */
      max-width: calc(100vw - 48px);
      max-height: calc(100vh - 48px);
      width: auto; height: auto;
      object-fit: contain;
      border-radius: 8px;
      cursor: default;
    }
    .photo-zoom-close {
      position: absolute; top: 16px; right: 16px;
      width: 44px; height: 44px; border-radius: 50%;
      background: rgba(255,255,255,0.12); color: #fff;
      border: 1px solid rgba(255,255,255,0.2);
      display: grid; place-items: center; cursor: pointer;
      font: 400 28px/1 system-ui, sans-serif;
    }
    .photo-zoom-close:hover { background: rgba(255,255,255,0.22); }
    /* Dot indicators — pinned bottom-left so they don't fight the
       close button (top-right). Hidden by default; only render when
       the gallery has 2+ photos via x-show in markup. */
    .modal-gallery-dots {
      position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
      display: flex; gap: 6px; z-index: 2;
      padding: 6px 10px;
      background: rgba(0,0,0,0.45);
      border-radius: 999px;
      backdrop-filter: blur(8px);
    }
    .modal-gallery-dot {
      width: 6px; height: 6px; border-radius: 50%;
      background: rgba(255,255,255,0.4);
      border: 0; padding: 0; cursor: pointer;
      transition: background 0.2s, width 0.2s;
    }
    .modal-gallery-dot.active { background: #fff; width: 18px; border-radius: 999px; }
    /* Prev/Next arrows — desktop only (mouse-hover devices). Touch
       users already have native scroll-snap swipe. Fade in on hover
       so the photo isn't obscured at rest. */
    .modal-gallery-arrow {
      position: absolute; top: 50%; transform: translateY(-50%);
      width: 40px; height: 40px; border-radius: 50%;
      background: rgba(0,0,0,0.55); color: #fff;
      border: 0; cursor: pointer;
      display: none; place-items: center;
      backdrop-filter: blur(8px);
      opacity: 0; transition: opacity 0.18s;
      z-index: 2;
    }
    .modal-gallery-arrow svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.2; }
    .modal-gallery-arrow.prev { left: 12px; }
    .modal-gallery-arrow.next { right: 12px; }
    @media (hover: hover) and (pointer: fine) {
      .modal-gallery-arrow { display: grid; }
      .modal-gallery:hover .modal-gallery-arrow { opacity: 0.9; }
      .modal-gallery-arrow:hover { opacity: 1 !important; background: rgba(0,0,0,0.75); }
    }
    /* Touch gets the arrows too. The original reasoning — "swipe is the
       affordance, arrows are for mice" — is sound about INPUT and wrong about
       DISCOVERY: a still photo does not tell you four more are behind it, and
       the dots read as decoration until something moves. Amazon, Flipkart and
       Myntra all carry arrows on mobile web for this reason.
       Two differences from the mouse case. They rest VISIBLE, because there is
       no hover to reveal them with. And they are 44px rather than 40 — the
       tap-target floor, and this is the one control sitting on top of the
       photo. The end-of-strip auto-hide still comes from :disabled below, so
       only the arrow you can actually use is on screen. */
    @media (hover: none) {
      .modal-gallery-arrow { display: grid; opacity: 0.9; width: 44px; height: 44px; }
      .modal-gallery-arrow svg { width: 20px; height: 20px; }
    }
    /* Re-states the collapsing-band fade from the phone block ~200 lines up.
       That rule is currently dead for arrows: `opacity: 0` in the base
       declaration above is written LATER in the sheet, and a media query adds
       no specificity, so it wins. It went unnoticed while the arrows were
       display:none on touch. Now that they render, the fade has to be restored
       here or they would hang over a 20px-tall smear of a photo. */
    @media (hover: none) and (max-width: 959px) {
      .modal-gallery-arrow { opacity: clamp(0, calc(0.9 - var(--pdp-p) * 3), 0.9); }
    }
    .modal-gallery-arrow:disabled { opacity: 0 !important; pointer-events: none; }

    .modal-close svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
    /* Bottom padding gives the last variant chip / description line
       breathing room above the sticky footer instead of butting right
       against it. Combined with the scroll-fade on .modal-footer
       below, customers see a clear visual gap between scrolling
       content and the action area. */
    /* Top padding grows with the collapsing top bar so the bar never covers
       live content — see .modal-topbar. */
    .modal-body { padding: calc(16px + var(--pdp-bar) * var(--pdp-bar-in)) 20px 24px; overflow-y: auto; flex: 1; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
    .modal-body h2 { font-family: var(--font-display); font-size: 1.3rem; font-weight: 700; margin-bottom: 6px; }
    .modal-cat-tag { display: inline-block; padding: 3px 9px; border-radius: 6px; background: var(--accent-dim); color: var(--accent); font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
    /* Description: gets clear breathing room above (separation from the
       variant picker) and below (from attribute chips). overflow-wrap
       handles long URLs / unbroken strings the merchant might paste —
       without it, long words break out of the panel horizontally.
       white-space: pre-wrap preserves merchant-typed line breaks. */
    .modal-desc {
      font-size: 0.88rem; color: var(--text-secondary); line-height: 1.65;
      margin: 14px 0 16px;
      white-space: pre-wrap;
      overflow-wrap: anywhere;
      word-break: break-word;
    }
    /* Top margin matters as much as the bottom one: when an item has no
       description, the chips sit directly under the variant picker and
       without it the two blocks touch. Sibling margins collapse, so this
       does not double up the gap when a description is present. */
    .modal-attrs { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0; }
    .attr-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: var(--radius-full); background: var(--surface2); border: 1px solid var(--border); font-size: 0.74rem; line-height: 1; }
    .attr-chip .attr-k { color: var(--text-secondary); font-weight: 500; }
    .attr-chip .attr-v { color: var(--text); font-weight: 650; }
    /* Affirmative trait (e.g. Organic, Eggless) — accent-tinted badge with a tick. */
    .attr-chip.attr-bool { background: color-mix(in srgb, var(--accent) 10%, var(--surface2)); border-color: color-mix(in srgb, var(--accent) 32%, var(--border)); }
    .attr-chip.attr-bool .attr-v { color: var(--accent); }
    .attr-chip .attr-check { width: 13px; height: 13px; color: var(--accent); flex-shrink: 0; }
    /* Specifications table (spec-table) — the chips' big sibling: 3+ attribute
       rows render as a 2-column zebra label/value table on the PDP (IKEA /
       Wayfair style). specRows() decides which rendering shows; never both. */
    .spec-table { margin: 16px 0 14px; }
    .spec-table-title { font: 700 0.95rem var(--font-display); letter-spacing: -0.01em; margin: 0 0 8px; }
    .spec-row { display: grid; grid-template-columns: minmax(110px, 38%) 1fr; gap: 12px; padding: 8px 10px; font-size: 0.85rem; line-height: 1.45; }
    .spec-row:nth-child(even) { background: var(--surface2); border-radius: 8px; }
    .spec-k { color: var(--text-secondary); font-weight: 500; overflow-wrap: anywhere; }
    .spec-v { color: var(--text); font-weight: 600; overflow-wrap: anywhere; }
    /* Showroom gives the table a touch more presence — furniture shoppers
       read dimensions before price. */
    html[data-layout="showroom"] .spec-row { font-size: 0.9rem; padding: 9px 12px; }
    /* === Bulk price-break ladder (migrations 213-214) ===
       The wholesale table on bulk products — the spec-table's sibling. The
       band containing the current stepper quantity carries .is-active and gets
       the accent treatment, moving live as the quantity changes. Tint always
       derives from the theme accent in CSS (never hardcoded). */
    .bulk-tiers { margin: 12px 0 2px; border: 1px solid var(--border-light); border-radius: var(--radius-sm); overflow: hidden; }
    .bulk-tiers-title { font: 700 0.8rem var(--font-display); letter-spacing: 0.02em; text-transform: uppercase; color: var(--text-muted); margin: 0; padding: 8px 12px 6px; }
    .bulk-tier-row { display: grid; grid-template-columns: minmax(64px, auto) 1fr auto; gap: 10px; align-items: baseline; padding: 7px 12px; font-size: 0.85rem; border-top: 1px solid var(--border-light); }
    .bulk-tier-q { color: var(--text-secondary); font-weight: 600; font-variant-numeric: tabular-nums; }
    .bulk-tier-p { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
    .bulk-tier-per { color: var(--text-muted); font-weight: 500; font-size: 0.78rem; }
    .bulk-tier-s { color: var(--text-muted); font-size: 0.78rem; font-weight: 600; }
    .bulk-tier-row.is-active { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); box-shadow: inset 3px 0 0 var(--accent); }
    .bulk-tier-row.is-active .bulk-tier-p, .bulk-tier-row.is-active .bulk-tier-q { color: var(--accent); }
    .bulk-tier-row.is-active .bulk-tier-s { color: var(--accent); }
    /* Min/case microcopy + the next-tier nudge under the buy bar. */
    .bulk-rule { font-size: 0.78rem; color: var(--text-muted); font-weight: 600; margin: 8px 0 0; }
    .bulk-nudge { font-size: 0.8rem; color: var(--accent); font-weight: 600; margin: 8px 0 0; }
    .modal-label { display: block; font-size: 0.7rem; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin: 14px 0 8px; }
    /* Variant picker — stacked name + price (Zomato / Swiggy pattern).
       Bigger tap target than the old single-line pill, and the price is
       immediately legible so customers can compare options at a glance. */
    .variants { display: flex; flex-wrap: wrap; gap: 8px; }
    .variant {
      display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
      padding: 9px 14px; min-width: 76px;
      border: 1.5px solid var(--border-light); border-radius: var(--radius-sm);
      background: var(--surface); color: var(--text-secondary);
      font: 600 0.78rem var(--font-body); cursor: pointer;
      transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
    }
    .variant:hover { border-color: var(--accent); color: var(--accent); }
    .variant.sel { border-color: var(--accent); background: var(--accent); color: var(--theme-accent-fg, #111); }
    .variant.oos { opacity: 0.4; cursor: not-allowed; text-decoration: line-through; }
    .variant-name  { font-size: 0.85rem; font-weight: 700; line-height: 1.2; }
    .variant-price { font-size: 0.72rem; font-weight: 600; opacity: 0.85; margin-top: 3px; font-variant-numeric: tabular-nums; white-space: nowrap; }
    .variant.sel .variant-price { opacity: 1; }

    /* Axis-grouped variant picker (Color + Size pattern). Each axis is
       a row with a label and the picked value shown beside it (Zara
       pattern: "Color: Red"), then the value chips below. Color axes
       render as photo swatches when the matching variant has an image;
       text-only when not. Size and any non-color axis stay as pill
       chips. OOS values for the currently-picked combo are dimmed +
       struck (Nike, H&M pattern). */
    .axis-row { margin: 14px 0; }
    .axis-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
    .axis-head .modal-label { margin: 0; }

    /* === Add-on / modifier customizer (migration 043) ===
       One block per group: a heading + required/optional hint, then a column
       of tappable option rows with a radio/checkbox affordance, optional veg
       dot, name, and a "+ ₹X" price delta. Required-but-unmet groups flag red. */
    .mod-group { margin: 16px 0; }
    .mod-hint { font: 600 0.72rem var(--font-body); letter-spacing: 0.02em; text-transform: uppercase; color: var(--text-muted); }
    .mod-hint--req { color: var(--accent); }
    .mod-group--error .mod-hint { color: var(--red, #dc2626); }
    .mod-group--error .modal-label::after { content: ' *'; color: var(--red, #dc2626); }
    .mod-options { display: flex; flex-direction: column; gap: 8px; }
    .mod-opt {
      display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
      min-height: 46px; padding: 8px 14px;
      border: 1.5px solid var(--border-light); border-radius: var(--radius-sm);
      background: var(--surface); color: var(--text);
      font: 600 0.9rem var(--font-body); cursor: pointer;
      transition: border-color 0.18s ease, background 0.18s ease;
    }
    .mod-opt:hover { border-color: var(--accent); }
    .mod-opt.sel { border-color: var(--accent); background: var(--accent-dim); }
    .mod-opt-name { flex: 1; min-width: 0; }
    .mod-opt-price { flex: 0 0 auto; font-weight: 700; color: var(--accent-text); font-variant-numeric: tabular-nums; }
    /* Radio / checkbox affordance — empty until .sel, then accent-filled. */
    .mod-opt-check { flex: 0 0 auto; width: 18px; height: 18px; border: 2px solid var(--border-strong, var(--border)); position: relative; transition: border-color 0.18s ease, background 0.18s ease; }
    .mod-opt-check.radio { border-radius: 50%; }
    .mod-opt-check.check { border-radius: 5px; }
    .mod-opt.sel .mod-opt-check { border-color: var(--accent); background: var(--accent); }
    .mod-opt.sel .mod-opt-check.radio::after { content: ''; position: absolute; inset: 3px; border-radius: 50%; background: var(--theme-accent-fg, #fff); }
    .mod-opt.sel .mod-opt-check.check::after { content: ''; position: absolute; left: 5px; top: 1px; width: 4px; height: 9px; border: solid var(--theme-accent-fg, #fff); border-width: 0 2px 2px 0; transform: rotate(45deg); }
    /* Personalisation fields (migration 102) — e.g. "Message on cake". */
    .cf-input-wrap { position: relative; }
    .cf-input {
      width: 100%; min-height: 46px; padding: 11px 14px;
      border: 1.5px solid var(--border-light); border-radius: var(--radius-sm);
      background: var(--surface); color: var(--text);
      font: 500 0.95rem var(--font-body); -webkit-appearance: none; appearance: none;
      transition: border-color 0.18s ease, box-shadow 0.18s ease;
    }
    .cf-input::placeholder { color: var(--muted, #9aa0a6); }
    .cf-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
    .cf-textarea { min-height: 80px; resize: vertical; line-height: 1.45; }
    .cf-input-wrap .cf-input { padding-right: 56px; }
    .cf-count {
      position: absolute; right: 12px; bottom: 9px; pointer-events: none;
      font: 600 0.72rem var(--font-body); color: var(--muted, #9aa0a6);
      font-variant-numeric: tabular-nums;
    }
    /* Personalisation section — a single, clearly-separated group so it never
       collides with the variant / add-on blocks above or the description below.
       Theme-aware: hairline + muted label from theme vars, no heavy chrome. */
    .cf-section {
      margin: 22px 0;
      padding-top: 18px;
      border-top: 1px solid var(--border-light);
      display: flex; flex-direction: column; gap: 18px;
    }
    .cf-section-title {
      display: block; font-size: 0.7rem; color: var(--text-muted);
      font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;
    }
    .cf-field { display: block; }
    .cf-field .axis-head { margin-bottom: 8px; }
    .cf-field--error .modal-label::after { content: ' *'; color: var(--red, #dc2626); }
    .cf-field--error .mod-hint { color: var(--red, #dc2626); }
    .cf-field--error .cf-input { border-color: var(--red, #dc2626); }
    .cf-bool { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font: 500 0.92rem var(--font-body); }
    .cf-bool input { width: 18px; height: 18px; accent-color: var(--accent); }
    .cf-help { margin: 6px 0 0; font-size: 0.8rem; color: var(--muted, #9aa0a6); line-height: 1.4; }
    .sub--field { font-style: italic; }
    .axis-picked { font-size: 0.82rem; color: var(--text); font-weight: 600; }
    .axis-values { display: flex; flex-wrap: wrap; gap: 8px; }
    .axis-val {
      display: inline-flex; align-items: center; justify-content: center;
      min-width: 44px; height: 40px; padding: 0 14px;
      border: 1.5px solid var(--border-light); border-radius: var(--radius-sm);
      background: var(--surface); color: var(--text);
      font: 600 0.85rem var(--font-body); cursor: pointer;
      transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
    }
    .axis-val:hover { border-color: var(--accent); color: var(--accent); }
    /* Selected text chip — foreground color is computed at theme-load
       time by _syncThemeColor() based on WCAG contrast vs the accent.
       Yellow / lime accent gets #111 text; navy / maroon accent gets
       #fff text. Variable falls back to dark if the JS pick fails for
       some reason (parse error on a weird hsl() accent). The text-
       shadow is intentionally subtle and the SAME color as the text
       — gives a half-pixel weight bump for readability on mid-tone
       accents without introducing the dual-color smear that the old
       white-only shadow caused on dark accents. */
    .axis-val.sel {
      border-color: var(--accent);
      background: var(--accent);
      color: var(--theme-accent-fg, #111);
      font-weight: 700;
    }
    .axis-val.sel:hover { color: var(--theme-accent-fg, #111); }
    .axis-val.oos { opacity: 0.35; cursor: not-allowed; text-decoration: line-through; }
    .axis-val.oos:hover { border-color: var(--border-light); color: var(--text); }
    /* is-photo flips a color swatch to a circular thumb when the
       variant has an uploaded image. Text-only color values (no
       variant photo yet) stay as pill chips — same shape as size
       chips — so they still look intentional.
       Defensive halo on the border so the swatch is distinguishable
       from the modal-body surface AND from the photo content inside —
       a thin dark hairline gives separation on light photos, the
       border itself provides separation on dark photos. */
    .axis-val.is-photo {
      padding: 0; width: 48px; height: 48px; min-width: 0;
      border-radius: 50%; overflow: hidden;
      border: 1.5px solid var(--border-light);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
      position: relative;
    }
    .axis-val.is-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
    /* Selected state: triple-ring sandwich so the selection is
       visible against ANY photo content, regardless of how close
       the accent color happens to be to colors inside the photo.
       Inner 1.5px dark hairline → 3px accent → outer 1.5px light
       hairline. At any photo color at least one of the three rings
       has high contrast. Same approach as .bestseller / .oos-badge
       overlay badges. */
    .axis-val.is-photo.sel {
      background: var(--surface); color: var(--text);
      border-color: rgba(0, 0, 0, 0.65);
      box-shadow:
        0 0 0 3px var(--accent),
        0 0 0 4.5px rgba(255, 255, 255, 0.45);
      outline: none;
    }

    /* OOS on a photo swatch: opacity dimming + line-through aren't
       enough by themselves on a busy photo (you can't see a 0.35
       strikethrough over a colorful product shot). Add an explicit
       diagonal "/" line in a high-contrast stroke pattern that sits
       ABOVE the photo regardless of underlying colors. Same dual-
       layer (dark line + thin white outline) idea used for badges. */
    .axis-val.is-photo.oos {
      opacity: 0.55;
    }
    .axis-val.is-photo.oos::after {
      content: '';
      position: absolute; inset: 0;
      background:
        linear-gradient(135deg,
          transparent 0%, transparent 47%,
          rgba(255, 255, 255, 0.9) 47%, rgba(255, 255, 255, 0.9) 53%,
          transparent 53%, transparent 100%),
        linear-gradient(135deg,
          transparent 0%, transparent 48%,
          rgba(0, 0, 0, 0.7) 48%, rgba(0, 0, 0, 0.7) 52%,
          transparent 52%, transparent 100%);
      background-blend-mode: normal;
      pointer-events: none;
    }
    /* Hover/focus reveals the variant name as a floating label above
       the swatch. Helps customers identify colors when the photo
       alone is ambiguous (two similar reds). Same chip styling the
       text-only axis values use for visual consistency. The label
       reads from the button's title attribute (set in the markup).
       Mobile :focus-within fires on tap so it works without hover. */
    .axis-val.is-photo:hover::before,
    .axis-val.is-photo:focus-visible::before {
      content: attr(aria-label);
      position: absolute;
      bottom: calc(100% + 8px);
      left: 50%; transform: translateX(-50%);
      padding: 4px 9px;
      background: rgba(15, 23, 42, 0.92); color: #fff;
      font: 600 0.7rem var(--font-body);
      letter-spacing: 0;
      border-radius: 4px;
      white-space: nowrap;
      pointer-events: none;
      z-index: 4;
      box-shadow: 0 4px 12px rgba(0,0,0,0.25);
    }
    /* Sticky bottom action area. A pseudo-element fades a 20px
       gradient ABOVE the footer so customers see content scrolling
       BENEATH it — telegraphs "more above" when the modal-body has
       overflowed. The gradient uses --surface-rgb-fallback indirectly
       via a same-surface gradient so it works on every theme without
       theme-token math. position:relative anchors the ::before. */
    .modal-footer {
      padding: 14px 20px 18px; border-top: 1px solid var(--border);
      background: var(--surface); flex-shrink: 0; position: relative;
    }
    .modal-footer::before {
      content: ''; position: absolute; left: 0; right: 0;
      top: -20px; height: 20px; pointer-events: none;
      background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, var(--surface) 100%);
    }
    .modal-price-row { display: flex; align-items: baseline; gap: 14px; margin-bottom: 12px; }
    .modal-price { font-family: var(--font-display); font-size: 1.6rem; font-weight: 700; color: var(--accent-text); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
    .modal-qty { margin-left: auto; display: inline-flex; align-items: center; border: 1.5px solid var(--border-light); border-radius: var(--radius-xs); overflow: hidden; }
    .modal-qty button { width: 36px; height: 36px; border: 0; background: var(--surface); color: var(--text); font-size: 1.1rem; cursor: pointer; transition: background 0.15s; }
    .modal-qty button:hover { background: var(--accent-dim); color: var(--accent); }
    .modal-qty input { width: 38px; text-align: center; border: 0; background: var(--surface); color: var(--text); font: 700 0.9rem var(--font-body); height: 36px; }
    .modal-add { width: 100%; padding: 14px; border: 0; border-radius: var(--radius-sm); background: var(--accent); color: var(--theme-accent-fg, #111); font: 700 0.92rem var(--font-body); cursor: pointer; transition: transform 0.15s ease, filter 0.2s ease; }
    .modal-add:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.08); }
    .modal-add:disabled { opacity: 0.4; cursor: not-allowed; }

    /* === Toast === */
    .toast { position: fixed; left: 50%; bottom: 92px; transform: translateX(-50%); z-index: 70; padding: 10px 18px; border-radius: var(--radius-full); background: var(--surface2); border: 1px solid var(--border-light); color: var(--text); font-size: 0.82rem; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; box-shadow: 0 8px 28px rgba(0,0,0,0.4); animation: toastIn 0.3s var(--ease); }
    .toast .check { width: 16px; height: 16px; border-radius: 50%; background: var(--green); display: grid; place-items: center; color: #fff; font-size: 0.7rem; font-weight: 700; }
    @keyframes toastIn { from { opacity: 0; transform: translate(-50%, 16px); } to { opacity: 1; transform: translate(-50%, 0); } }

    /* === Footer === */
    footer { max-width: 480px; margin: 32px auto 0; padding: 20px 16px 100px; border-top: 1px solid var(--border); text-align: center; color: var(--text-muted); font-size: 0.78rem; }
    footer .f-name { font-family: var(--font-display); font-size: 1rem; font-weight: 700; color: var(--text); margin-bottom: 4px; }
    footer .f-addr { white-space: pre-line; margin-bottom: 12px; line-height: 1.6; }
    footer .f-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-bottom: 14px; }
    footer .f-links a { color: var(--text-secondary); border-bottom: 1px dotted var(--text-muted); }
    footer .f-links a:hover { color: var(--accent); border-color: var(--accent); }
    /* "Follow us" icons (migration 092) — the simple-footer twin of .sf-social.
       Same monochrome set, centred to match this footer's stacked column
       instead of the storefront footer's left-aligned brand column. */
    footer .f-social { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 14px; }
    footer .f-social a {
      display: inline-grid; place-items: center; box-sizing: border-box; flex: 0 0 auto;
      width: 34px; height: 34px; padding: 0; line-height: 0;
      border: 1px solid var(--border); border-radius: 9px; color: var(--text-secondary);
      transition: color .15s, border-color .15s, transform .15s;
    }
    footer .f-social a:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-2px); }
    footer .f-social svg { width: 17px; height: 17px; display: block; }
    footer .compliance { display: grid; gap: 4px; margin: 14px auto; max-width: 280px; font-size: 0.7rem; text-align: left; }
    footer .compliance .row { display: flex; justify-content: space-between; gap: 8px; padding: 3px 0; border-bottom: 1px dashed var(--border); }
    footer .compliance .row:last-child { border-bottom: 0; }
    footer .compliance .row .l { color: var(--text-muted); }
    footer .compliance .row .v { color: var(--text-secondary); font-weight: 500; }
    /* Powered-by attribution — opens in a new tab so we don't break the
       customer's flow, carries UTM params keyed to the tenant slug so we
       can see which merchant stores drive ZSite signups, and includes a
       small brand mark + soft CTA so first-time viewers know it's a
       service they can use themselves. */
    /* "Built with ZSite" — same standout theme-aware pill the website themes
       use (.sf-built): an accent-gradient lightning disc + accent wordmark +
       a soft accent glow on hover. Uses theme vars so it adapts per store. */
    footer .powered { margin-top: 16px; }
    footer .powered a {
      display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px 6px 7px;
      border-radius: 999px; text-decoration: none; color: var(--text-secondary);
      background: var(--surface2); border: 1px solid var(--border);
      font: 600 0.8rem var(--font-body);
      transition: color .18s, border-color .18s, box-shadow .18s, transform .18s;
    }
    footer .powered a:hover { color: var(--text); border-color: var(--accent); box-shadow: 0 6px 20px var(--accent-shadow); transform: translateY(-1px); }
    footer .powered .mark {
      width: 24px; height: 24px; border-radius: 50%; flex: 0 0 auto; display: inline-grid; place-items: center;
      color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent-light));
      box-shadow: 0 2px 8px var(--accent-shadow);
    }
    footer .powered .mark svg { width: 13px; height: 13px; stroke: #fff; stroke-width: 2.4; fill: none; }
    footer .powered .brand { color: var(--accent); font-weight: 800; letter-spacing: -0.01em; }
    footer .powered a:hover .mark { transform: rotate(-8deg) scale(1.05); }
    /* Co-branded reseller credit (migration 022). Inline next to the
       "Built with ZSite" link — same baseline, quieter weight so the
       ZSite mark stays the primary attribution. Wraps onto the next
       line on narrow screens via the flexbox parent. */
    footer .powered .managed-by {
      display: inline-flex; align-items: center; gap: 6px;
      margin-left: 8px; color: var(--text-muted); font: 500 0.8rem var(--font-body);
    }
    footer .powered .managed-by .sep { opacity: 0.5; }
    footer .powered .managed-by .reseller { color: var(--text-secondary); font-weight: 600; }

    /* Legal/policy page links. Surfaces above "Built with ZSite" so
       customers (and PG scrapers) can find Terms, Privacy etc easily.
       Inline wrap so the layout never goes off the side on mobile. */
    footer .policy-links {
      display: flex; flex-wrap: wrap; gap: 8px 16px;
      margin-top: 16px; padding-top: 14px;
      border-top: 1px solid var(--border);
      font-size: 0.78rem;
    }
    footer .policy-links a {
      color: var(--text-muted); text-decoration: none;
      font-weight: 500;
    }
    footer .policy-links a:hover { color: var(--accent); text-decoration: underline; }

    /* Phones: a compact action and a slightly smaller price keep the price and
       the button on ONE row for short and mid-range amounts. Deliberately mild
       — chasing the long lakh-rupee prices onto a single row needs ~12px type,
       which is a worse outcome than a second row. 36px min-height is kept: the
       button stays a real tap target. */
    @media (max-width: 719px) {
      /* The price STACKS on phones — amount on top, struck MRP beneath it —
         instead of sitting side by side. This is the single biggest reason the
         Add button used to get pushed onto its own row: "₹42 ₹70" laid out
         horizontally is a ~170px block on a card whose footer is ~132px wide,
         so nothing could sit beside it. Stacked, the same block is ~90px and
         the button fits. Measured across 2000 card renders with an MRP
         present: 11% of cards kept price and button on one row before, 57%
         after. It is also what every Indian q-commerce app does on a
         two-column tile (Blinkit, Zepto, Instamart) — for exactly this
         reason. */
      .card-price { font-size: 0.95rem; flex-direction: column; align-items: flex-start; gap: 0; row-gap: 1px; }
      .add-btn { font-size: 0.72rem; }
      /* The stepper is the widest control a card can show (~101px against
         Add's ~76px), so it is what actually decides whether the footer fits.
         Trimmed to ~75px here, which lifts the in-cart state from 34% of
         cards keeping one row to 78% — level with the Add state. Height stays
         34px: the row is the tap target, and it is not what costs the width. */
      .card-footer { gap: 8px 8px; }
      .qty-btn { width: 22px; height: 34px; font-size: 0.95rem; }
      .qty-val { min-width: 16px; padding: 0 1px; font-size: 0.78rem; }

      /* ONE control footprint, and a card wide enough to hold it beside the
         price. Both halves are needed, and neither works alone.
           - The Add button and the qty stepper were 54px and 90px. A card
             therefore reflowed the moment you added an item, and two products
             at the SAME price rendered differently depending on whether one
             was in the cart. Pinning Add to 64px against the stepper's 62px
             makes the two states interchangeable.
           - A phone rail card was 156px, and 32px of that went to body
             padding, leaving a 122px footer. ₹5,000 + Add came to 120px, so
             it fitted by 2px and ₹14,000 did not — which is why the same
             screen showed three different layouts. 166px with 12px padding
             gives a 142px footer and clears the whole realistic range.
         Measured on this store's own prices (₹5,000-₹22,000) at 390px:
         50% of Add cards and 0% of stepper cards sat inline before; 100% of
         both do now. Lakh-rupee prices still wrap, and still wrap to a
         full-width control, which is the right answer at that length. */
      .sf-rail-card { flex-basis: 174px; }
      .add-btn { min-width: 64px; padding: 8px 6px; }
    }

    @media (min-width: 720px) {
      .main, .hero, .nav-inner, .cart-strip-inner, footer { max-width: 560px; }
      .hero h1 { font-size: 2.4rem; }
      .card-img-wrap { width: 168px; }
      .card-name { font-size: 1.05rem; }
      .card-desc { font-size: 0.8rem; }
      .card-price { font-size: 1.15rem; }
    }

    /* ── Desktop polish (≥1024px) for the 4 layouts ──────────────────
       Placed at end-of-<style> on purpose: same-specificity rules
       earlier in the file (base styles + the 720px media block above)
       MUST NOT override these at desktop widths. CSS cascade with
       equal specificity goes by source order — these have to come
       last to win. Previously this block sat near the top of the
       stylesheet and silently lost every conflict (max-width 960
       beat by 560, font-size 2rem by 2.4rem, etc.) which is why
       the desktop hero looked broken. */
    @media (min-width: 1024px) {
      /* List default — Zomato pattern: 2-col cards in a 1200px column.
         html:not([data-layout]) catches legacy storefronts whose JS
         hasn't set data-layout yet on first paint (the attribute lands
         after the catalog response). Once set, html[data-layout="list"]
         takes over — same rule, just stricter specificity. */
      html[data-layout="list"] .main,
      html:not([data-layout]) .main { max-width: 1200px; }
      html[data-layout="list"] .menu-list,
      html:not([data-layout]) .menu-list {
        display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-4);
      }
      html[data-layout="list"] .menu-card,
      html:not([data-layout]) .menu-card { margin-bottom: 0; }

      /* Gallery — Myntra pattern: 1280px container, hover-reveal Add. */
      html[data-layout="gallery"] .main { max-width: 1280px; }
      html[data-layout="gallery"] .menu-card { position: relative; }
      html[data-layout="gallery"] .menu-card .card-footer .add-btn {
        position: absolute; left: 50%; bottom: 12px;
        transform: translate(-50%, 8px);
        opacity: 0; transition: opacity 0.2s, transform 0.2s;
        z-index: 3; box-shadow: 0 6px 18px rgba(0,0,0,0.35);
      }
      html[data-layout="gallery"] .menu-card:hover .card-footer .add-btn {
        opacity: 1; transform: translate(-50%, 0);
      }

      /* Grid — Blinkit pattern: 1320px wide, tighter inter-tile gap. */
      html[data-layout="grid"] .main { max-width: 1320px; }
      html[data-layout="grid"] { --layout-gap: 6px; }

      /* Magazine — boutique editorial: 720px centered column, generous
         vertical rhythm between cards. */
      html[data-layout="magazine"] .main { max-width: 720px; }
      html[data-layout="magazine"] .menu-card { margin-bottom: var(--space-8); }

      /* Shared: card hover deepens on desktop. */
      .menu-card:hover {
        transform: translateY(-4px);
        box-shadow: 0 14px 40px rgba(0,0,0,0.18);
      }

      /* Hero — horizontal-split layout (Swiggy / Zomato restaurant
         page pattern, scaled for SMB). Logo left, brand block center,
         actions right. CSS grid reflows the existing 6-child markup
         (logo, h1, divider, tagline, meta, actions) without touching
         the DOM. */
      .hero { max-width: 960px; padding: 36px 24px 22px; text-align: left; }
      .hero-content {
        display: grid;
        grid-template-columns: 100px minmax(0, 1fr) auto;
        /* Explicit row count so `grid-row: 1 / -1` on logo + actions
           resolves to the actual last grid line. Without it, -1 = 1
           and the spanning items collapse onto the first brand row. */
        grid-template-rows: auto auto auto;
        align-items: center;
        column-gap: 28px;
        row-gap: 4px;
      }
      .logo-wrap {
        grid-column: 1; grid-row: 1 / -1;
        margin: 0; align-self: center;
        /* Don't resize the wrapper — .logo-inner is hardcoded 92px
           and the .logo-ring mask is calibrated at 46/47px for a
           100px wrapper. */
      }
      .hero h1 { grid-column: 2; font-size: 2rem; margin: 0 0 2px; text-align: left; line-height: 1.15; }
      /* Centered horizontal separator only reads in a centered
         vertical stack — hide in the horizontal split. */
      .hero .divider { display: none; }
      .hero .tagline { grid-column: 2; margin: 0 0 6px; text-align: left; letter-spacing: 1.5px; }
      .hero-meta {
        grid-column: 2;
        justify-content: flex-start;
        margin: 0; gap: 12px;
      }
      .hero-actions {
        grid-column: 3; grid-row: 1 / -1;
        align-self: center; justify-content: flex-end;
        max-width: 300px;
      }
      /* The 'bold' open/closed sign is a FOURTH brand-column row, and this
         grid places every child by hand for a reason: an auto-placed child
         would spawn an implicit row, which moves the line `-1` resolves to
         and silently re-spans the logo and the actions above.
         :has() scopes the extra row to the stores that actually render a
         sign, so every other store's hero grid is untouched. */
      .hero-content:has(> .hero-sign) { grid-template-rows: auto auto auto auto; }
      .hero-sign { grid-column: 2; justify-self: start; }
      /* Merchant hid the hero-actions (store.hide_hero_actions): the 3-col
         split would strand the logo + brand on the left with an empty right
         gutter where the actions were. Drop back to the centred vertical
         stack (logo on top, brand centred below) so the hero reads balanced. */
      .hero.hero--no-actions { max-width: 640px; text-align: center; }
      .hero.hero--no-actions .hero-content { display: block; }
      .hero.hero--no-actions .logo-wrap { margin: 0 auto 14px; }
      .hero.hero--no-actions h1 { text-align: center; }
      .hero.hero--no-actions .divider { display: block; }
      .hero.hero--no-actions .tagline { text-align: center; }
      .hero.hero--no-actions .hero-meta { justify-content: center; }

      /* Cart strip widens slightly; cat-tabs releases its width
         constraint so it can stretch across the new wider .main. */
      .cart-strip-inner { max-width: 720px; }
      .cat-tabs { max-width: 100%; }

      /* Sticky-nav: 3-col grid so the search box stays centered at
         its original width regardless of whether the brand is
         visible. Matches the Amazon / Flipkart / Myntra / Swiggy
         desktop pattern: brand left, search center (fixed), balance
         right. Brand reveals only after the hero scrolls out of
         view (via .is-compact). */
      .nav-inner {
        max-width: 1200px; padding: 10px 24px;
        display: grid;
        /* 1fr | 560px center | 1fr — search column never resizes. */
        grid-template-columns: 1fr minmax(0, 560px) 1fr;
        column-gap: 20px;
        align-items: center;
      }
      /* When the brand is hidden (page top, before scroll) the bar
         collapses to a tighter height since there's no 72px element
         pinning it. Keep the padding generous when the brand IS
         visible so the logo has breathing room. */
      .sticky-nav.is-compact .nav-inner { padding: 12px 24px; }
      .nav-controls {
        grid-column: 2;
        min-width: 0;
      }
      /* Brand sits in col 1, anchored to the left edge of the nav.
         Hidden by default; reveals via .sticky-nav.is-compact. We
         animate ONLY opacity + transform — not max-width — so the
         grid layout never shifts during the reveal (search column
         is locked to col 2 regardless). */
      .nav-brand {
        grid-column: 1; justify-self: start; align-self: center;
        display: flex; align-items: center; gap: 14px;
        text-decoration: none;
        color: var(--text);                  /* default text color */
        cursor: pointer;
        opacity: 0; transform: translateX(-14px);
        pointer-events: none;
        transition: opacity 0.3s ease, transform 0.32s ease;
      }
      /* No color change on hover — brand stays in the default text
         tone all the time, per "title colors default" requirement.
         Only the mark gets a subtle lift to confirm clickability. */
      .nav-brand:hover .nav-brand-mark {
        transform: scale(1.04);
        box-shadow: 0 4px 14px var(--accent-shadow, rgba(0,0,0,0.22));
      }
      .sticky-nav.is-compact .nav-brand {
        opacity: 1; transform: translateX(0);
        pointer-events: auto;
      }
      /* Brand mark — SAME treatment as the hero logo (.logo-inner)
         but at sticky-nav size. Round, surface-colored background
         with a subtle border, image cover-cropped to fill the circle.
         Bumped to 72px so it has hero-like presence without bloating
         the sticky bar (hero is 92px; this is close without making
         the bar 110px+ tall). The store-name label next to it is
         hidden because the logo alone carries enough identity once
         the customer has seen the hero — duplicating the name in
         the sticky bar is redundant. */
      .nav-brand-mark {
        width: 72px; height: 72px;
        border-radius: 50%;
        background: var(--surface);
        border: 1px solid var(--border);
        display: flex; align-items: center; justify-content: center;
        overflow: hidden;
        font: 700 1.7rem var(--font-display);
        color: var(--accent);
        flex-shrink: 0;
        box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
        transition: transform 0.18s ease, box-shadow 0.18s ease;
      }
      .nav-brand-mark img {
        width: 100%; height: 100%;
        object-fit: cover;
        display: block;
      }
      .nav-brand:hover .nav-brand-mark {
        transform: scale(1.04);
        box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12);
      }
      /* Title hidden in the sticky bar — logo alone carries identity. */
      .nav-brand-name { display: none; }

      /* Sectioned desktop view — sidebar + sectioned content. Only
         engaged when storefront sets .is-sectioned (3+ categories,
         no active filter, ≥1024px). Otherwise the wrapper is a
         transparent block container that passes layout through to
         the existing .main rules. */
      /* NOTE: the banner strip deliberately does NOT follow this grid.
         It lives above .sticky-nav, in the brand zone — its neighbours are
         the centred .hero and the centred search bar, and it stays centred
         with them. Matching the sidebar offset here was tried and looked
         broken: the banner's right edge shot ~270px past the hero's while
         its left edge sat just short of the logo, so the page read as two
         competing axes. The products are two zones down, behind a
         full-width sticky bar; nobody eye-lines a banner against them. */
      .layout-with-sidebar.is-sectioned {
        display: grid;
        grid-template-columns: 220px minmax(0, 1fr);
        gap: 32px;
        max-width: 1500px;
        margin: 0 auto;
        padding: 0 20px;
        align-items: start;
      }
      .layout-with-sidebar.is-sectioned .main {
        /* Override the per-layout `margin: 0 auto` so .main left-aligns
           inside its grid column instead of centering away from the
           sidebar. Each layout's own max-width still caps the column. */
        margin: 0;
        max-width: none;
        padding-left: 0;
        padding-right: 0;
      }

      .cat-sidebar {
        position: sticky;
        top: 80px;                          /* clear of sticky-nav */
        max-height: calc(100vh - 100px);
        overflow-y: auto;
        padding: 4px 0;
      }
      .cat-sidebar-head {
        font-family: var(--font-display);
        font-size: 0.7rem; color: var(--text-muted);
        font-weight: 700; text-transform: uppercase;
        letter-spacing: 1.6px;
        padding: 0 12px 12px;
        border-bottom: 1px solid var(--border);
        margin-bottom: 6px;
      }
      .cat-sidebar-list { list-style: none; padding: 0; margin: 0; }
      .cat-sidebar-list li { margin: 0; padding: 0; }
      .cat-sidebar-list a {
        display: flex; align-items: center; justify-content: space-between;
        gap: 12px; padding: 10px 12px; border-radius: var(--radius-xs);
        color: var(--text-secondary); text-decoration: none;
        font-size: 0.88rem; font-weight: 500; line-height: 1.3;
        border-left: 2px solid transparent;
        transition: color 0.15s, background 0.15s, border-color 0.15s;
      }
      .cat-sidebar-list a:hover {
        color: var(--text);
        background: var(--surface2);
      }
      /* is-active now lives on the <a> (the <li> wraps a parent link +
         a nested sublist, so the class can't sit on the li). */
      .cat-sidebar-list a.is-active {
        color: var(--accent);
        background: var(--accent-dim);
        border-left-color: var(--accent);
        font-weight: 600;
      }
      .cat-sidebar-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .cat-sidebar-count {
        font-size: 0.75rem; color: var(--text-muted);
        font-variant-numeric: tabular-nums;
      }
      /* Nested subcategory links — indented, lighter, smaller. */
      .cat-sidebar-sublist { list-style: none; margin: 2px 0 6px; padding: 0; }
      .cat-sidebar-sublist a {
        padding: 7px 12px 7px 24px;
        font-size: 0.82rem; font-weight: 500;
        color: var(--text-muted);
      }
      .cat-sidebar-sublist a:hover { color: var(--text); }
      .cat-sidebar-sublist a.is-active {
        color: var(--accent); background: var(--accent-dim);
        border-left-color: var(--accent); font-weight: 600;
      }

      /* Sectioned content — each category becomes a <section> with its
         own header. Generous top margin between sections gives the
         menu a visual rhythm; the first section sits flush with the
         top of the column. */
      .layout-with-sidebar.is-sectioned .cat-section + .cat-section {
        margin-top: 36px;
      }
      .cat-section-head {
        display: flex; align-items: baseline; gap: 12px;
        margin: 0 0 14px;
        padding-bottom: 8px;
        border-bottom: 1px solid var(--border-light);
      }
      .cat-section-head h2 {
        font-family: var(--font-display);
        font-size: 1.4rem; font-weight: 700;
        color: var(--text); letter-spacing: -0.01em;
        margin: 0;
      }
      .cat-section-count {
        font-size: 0.8rem; color: var(--text-muted);
        font-variant-numeric: tabular-nums;
      }
      /* Subcategory section (level 2). Tighter top margin than a new
         top-level section so children read as belonging to the parent
         above. h3 header is smaller than the category h2. */
      .layout-with-sidebar.is-sectioned .cat-section.subcat-section {
        margin-top: 22px;
      }
      .cat-section-head--sub {
        border-bottom-color: transparent;
        margin-bottom: 10px; padding-bottom: 2px;
      }
      .cat-section-head--sub h3 {
        font-family: var(--font-display);
        font-size: 1.08rem; font-weight: 700;
        color: var(--text); letter-spacing: -0.005em; margin: 0;
      }
      /* Parent super-header shown above the first subcategory when the
         parent has no loose lead items — keeps the category name in the
         flow exactly once. */
      .cat-super-head {
        font-family: var(--font-display);
        font-size: 1.4rem; font-weight: 700; color: var(--text);
        letter-spacing: -0.01em;
        margin: 0 0 14px; padding-bottom: 8px;
        border-bottom: 1px solid var(--border-light);
      }
    }

    /* Below 1024px the wrapper is invisible — block flow only, sidebar
       hidden via x-show. cat-section-head ALSO doesn't render because
       block.categoryName is null in filter mode. So the only addition
       at mobile / tablet is the inert .layout-with-sidebar wrapper
       div, which costs nothing. */

    @media (min-width: 1280px) {
      /* Wide-desktop column bumps. Gallery → 5 cols (Myntra), Grid → 6
         cols (Blinkit). List / Magazine deliberately don't densify
         further — they're meant to stay readable, not packed. */
      html[data-layout="gallery"] { --layout-cols: 5; }
      html[data-layout="grid"]    { --layout-cols: 6; }
    }

    /* ===================================================================
       BOOKING THEME (ZSite Booking) — services-forward skin over the SAME
       website chrome (header, hero, tiles, product page, footer all reused
       via isStorefront()). Scoped under html[data-layout="booking"]. Design
       language: calm, trust-forward; content-led SERVICE cards with a
       duration chip, a green "next available" chip, and a loud Book CTA.
       For salon / astrology / clinic / turf / classes.
       =================================================================== */
    html[data-layout="booking"] { --layout-cols: 3; --layout-gap: 18px; --layout-pad: 20px; --layout-max: 1180px; --layout-tiles: 5; --tiles-gap: 16px; }
    html[data-layout="booking"] .main { max-width: 1180px; }
    html[data-layout="booking"] .menu-card { margin-bottom: 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 1px 3px color-mix(in srgb, var(--shadow-color) 5%, transparent); overflow: hidden; transition: border-color .15s, box-shadow .15s, transform .15s; }
    html[data-layout="booking"] .menu-card:hover { transform: translateY(-2px); box-shadow: 0 12px 30px color-mix(in srgb, var(--shadow-color) 12%, transparent); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
    html[data-layout="booking"] .menu-card-inner { flex-direction: column; align-items: stretch; gap: 0; padding: 0; }
    /* Photo optional + content-led; a wide soft banner when present. */
    html[data-layout="booking"] .card-img-wrap { width: 100%; aspect-ratio: 16 / 10; background: var(--surface2); }
    html[data-layout="booking"] .card-img { object-fit: cover; }
    html[data-layout="booking"] .card-body { padding: 14px 16px 16px; gap: 6px; }
    html[data-layout="booking"] .card-name { font: 700 1.05rem var(--font-display); letter-spacing: -0.01em; line-height: 1.3; }
    html[data-layout="booking"] .card-desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 0.86rem; color: var(--text-secondary); }
    /* Booking meta chips (rendered only for bookable items on this theme). */
    .card-booking { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
    .card-booking-dur { display: inline-flex; align-items: center; gap: 4px; font: 600 0.74rem var(--font-body); color: var(--text-muted); background: var(--surface2); border-radius: 6px; padding: 3px 8px; }
    .card-booking-next { display: inline-flex; align-items: center; gap: 4px; font: 700 0.74rem var(--font-body); color: var(--green, #16a34a); background: color-mix(in srgb, var(--green, #16a34a) 13%, transparent); border-radius: 6px; padding: 3px 8px; }
    /* Price + a loud accent Book button on the footer baseline. */
    html[data-layout="booking"] .card-footer { margin-top: 10px; padding-top: var(--space-3); border-top: 1px solid var(--border); gap: 10px; align-items: center; }
    html[data-layout="booking"] .card-price { font-size: var(--step-0); font-weight: 800; }
    html[data-layout="booking"] .add-btn { margin-left: auto; min-width: 92px; min-height: 38px; padding: 8px 18px; border-radius: 10px; border: none; background: var(--accent); color: var(--theme-accent-fg, #fff); font-weight: 800; font-size: 0.84rem; letter-spacing: 0.02em; box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 35%, transparent); }
    html[data-layout="booking"] .add-btn:hover { filter: brightness(1.06); }

    /* Retail products sold alongside services (item not bookable): the wide
       16:10 service banner would crop a packshot, so these cards use the
       standard product treatment the advanced layouts ship — the FULL photo
       fitted into a padded square — while service cards keep the banner.
       The store's "Fill product images" toggle (body.pimg-fill) opts back
       into edge-to-edge cover, exactly as it does on swift/bazaar/clinic. */
    html[data-layout="booking"] .menu-card.card--product .card-img-wrap { aspect-ratio: var(--card-aspect, 1/1); padding: var(--space-2); }
    html[data-layout="booking"] .menu-card.card--product .card-img { object-fit: contain; object-position: center; }
    html[data-layout="booking"] body.pimg-fill .menu-card.card--product .card-img-wrap { padding: 0; }
    html[data-layout="booking"] body.pimg-fill .menu-card.card--product .card-img { object-fit: cover; object-position: var(--img-anchor, center); }

    /* Booking on phones. These MUST sit after the three rules above: they
       share their specificity, and the phone block earlier in the file adds
       none, so source order is what decides. Booking's Book button is 92px
       and its price 1rem — together 174px against a 140px footer, which is
       why Add wrapped here while the stepper (62px) did not, giving one
       theme two different card layouts. Trimmed to fit, and the body padding
       gives back the 8px that makes it comfortable rather than exact. */
    @media (max-width: 719px) {
      html[data-layout="booking"] .card-body { padding-left: var(--space-3); padding-right: var(--space-3); }
      html[data-layout="booking"] .card-price { font-size: 0.95rem; }
      html[data-layout="booking"] .add-btn { min-width: 72px; padding: 8px 10px; }
    }
    /* Calmer hero + service-category tiles. */
    html[data-layout="booking"] .sf-hero { aspect-ratio: 1600 / 600; max-height: 600px; }
    html[data-layout="booking"] .sf-section-title { font-size: 1.4rem; }
    html[data-layout="booking"] .sf-tile-img { aspect-ratio: 1 / 1; border-radius: 14px; }
    @media (max-width: 900px) {
      html[data-layout="booking"] { --layout-cols: 2; --layout-gap: 12px; --layout-tiles: 3; }
    }
    @media (max-width: 560px) {
      html[data-layout="booking"] { --layout-cols: 1; }
      html[data-layout="booking"] .menu-card.card--product .card-img-wrap { aspect-ratio: 4 / 3; }
    }

    /* ===================================================================
       CATERING THEME (ZSite Catering) — caterer-first EVENT LANDING skin
       over the shared website chrome (header/hero/footer via isStorefront()).
       Home replaces the product grid with a quote band + per-head PACKAGES
       showcase + service-styles strip; the configurator/estimate + PDP already
       auto-theme. Warm, generous, trust-forward. html[data-layout="catering"].
       =================================================================== */
    html[data-layout="catering"] .main { max-width: 1120px; }
    html[data-layout="catering"] .sf-hero { aspect-ratio: 1600 / 620; max-height: 620px; }
    html[data-layout="catering"] .sf-section-title { font: 700 1.5rem var(--font-display); letter-spacing: -0.02em; text-align: center; margin: 8px 0 18px; }
    .ct-land { max-width: 1120px; margin: 0 auto; padding: 0 20px; }
    .ct-land-intro { text-align: center; padding: 34px 12px 8px; }
    .ct-land-intro-inner { max-width: 760px; margin: 0 auto; }
    /* The banner is the shared Store→hero carousel (.sf-hero); this intro is a
       clean text band beneath it. */
    .ct-land-h { font: 800 clamp(1.7rem, 4.4vw, 2.6rem) var(--font-display); letter-spacing: -0.02em; line-height: 1.12; margin: 0 auto; max-width: 22ch; }
    .ct-land-sub { font-size: 1.02rem; color: var(--text-secondary); line-height: 1.6; max-width: 62ch; margin: 14px auto 0; }
    .ct-land-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 24px; }
    .ct-land-quote { display: inline-flex; align-items: center; background: var(--accent); color: var(--theme-accent-fg, #fff); border: none; cursor: pointer;
      font: 700 1rem var(--font-body); padding: 14px 34px; border-radius: var(--radius-full, 999px); box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 38%, transparent); transition: filter .15s, transform .06s; }
    .ct-land-quote:hover { filter: brightness(1.07); }
    .ct-land-quote:active { transform: scale(.985); }
    .ct-land-wa { display: inline-flex; align-items: center; gap: 8px; padding: 13px 22px; border-radius: var(--radius-full, 999px); border: 1.5px solid var(--border); background: var(--surface); color: var(--text); font-weight: 700; font-size: 0.94rem; text-decoration: none; }
    .ct-land-wa:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
    .ct-land-serving { margin-top: 16px; font-size: 0.9rem; color: var(--text-muted); }
    .ct-land-packs { padding: 34px 0 6px; }
    .ct-packs { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
    .ct-pack { display: flex; flex-direction: column; text-align: left; background: var(--surface); border: 1.5px solid var(--border); border-radius: 18px; overflow: hidden; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: border-color .15s, box-shadow .15s, transform .15s; }
    .ct-pack:hover { transform: translateY(-3px); box-shadow: 0 16px 40px rgba(0,0,0,0.12); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
    .ct-pack-img { display: block; width: 100%; aspect-ratio: 4 / 3; background-size: cover; background-position: center; background-color: var(--surface2); }
    .ct-pack-body { padding: 18px 18px 6px; display: flex; flex-direction: column; gap: 7px; flex: 1 1 auto; }
    .ct-pack-name { font: 750 1.25rem var(--font-display); letter-spacing: -0.01em; }
    .ct-pack-desc { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.5; }
    .ct-pack-meta { font-size: 0.8rem; color: var(--text-muted); margin-top: 2px; }
    .ct-pack-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--border); margin-top: 8px; }
    .ct-pack-price { font-weight: 800; font-size: 1.15rem; }
    .ct-pack-price small { font-weight: 600; font-size: 0.72rem; color: var(--text-muted); margin-left: 3px; }
    .ct-pack-go { font-weight: 750; font-size: 0.82rem; color: var(--accent); white-space: nowrap; }
    .ct-land-note { text-align: center; font-size: 0.84rem; color: var(--text-muted); margin: 18px auto 0; max-width: 60ch; line-height: 1.5; }
    .ct-land-styles { padding: 32px 0 10px; }
    .ct-styles { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
    .ct-style { display: inline-flex; align-items: center; gap: 6px; padding: 10px 18px; border-radius: var(--radius-full, 999px); background: var(--surface2); border: 1px solid var(--border); font-weight: 650; font-size: 0.9rem; }
    /* "From our events" gallery — a photo grid with a tap-to-zoom lightbox. */
    .ct-land-gallery { padding: 34px 0 12px; }
    .ct-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; }
    .ct-gallery-item { aspect-ratio: 1 / 1; border: none; padding: 0; border-radius: 14px; background-size: cover; background-position: center; background-color: var(--surface2); cursor: pointer; transition: transform .15s, box-shadow .15s; }
    .ct-gallery-item:hover { transform: scale(1.02); box-shadow: 0 12px 32px rgba(0,0,0,0.16); }
    @media (max-width: 640px) {
      .ct-land-intro { padding-top: 24px; }
      .ct-packs { grid-template-columns: 1fr; }
      .ct-gallery { grid-template-columns: repeat(2, 1fr); }
    }

    /* ===================================================================
       SHOWROOM THEME (ZSite Showroom) — furniture/home skin over the SAME
       website chrome as 'storefront' (header, hero, tiles, footer, PDP
       all reused; membership in isStorefront()). Design language: big
       lifestyle photography, wide 3:2 cards in a roomy 3-across grid,
       "Shop by room" category tiles with a caption overlay, calm display
       titles. Think IKEA / Wayfair / Urban Ladder. Pure CSS — no
       fragment, no entitlement gate; any vertical can run it.
       Wide-card GEOMETRY lives here on the layout; the vertical only
       contributes its accent, so a decor or appliance store gets the
       same look. html[data-layout="showroom"].
       =================================================================== */
    html[data-layout="showroom"] {
      --card-aspect: 3/2; --modal-aspect: 4/3;
      --layout-cols: 3; --layout-gap: 26px; --layout-pad: 24px; --layout-max: 1320px;
      --layout-tiles: 3; --tiles-gap: 20px;
    }
    html[data-layout="showroom"] .main { max-width: 1320px; }
    html[data-layout="showroom"] .menu-card { margin-bottom: 0; transition: transform .2s, box-shadow .2s; }
    html[data-layout="showroom"] .menu-card:hover { transform: translateY(-4px); box-shadow: 0 18px 44px color-mix(in srgb, var(--shadow-color) 14%, transparent); }
    html[data-layout="showroom"] .menu-card-inner { flex-direction: column; align-items: stretch; }
    html[data-layout="showroom"] .card-img-wrap { width: 100%; }
    html[data-layout="showroom"] .card-body { padding: 14px 16px; min-width: 0; }
    /* Phones: 12px, not 16px. Those 8px are the difference between a 132px
       footer and a 150px one, and ₹14,000 + a 64px control needs 142px — so
       at 16px the card wrapped while ₹7,000 (which needs exactly 132px) did
       not, and the same rail showed two different layouts. MUST stay after
       the rule above: both selectors have identical specificity and a media
       query adds none, so whichever is written last wins. Placing it in the
       phone block further up the file is what made the first attempt at this
       silently do nothing. */
    @media (max-width: 719px) {
      html[data-layout="showroom"] .card-body { padding: 14px 12px; }
    }
    html[data-layout="showroom"] .card-name { font-family: var(--font-display); font-size: 1.05rem; }
    html[data-layout="showroom"] .card-desc { display: none; }

    /* Hero: the shared 1600/600 band and the shared `contain`, deliberately.
       This used to be a taller 1600/700 frame with background-size: cover, to
       let room photography fill it edge to edge. That assumed merchants upload
       PHOTOGRAPHS. They upload designed banners — a logo, a headline, a SHOP
       NOW button, a row of room icons, all sitting near the edges — and cover
       ate them: a standard 1600x600 (2.67:1) banner in a 2.29:1 frame is
       scaled to fill the height and loses ~8% off each side. On fabfurniture
       that cut the "FAB" wordmark in half, turned "LIVE IN COMFORT" into "VE
       IN COMFORT", and clipped the CTA. Letterboxing an odd-sized image is a
       cosmetic disappointment; cropping one destroys the merchant's branding,
       so showroom now behaves like every other website layout. */
    html[data-layout="showroom"] .sf-hero { aspect-ratio: 1600 / 600; max-height: 600px; }

    /* "Shop by room" — the ordinary category tiles enlarged to wide
       16:10 photo cards, 3 across, room name on a bottom gradient
       (editorial's overlay pattern, landscape instead of portrait). */
    html[data-layout="showroom"] .sf-tiles-wrap { padding-top: 44px; }
    html[data-layout="showroom"] .sf-tile { position: relative; gap: 0; }
    html[data-layout="showroom"] .sf-tile-img { aspect-ratio: 16/10; border-radius: var(--radius); }
    html[data-layout="showroom"] .sf-tile-name {
      position: absolute; left: 0; right: 0; bottom: 0; padding: 44px 16px 14px; color: #fff;
      text-align: left; font-family: var(--font-display); font-weight: 650; font-size: 1.15rem;
      letter-spacing: 0.01em; background: linear-gradient(0deg, rgba(0,0,0,0.62), transparent);
      border-radius: 0 0 var(--radius) var(--radius); pointer-events: none;
    }
    html[data-layout="showroom"] .sf-section-title { font-size: clamp(1.5rem, 2.6vw, 1.9rem); }

    /* PDP: landscape gallery — room shots cover-cropped, no letterbox. */
    html[data-layout="showroom"] .sf-pdp-slide { aspect-ratio: 4/3; background-size: cover; }

    @media (max-width: 1023px) {
      html[data-layout="showroom"] { --layout-cols: 2; --layout-gap: 18px; --layout-pad: 20px; --layout-tiles: 2; }
    }
    @media (max-width: 719px) {
      html[data-layout="showroom"] { --layout-cols: 1; --layout-gap: 18px; --layout-pad: 14px; --layout-tiles: 2; --tiles-gap: 12px; }
    }

    /* ===== Tiffin / meal-plan builder (migrations 121-125) — themed via the
       storefront's own tokens so it adapts to every layout automatically. ===== */
    .mpb { display:flex; flex-direction:column; gap:22px; margin:10px 0 4px; }
    .mpb-sec { display:flex; flex-direction:column; gap:11px; }
    .mpb-h { display:flex; align-items:baseline; justify-content:space-between; gap:10px; }
    .mpb-h-t { font-weight:650; font-size:14.5px; color:var(--text); letter-spacing:-.01em; }
    .mpb-h-x { font-size:12px; color:var(--text-muted); }
    .mpb-presets { display:flex; gap:14px; }
    .mpb-preset { background:none; border:none; padding:0; cursor:pointer; font-weight:600; font-size:12.5px; color:var(--accent-text); }
    .mpb-preset:active { opacity:.6; }

    .mpb-packs { display:flex; flex-direction:column; gap:10px; }
    .mpb-pack { position:relative; display:flex; align-items:center; gap:13px; width:100%; text-align:left;
      padding:14px 16px; border:1.5px solid var(--border); border-radius:var(--radius-md,14px);
      background:var(--surface); cursor:pointer; transition:border-color .15s, background .15s, transform .06s; }
    .mpb-pack:active { transform:scale(.99); }
    .mpb-pack.sel { border-color:var(--accent); background:var(--accent-dim); }
    .mpb-pack-radio { flex:0 0 auto; width:20px; height:20px; border-radius:50%; border:2px solid var(--border-light); display:grid; place-items:center; transition:.15s; }
    .mpb-pack.sel .mpb-pack-radio { border-color:var(--accent); background:var(--accent); }
    .mpb-pack-radio svg { width:12px; height:12px; opacity:0; stroke:var(--accent-on); }
    .mpb-pack.sel .mpb-pack-radio svg { opacity:1; }
    /* Catering add-on rows reuse the pack radio; give them their OWN selected
       state (the .mpb-pack.sel selector never matches inside .ct-addon). */
    .ct-addon { display:flex; align-items:center; gap:12px; padding:12px 14px; border:1.5px solid var(--border);
      border-radius:var(--radius-md,14px); background:var(--surface); cursor:pointer; transition:border-color .15s, background .15s, transform .06s; }
    .ct-addon:active { transform:scale(.99); }
    .ct-addon.sel { border-color:var(--accent); background:var(--accent-dim); }
    .ct-addon.sel .mpb-pack-radio { border-color:var(--accent); background:var(--accent); }
    .ct-addon.sel .mpb-pack-radio svg { opacity:1; }
    .mpb-pack-main { flex:1 1 auto; min-width:0; }
    .mpb-pack-name { font-weight:680; font-size:15.5px; color:var(--text); letter-spacing:-.01em; }
    .mpb-pack-sub { font-size:12.5px; color:var(--text-muted); margin-top:2px; }
    .mpb-pack-right { text-align:right; flex:0 0 auto; }
    .mpb-pack-price { font-weight:750; font-size:16.5px; color:var(--text); font-variant-numeric:tabular-nums; }
    .mpb-pack-per { font-size:11.5px; color:var(--text-muted); margin-top:1px; }
    .mpb-pack-badge { position:absolute; top:-8px; left:14px; font-weight:700; font-size:9.5px; letter-spacing:.04em;
      text-transform:uppercase; color:var(--accent-on); background:var(--accent); padding:3px 8px; border-radius:999px; }

    .mpb-seg { display:flex; gap:6px; padding:4px; background:var(--surface2,var(--surface)); border:1px solid var(--border); border-radius:var(--radius-md,14px); }
    .mpb-seg-opt { flex:1; display:flex; align-items:center; justify-content:center; gap:8px; padding:11px; border:none;
      border-radius:calc(var(--radius-md,14px) - 5px); background:transparent; cursor:pointer; font-weight:600; font-size:14px; color:var(--text-secondary); transition:.15s; }
    .mpb-seg-opt.sel { background:var(--surface); color:var(--text); box-shadow:0 1px 3px rgba(0,0,0,.14); }
    .mpb-vdot { width:11px; height:11px; border-radius:3px; border:1.6px solid currentColor; display:inline-grid; place-items:center; }
    .mpb-vdot::after { content:''; width:5px; height:5px; border-radius:50%; background:currentColor; }
    .mpb-vdot--veg { color:#3a9e4f; } .mpb-vdot--nv { color:#d9433f; }
    .mpb-diet-badge { display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border:1px solid var(--border); border-radius:var(--radius-md,14px); background:var(--surface2,var(--surface)); font-weight:650; font-size:14px; color:var(--text); }

    .mpb-chips { display:flex; flex-wrap:wrap; gap:9px; }
    .mpb-chip { display:flex; flex-direction:column; align-items:flex-start; gap:1px; padding:9px 15px; border:1.5px solid var(--border);
      border-radius:var(--radius-md,12px); background:var(--surface); cursor:pointer; transition:.15s; }
    .mpb-chip.sel { border-color:var(--accent); background:var(--accent-dim); }
    .mpb-chip:disabled { opacity:.4; cursor:not-allowed; }
    .mpb-chip b { font-weight:640; font-size:14px; color:var(--text); }
    .mpb-chip span { font-size:11.5px; color:var(--text-muted); }
    .mpb-note { font-size:12px; color:var(--text-muted); margin-top:6px; line-height:1.4; }
    /* Itemized catering estimate (review step) */
    .ct-estimate { border:1.5px solid var(--border); border-radius:var(--radius-md,14px); padding:14px 16px; background:var(--surface); margin-bottom:14px; }
    .ct-est-head { display:flex; justify-content:space-between; align-items:baseline; font-weight:750; font-size:15.5px; color:var(--text); }
    .ct-est-head b { font-size:19px; font-variant-numeric:tabular-nums; }
    .ct-est-row { display:flex; justify-content:space-between; gap:14px; font-size:13px; color:var(--text-muted); padding:6px 0; border-top:1px solid var(--border); margin-top:8px; }
    .ct-est-row span:last-child { font-variant-numeric:tabular-nums; white-space:nowrap; }
    .ct-est-dep { font-size:12.5px; color:var(--text); background:var(--accent-dim); border-radius:10px; padding:9px 12px; margin-top:12px; line-height:1.5; }
    .ct-est-cap { font-size:11.5px; color:var(--text-muted); margin-top:8px; line-height:1.4; }
    .mpb-blockreason { font-size:12px; color:var(--warn,#b06a00); text-align:center; padding:8px 12px 0; line-height:1.4; }
    .mpb-check { display:flex; align-items:center; gap:10px; font-size:14px; color:var(--text); cursor:pointer; padding:4px 0; }
    .mpb-check input { width:18px; height:18px; accent-color:var(--accent); flex:0 0 auto; }

    .mpb-days { display:flex; gap:8px; flex-wrap:wrap; }
    .mpb-day { width:42px; height:42px; border-radius:50%; border:1.5px solid var(--border); background:var(--surface);
      cursor:pointer; font-weight:650; font-size:13px; color:var(--text-secondary); transition:.15s; }
    .mpb-day.sel { border-color:var(--accent); background:var(--accent); color:var(--accent-on); }

    .mpb-menu { display:flex; gap:10px; overflow-x:auto; padding:2px 0 6px; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; }
    .mpb-menu::-webkit-scrollbar { height:0; }
    .mpb-menu-day { flex:0 0 auto; width:136px; scroll-snap-align:start; padding:12px 13px; border:1px solid var(--border); border-radius:var(--radius-md,12px); background:var(--surface); }
    .mpb-menu-dow { font-weight:700; font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; color:var(--accent-text); margin-bottom:5px; }
    .mpb-menu-dish { font-size:12.5px; color:var(--text-secondary); line-height:1.45; }

    .mpb-inputs { display:flex; flex-direction:column; gap:9px; }
    .mpb-input { width:100%; padding:12px 14px; border:1.5px solid var(--border); border-radius:var(--radius-md,12px);
      background:var(--surface); color:var(--text); font-size:14.5px; font-family:inherit; }
    .mpb-input:focus { outline:none; border-color:var(--accent); }
    .mpb-input::placeholder { color:var(--text-muted); }
    .mpb-err { font-size:12.5px; font-weight:600; color:#d9433f; }

    .mpb-summary { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 16px; background:var(--accent-dim); border-radius:var(--radius-md,14px); }
    .mpb-summary-l { font-size:12.5px; color:var(--text-secondary); line-height:1.45; }
    .mpb-summary-l b { color:var(--text); font-weight:650; }
    .mpb-summary-tot { font-weight:800; font-size:20px; color:var(--text); white-space:nowrap; font-variant-numeric:tabular-nums; }
    .mpb-cta { width:100%; padding:15px; border:none; border-radius:var(--radius-md,14px); background:var(--accent); color:var(--accent-on);
      font-weight:700; font-size:15.5px; cursor:pointer; transition:opacity .15s, transform .06s; }
    .mpb-cta:active { transform:scale(.99); }
    .mpb-cta[disabled] { opacity:.5; cursor:not-allowed; }
    .mpb-trust { display:flex; align-items:center; justify-content:center; gap:7px; flex-wrap:wrap; font-size:11.5px; color:var(--text-muted); text-align:center; }
    .mpb-pays { display:flex; flex-direction:column; gap:9px; }
    .mpb-pay { display:flex; align-items:center; gap:12px; padding:13px 15px; border:1.5px solid var(--border); border-radius:var(--radius-md,12px); background:var(--surface); cursor:pointer; transition:.15s; }
    .mpb-pay.sel { border-color:var(--accent); background:var(--accent-dim); }
    .mpb-pay-ico { font-size:19px; flex:0 0 auto; width:24px; text-align:center; }
    .mpb-pay-main { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; }
    .mpb-pay-t { display:block; font-weight:640; font-size:14.5px; color:var(--text); line-height:1.3; }
    .mpb-pay-s { display:block; font-size:12px; color:var(--text-muted); margin-top:1px; line-height:1.3; }
    .mpb-pay-radio { flex:0 0 auto; width:18px; height:18px; border-radius:50%; border:2px solid var(--border-light); transition:.15s; }
    .mpb-pay.sel .mpb-pay-radio { border-color:var(--accent); border-width:6px; }

    /* Tiffin builder wizard chrome — progress header + sticky footer pay bar. */
    .mpb-steps { margin-bottom:2px; }
    .mpb-steps-bar { height:4px; border-radius:3px; background:color-mix(in srgb, var(--text) 12%, transparent); overflow:hidden; }
    .mpb-steps-bar > i { display:block; height:100%; background:var(--accent); border-radius:3px; transition:width .25s ease; }
    .mpb-steps-lbl { display:flex; align-items:baseline; justify-content:space-between; margin-top:7px; }
    .mpb-steps-lbl .s1 { font-size:11.5px; font-weight:650; color:var(--text-muted); text-transform:uppercase; letter-spacing:.04em; }
    .mpb-steps-lbl .s2 { font-size:14px; font-weight:680; color:var(--text); letter-spacing:-.01em; }
    .mpb-step { display:flex; flex-direction:column; gap:22px; }
    .mpb-foot { position:sticky; bottom:0; z-index:2; display:flex; align-items:center; gap:10px; margin:4px -2px -2px;
      padding:12px 2px calc(8px + env(safe-area-inset-bottom)); background:linear-gradient(to top, var(--surface) 78%, transparent); }
    .mpb-foot-sum { flex:1 1 auto; min-width:0; font-size:12.5px; color:var(--text-muted); font-variant-numeric:tabular-nums; }
    .mpb-foot-sum b { display:block; color:var(--text); font-weight:750; font-size:17px; letter-spacing:-.01em; }
    .mpb-back { flex:0 0 auto; padding:13px 18px; border-radius:var(--radius-md,12px); border:1.5px solid var(--border);
      background:var(--surface); color:var(--text); font-weight:620; font-size:14.5px; cursor:pointer; }
    .mpb-back:active { transform:scale(.98); }
    .mpb-next { flex:0 0 auto; min-width:118px; padding:14px 22px; border:none; border-radius:var(--radius-md,12px);
      display:inline-flex; align-items:center; justify-content:center; gap:6px; line-height:1.2; text-align:center; white-space:nowrap;
      background:var(--accent); color:var(--accent-on); font-weight:700; font-size:15px; cursor:pointer; transition:opacity .15s, transform .06s; }
    .mpb-next.grow { flex:1 1 auto; }
    .mpb-next:active { transform:scale(.99); }
    .mpb-next[disabled] { opacity:.45; cursor:not-allowed; }
    .mpb-loc { display:flex; align-items:center; justify-content:center; gap:9px; width:100%; padding:12px 14px;
      border:1.5px dashed var(--border-light); border-radius:var(--radius-md,12px); background:var(--surface);
      color:var(--accent-text); font-weight:620; font-size:14px; cursor:pointer; }
    .mpb-loc.done { border-style:solid; color:var(--success,#16a34a); border-color:color-mix(in srgb, var(--success,#16a34a) 45%, transparent); }
    .mpb-loc[disabled] { opacity:.6; cursor:default; }

    /* ===== Catering configurator (.mpb-ct) — mobile + cross-browser field
       alignment. Scoped to .mpb-ct so the meal-plan / tiffin builder that shares
       the .mpb-* styles is left untouched. ===== */
    .mpb-ct input.mpb-input, .mpb-ct select.mpb-input { min-height:46px; }
    /* Drop the number spinners for a clean guest / quantity field. */
    .mpb-ct input[type="number"].mpb-input { -moz-appearance:textfield; }
    .mpb-ct input[type="number"].mpb-input::-webkit-outer-spin-button,
    .mpb-ct input[type="number"].mpb-input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
    /* Native <select> renders inconsistently across browsers — strip its chrome
       and draw a matching chevron so it lines up with the text inputs. */
    .mpb-ct select.mpb-input {
      -webkit-appearance:none; appearance:none; cursor:pointer; padding-right:40px;
      background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
      background-repeat:no-repeat; background-position:right 14px center;
    }
    /* Keep the NATIVE date/time control — the calendar icon, tap-to-pick and the
       browser's placeholder mask all live in its default appearance. (appearance:none
       stripped the icon + placeholder and collapsed the height on mobile.) Just hold
       a consistent height and let the value sit centred. */
    .mpb-ct input[type="date"].mpb-input,
    .mpb-ct input[type="time"].mpb-input { min-height:46px; display:flex; align-items:center; }
    .mpb-ct input[type="date"].mpb-input::-webkit-date-and-time-value { text-align:left; }
    /* Add-on rows: on narrow phones let the hours stepper + amount wrap below the
       name instead of crushing it. (Overrides the inline flex on the name span.) */
    .mpb-ct .ct-addon { flex-wrap:wrap; column-gap:12px; row-gap:8px; }
    .mpb-ct .ct-addon > span[style*="min-width:0"] { flex:1 1 140px !important; }
    .mpb-ct .ct-addon > b { margin-left:auto; white-space:nowrap; }
    /* Dish chips: the veg/non-veg dot is inline-grid and otherwise sits on the
       text baseline, dropping low and butting against the name. Centre it with
       the dish name and give it a small gap. */
    .mpb-ct .mpb-chip b { display:inline-flex; align-items:center; gap:6px; }
    .mpb-ct .mpb-chip .mpb-vdot { flex:0 0 auto; }
    /* Desktop: the PDP info column is ~500px wide — pair the short contact fields
       into two columns instead of one over-wide stack. */
    @media (min-width:860px) {
      .mpb-ct .mpb-fields { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
      .mpb-ct .mpb-fields > input[type="email"],
      .mpb-ct .mpb-fields > textarea { grid-column:1 / -1; }
    }

    /* "Your subscription" welcome-back control — a header icon on every layout
       (was a home-feed banner strip). Device-remembered manage-link recovery;
       shown only while customer accounts are OFF — with accounts on, the
       signed-in menu's "My subscriptions" is the way back in. */
    .sub-head-wrap { position:relative; display:inline-flex; }
    .sub-head-wrap a.sf-search-btn { display:inline-grid; place-items:center; text-decoration:none; }
    /* Simple layouts: sits in the sticky nav next to search, sized to match
       the search input's height. */
    /* One row inside the column-flex .nav-controls: icon + search side by side.
       The account fragment's .acct-navrow nests inside this without effect. */
    .sub-navrow { display:flex; align-items:center; gap:8px; min-width:0; }
    .sub-navrow > .search-wrap, .sub-navrow > .acct-navrow { flex:1 1 auto; min-width:0; }
    .sub-nav-btn { flex:0 0 auto; width:42px; height:42px; border-radius:12px; border:1px solid var(--border);
      background:var(--surface); color:var(--accent-text); display:grid; place-items:center; cursor:pointer; padding:0; text-decoration:none; }
    .sub-nav-btn:hover { border-color:var(--accent); }
    .sub-nav-btn svg { width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:2; }
    .mmb-menu { position:absolute; right:0; top:calc(100% + 8px); z-index:60; width:min(300px, 82vw); background:var(--surface);
      border:1px solid var(--border); border-radius:14px; box-shadow:0 14px 38px rgba(0,0,0,.22); padding:6px; }
    .mmb-item { display:flex; align-items:center; gap:8px; border-radius:10px; }
    .mmb-item:hover { background:var(--accent-dim); }
    .mmb-item a { display:flex; align-items:center; gap:10px; flex:1 1 auto; min-width:0; padding:10px 11px; text-decoration:none; color:var(--text); }
    .mmb-item .n { flex:1 1 auto; font-size:13.5px; font-weight:560; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .mmb-item .go { font-size:12px; font-weight:600; color:var(--accent-text); white-space:nowrap; }
    .mmb-item .rm { border:none; background:transparent; color:var(--text-muted); cursor:pointer; font-size:16px; line-height:1; padding:6px 9px; }
  
    /* --- Subscribe & Save purchase options -------------------------------
       A RADIO GROUP above ONE button, not a second button. Two equal primary
       CTAs make people choose between actions instead of between plans, which
       is why Amazon, Chewy, Recharge and Skio all landed on this shape. The
       saving sits on the option itself, because it is the entire reason anyone
       picks the second one. */
    .po-wrap { display:flex; flex-direction:column; gap:8px; margin:14px 0 4px; }
    .po { display:flex; align-items:flex-start; gap:11px; padding:12px 13px; cursor:pointer;
          border:1.5px solid var(--line, #e6e6eb); border-radius:14px; background:transparent;
          text-align:left; font:inherit; color:inherit; width:100%; transition:border-color .15s, background .15s; }
    .po.on { border-color:var(--accent, #111827); background:color-mix(in srgb, var(--accent, #111827) 5%, transparent); }
    .po-dot { width:18px; height:18px; border-radius:999px; border:2px solid var(--line, #e6e6eb);
              flex:0 0 auto; margin-top:1px; position:relative; }
    .po.on .po-dot { border-color:var(--accent, #111827); }
    .po.on .po-dot::after { content:''; position:absolute; inset:3px; border-radius:999px; background:var(--accent, #111827); }
    .po-main { flex:1; min-width:0; }
    .po-t { font-weight:620; font-size:14.5px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
    .po-s { font-size:12.5px; opacity:.65; margin-top:2px; }
    .po-p { font-weight:700; font-size:15px; white-space:nowrap; margin-left:auto; }
    .po-p-old { font-size:12.5px; opacity:.5; text-decoration:line-through; margin-left:6px; font-weight:500; }
    .po-save { font-size:11px; font-weight:700; padding:2px 7px; border-radius:999px;
               background:color-mix(in srgb, #1aa64b 16%, transparent); color:#12833a; }
    .po-body { padding:2px 13px 12px 42px; display:flex; flex-direction:column; gap:10px; }
    .po-lbl { font-size:12px; font-weight:650; opacity:.65; }
    .po-cad { display:flex; flex-wrap:wrap; gap:6px; }
    .po-chip { padding:7px 12px; border-radius:999px; border:1px solid var(--line, #e6e6eb);
               background:transparent; color:inherit; font:inherit; font-size:12.5px; font-weight:600; cursor:pointer; }
    .po-chip.on { background:var(--accent, #111827); color:var(--accent-fg, #fff); border-color:var(--accent, #111827); }
    .po-days { display:grid; grid-template-columns:repeat(7,1fr); gap:5px; }
    .po-day { padding:8px 0; border-radius:10px; border:1px solid var(--line, #e6e6eb); background:transparent;
              color:inherit; font:inherit; font-size:11.5px; font-weight:650; cursor:pointer; }
    .po-day.on { background:var(--accent, #111827); color:var(--accent-fg, #fff); border-color:var(--accent, #111827); }
    .po-note { font-size:12px; opacity:.62; line-height:1.4; }

    /* --- Verify-in-place for subscriptions ---------------------------------
       A step INSIDE the checkout form, never a login sheet. Opening the account
       sheet here would bury the cart behind it, which is the failure mode this
       whole surface exists to avoid. */
    .vfy { border:1px solid var(--line,#e6e6eb); border-radius:14px; padding:13px; margin-top:10px;
           background:color-mix(in srgb, var(--accent,#111827) 4%, transparent); }
    .vfy-t { font-size:13.5px; font-weight:640; display:flex; align-items:center; gap:7px; }
    .vfy-s { font-size:12.5px; opacity:.7; margin-top:3px; line-height:1.45; }
    .vfy-row { display:flex; gap:8px; margin-top:10px; align-items:center; }
    .vfy-code { flex:1; font-size:19px; letter-spacing:.36em; text-align:center; font-weight:650;
                padding:11px 12px; border-radius:11px; border:1px solid var(--line,#e6e6eb);
                background:var(--bg,#fff); color:inherit; font-variant-numeric:tabular-nums; }
    /* The identifier being confirmed: shown as a fact with one way to change it,
       rather than an empty field asking for something already typed. */
    .vfy-seed { justify-content:space-between; padding:10px 12px; border-radius:11px;
                border:1px solid var(--line,#e6e6eb); background:var(--bg,#fff);
                font-weight:600; font-size:14px; }
    .vfy-change { background:none; border:0; padding:0 0 0 10px; font:inherit; font-size:12.5px;
                  color:inherit; opacity:.7; text-decoration:underline; cursor:pointer;
                  min-height:24px; flex:0 0 auto; }
    /* Same 46px target as every other field on the builder — a mistyped email is
       the one thing that makes this step fail twice. */
    .vfy-id { flex:1; min-width:0; font-size:15px; padding:12px; border-radius:11px;
              border:1px solid var(--line,#e6e6eb); background:var(--bg,#fff); color:inherit; }
    .vfy-id:focus { outline:none; border-color:var(--accent,#111827); }
    /* ── What you get, and how much of it ─────────────────────────────────
       A quantity per line, with the price of each and a running total. The
       total is the number people actually decide on, so it is never more than a
       glance away from the steppers that change it. */
    .mpi { border:1px solid var(--line,#e6e6eb); border-radius:14px; overflow:hidden; margin-top:4px; }
    .mpi-row { display:flex; align-items:center; gap:12px; padding:12px 13px; border-top:1px solid var(--line,#e6e6eb); }
    .mpi-row:first-child { border-top:0; }
    .mpi-row.is-off { opacity:.55; }
    .mpi-main { flex:1; min-width:0; }
    .mpi-name { font-size:14.5px; font-weight:600; }
    .mpi-price { font-size:12.5px; opacity:.7; margin-top:1px; }
    .mpi-step { display:flex; align-items:center; gap:4px; flex:0 0 auto; }
    .mpi-step button { width:38px; height:38px; border-radius:999px; border:1px solid var(--line,#e6e6eb);
                       background:var(--bg,#fff); color:inherit; font:inherit; font-size:18px; line-height:1;
                       cursor:pointer; }
    .mpi-step button:disabled { opacity:.35; cursor:default; }
    .mpi-step .n { min-width:34px; text-align:center; font-weight:680; font-variant-numeric:tabular-nums; font-size:15px; }
    .mpi-sum { display:flex; align-items:baseline; justify-content:space-between; gap:10px;
               padding:12px 13px; border-top:1px solid var(--line,#e6e6eb);
               background:color-mix(in srgb, var(--accent,#111827) 4%, transparent); }
    .mpi-sum-l { font-size:13px; opacity:.75; }
    .mpi-sum-v { font-size:17px; font-weight:720; letter-spacing:-.01em; font-variant-numeric:tabular-nums; }
    .mpi-sum-x { font-size:12px; opacity:.65; }

    /* Per-day override: folded away, because almost nobody wants it and the
       ones who do are looking for it. */
    .mpd-toggle { background:none; border:0; padding:10px 0 0; font:inherit; font-size:12.5px;
                  color:inherit; opacity:.75; text-decoration:underline; cursor:pointer; }
    .mpd { margin-top:10px; border:1px solid var(--line,#e6e6eb); border-radius:14px; overflow:hidden; }
    .mpd-item { padding:11px 13px; border-top:1px solid var(--line,#e6e6eb); }
    .mpd-item:first-child { border-top:0; }
    .mpd-name { font-size:13px; font-weight:600; margin-bottom:8px; }
    .mpd-days { display:grid; grid-template-columns:repeat(auto-fit, minmax(42px, 1fr)); gap:6px; }
    .mpd-day { text-align:center; }
    .mpd-day label { display:block; font-size:10px; font-weight:700; letter-spacing:.04em;
                     text-transform:uppercase; opacity:.6; margin-bottom:3px; }
    .mpd-day input { width:100%; min-height:40px; text-align:center; font:inherit; font-size:15px;
                     font-variant-numeric:tabular-nums; border:1px solid var(--line,#e6e6eb);
                     border-radius:10px; background:var(--bg,#fff); color:inherit; padding:4px; }
    .mpd-day input:focus { outline:none; border-color:var(--accent,#111827); }

    .vfy-meta { display:flex; justify-content:space-between; align-items:center;
                font-size:12px; opacity:.65; margin-top:8px; }
    .vfy-meta button { background:none; border:0; padding:0; font:inherit; font-size:12px;
                       color:inherit; text-decoration:underline; cursor:pointer; }
    .vfy-meta button:disabled { opacity:.45; cursor:default; text-decoration:none; }
    .vfy-ok { color:#12833a; font-weight:640; }
    .vfy-err { color:#c0392b; font-size:12.5px; margin-top:7px; }

    /* === EQUAL-HEIGHT PRODUCT CARDS ==================================
       A cross-cutting invariant, deliberately last in the sheet and
       deliberately !important: every product card in a row ends at the same
       line, and every price row sits on the same baseline, in all 12 layouts
       on every device.

       It has to override theme rules because the themes are what broke it,
       each in its own way — showroom pinned height:100% (indeterminate against
       a rail's auto height, and an explicit height disables stretch); swift,
       bazaar, clinic and booking replaced .card-footer's margin-top:auto with
       a fixed 9-10px, which unpins the footer; and several layouts never made
       the card a column at all, so .card-body's flex:1 had nothing to fill.
       A plain .card-footer selector loses to html[data-layout="x"] .card-footer
       on specificity no matter where it sits, so scoping alone cannot express
       "this holds everywhere".

       The mechanism is the ordinary one: the card is a column, the inner
       wrapper grows, and the action row is pushed down by an auto margin. What
       varies between cards — a name that wraps, a struck MRP — is absorbed as
       slack above the price rather than changing the card's height. */
    .menu-card { display: flex; flex-direction: column; height: auto !important; }
    .menu-card-inner { flex: 1 1 auto; }
    .sf-rail-card { align-self: stretch !important; }
    .sf-rail-track { align-items: stretch !important; }
    .card-footer { margin-top: auto !important; }

  /* iOS Safari zooms the page when a focused field is < 16px. Phone-only, wins over every component size.
     :where() keeps the guard at (0,0,1) so the few deliberately-large fields below can still out-rank it. */
  @media (max-width: 880px) {
    input:where(:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=color]):not([type=hidden])),
    select, textarea { font-size: 16px !important; }
    /* Fields that are deliberately LARGER than 16px keep their size (the guard is a floor, not a value). */
    .sf-search-layer.is-full .sf-search-obar input { font-size: 1.02rem !important; }
    .vfy-code { font-size: 19px !important; }
    .acct-otp { font-size: 24px !important; }
  }
  @media (min-width: 720px) and (max-width: 880px) {
    .sf-search-layer.is-full .sf-search-obar input { font-size: 1.2rem !important; }
  }
    /* === Reduced motion (global sweep) === The OS-level preference. Earlier
       blocks in this file guard their own surfaces (banners, cover drift, the
       neon sign, the WhatsApp FAB, the closed-strip dot); this one catches the
       decorative motion that had none: the hero logo ring, the skeleton
       shimmer, the card zoom / lift on hover and the add-pulse. Deliberately
       NOT touched: the neon `.sg-*` rules (their lit / unlit look lives in the
       base rules and their own guard above already handles motion), the photo
       carousel (finger-tracked, see its note), and sheet / modal enter
       transitions (positioning, and short). Nothing here adds a fill-mode or
       changes an element's resting state. */
    @media (prefers-reduced-motion: reduce) {
      .logo-ring, .sk, .add-btn, .add-pulse { animation: none !important; }
      .menu-card:hover, .menu-card:hover .card-img,
      .sf-tile:hover .sf-tile-img, .sf-catrail--pills > .sf-tile:hover,
      .offer-card:hover, .bn.is-link:hover:not(:active), .cart-strip-inner:hover,
      .upi-qr-wrap:hover, .sf-social .sf-social-link:hover, footer .f-social a:hover,
      .sf-built:hover .sf-built-mark, footer .powered a:hover .mark { transform: none !important; }
      .sf-hero-slide { transition: none !important; }
      html { scroll-behavior: auto !important; }
    }

    /* ── OS dark-mode fallback (OPT-IN) ────────────────────────────────────
       Applies ONLY when theme.mode === 'auto'. CONTRACT: store.html stamps
       data-mode="light|dark|auto" on <html> from theme.mode inside
       applyTheme(). It is deliberately NOT `html:not([data-mode])`: most
       stores have no mode at all, and that form would have re-skinned every
       one of them dark for OS-dark visitors with no merchant action. Nothing
       writes 'auto' yet (a Theme Studio option can). A theme that sets
       bg/surface/text wins regardless — those land as inline custom
       properties, which outrank any stylesheet rule.
       Only the neutrals move; --accent and every --theme-accent-* source are
       untouched, and --text-muted / --border / --text-secondary re-derive
       from these via the color-mix() in :root. */
    @media (prefers-color-scheme: dark) {
      html[data-mode="auto"] {
        --bg: #0f1115;
        --surface: #161920;
        --surface2: #1d212a;
        --surface3: #262b35;
        --text: #eceef2;
        --sk-bg: var(--bg); --sk-surface: var(--surface); --sk-border: var(--border);
        --sk-bone: var(--surface2); --sk-bone-hi: var(--surface3);
      }
    }
    /* A merchant-declared dark theme: the palette is theirs (inline vars);
       only the pre-theme skeleton needs to know not to flash light. */
    html[data-mode="dark"] {
      --sk-bg: var(--bg); --sk-surface: var(--surface); --sk-border: var(--border);
      --sk-bone: var(--surface2); --sk-bone-hi: var(--surface3);
    }
