:root{
    /* ---- legacy tokens (still referenced directly throughout this file —
       kept as-is so nothing that reads them needs to change) ---- */
    --bg:#0B0C0D;
    --surface:#15171B;
    --surface-2:#1D2025;
    --line:#2A2E33;
    --text:#F3F1EA;
    --muted:#8D9096;
    --amber:#FFA83D;
    --amber-dim:rgba(255,168,61,0.16);
    --green:#37C48C;
    --green-dim:rgba(55,196,140,0.16);
    --red:#E2604F;
    --radius:10px;
    --app-max:520px; /* width of the fixed-position summary bar / toast — unrelated to workspace width below, deliberately left alone */

    /* ============================================================
       DESIGN SYSTEM TOKENS (Phase B)
       Additive layer on top of the legacy tokens above. Nothing below
       renames or removes a legacy token — components keep working
       exactly as before; new/updated component rules read from here.
       ============================================================ */

    /* -- surfaces -- */
    --pw-bg: var(--bg);
    --pw-surface: var(--surface);
    --pw-surface-raised: var(--surface-2);
    --pw-overlay: rgba(0,0,0,0.6);

    /* -- text hierarchy -- */
    --pw-text: var(--text);
    --pw-text-soft: #C9CDC5;
    --pw-text-muted: var(--muted);
    /* Category headers (Order/Products) sat one step below --pw-text in
       lightness but the same warm-white hue, so at a glance they could
       read as just another (dim) product line rather than a heading —
       a real user reported this. A cool slate-blue reads as structural
       at a glance regardless of lightness, and deliberately isn't the
       amber accent (reserved for actionable/active state) or the
       success/danger/linked hues, so it can't be misread as any of those. */
    --pw-category: #7C94AA;

    /* -- borders -- */
    --pw-border: var(--line);
    --pw-border-strong: #454D46;

    /* -- accent (use sparingly: one primary action per screen, active nav,
       the share code) -- */
    --pw-accent: var(--amber);
    --pw-accent-ink: #241A05;      /* text color ON the accent fill */
    /* Accent-as-TEXT, separate from accent-as-FILL.
       Amber reads perfectly as text on a near-black page and is unusable on
       a white one (#FFA83D on #FFFFFF is about 1.9:1). But the same token
       also fills the primary button, where it must stay bright so the dark
       ink on top of it stays legible. One token cannot be both once a light
       theme exists, so text gets its own. In dark these alias the fill and
       nothing changes; light redefines only these. */
    /* Secondary text sitting ON a raised glass surface rather than on the
     page. --muted is tuned for the page: on the picker head's fill it
     measured 3.45:1, under AA. The page keeps --muted; only text that
     sits on glass takes this. */
  --pw-label-on-glass: #A6A9AF;
  /* The lens of a segmented control: raised out of a recessed track with
     the accent glowing inside it. Weaker than --pw-g-amber-inner, which is
     for a piece that floats on the page — this one already reads as
     "selected" from sitting proud of its own track, so it needs light, not
     a second outline. */
  --pw-g-seg-wash: radial-gradient(100% 140% at 50% 138%, rgba(255,168,61,0.30) 0%, rgba(255,168,61,0) 74%);
  --pw-g-seg-lens:
    inset 0 1px 0 rgba(255,255,255,0.24),
    inset 0 -1px 0 rgba(0,0,0,0.34),
    0 0 0 1px rgba(255,255,255,0.07),
    0 2px 6px -3px rgba(0,0,0,0.60);
  --pw-accent-text: var(--amber);
    --pw-success-text: var(--green);
    --pw-accent-soft: var(--amber-dim);
    --pw-accent-strong: #FFC24D;

    /* -- semantic states (deliberately distinct from the accent hue so
       "collected" and "call to action" never compete for attention) -- */
    --pw-success: var(--green);
    --pw-success-soft: var(--green-dim);
    --pw-success-ink: #0F1B14;
    --pw-danger: var(--red);
    --pw-danger-soft: #3A2320;
    --pw-linked: #7FA9D6;          /* a colleague's shared shop/list */
    --pw-linked-soft: #1C2A38;

    /* -- spacing scale -- */
    --pw-sp-1: 4px;
    --pw-sp-2: 8px;
    --pw-sp-3: 12px;
    --pw-sp-4: 16px;
    --pw-sp-5: 20px;
    --pw-sp-6: 24px;
    --pw-sp-8: 32px;

    /* -- radii -- */
    --pw-radius-sm: 6px;
    --pw-radius-md: var(--radius);
    --pw-radius-lg: 14px;
    --pw-radius-pill: 999px;

    /* -- shadows (restrained — one level for resting cards, one for
       floating/overlay surfaces) -- */
    --pw-shadow-card: 0 1px 2px rgba(0,0,0,0.18);
    --pw-shadow-float: 0 10px 28px rgba(0,0,0,0.38);

    /* -- typography -- */
    --pw-font: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    --pw-fs-caption: 11.5px;
    --pw-fs-small: 13px;
    --pw-fs-body: 15px;
    --pw-fs-body-lg: 16px;
    --pw-fs-title: 19px;
    --pw-fs-display: 24px;

    /* -- touch targets -- */
    --pw-tap-min: 44px;
    /* Order screen's pick-checkbox and qty steppers, tapped constantly
       while walking a shop, were sized to the same 44px as one-off
       header/settings buttons — at list-row density that read as
       oversized rather than accessible. Still comfortably tappable,
       just scaled to how often these actually get touched in a row. */
    --pw-tap-compact: 34px;

    /* -- layout widths --
       --app-max above stays the width of viewport-fixed overlays.
       These govern the actual reading/working column. */
    --pw-content-max: 560px;   /* mobile/tablet reading column */
    --pw-content-max-lg: 760px; /* desktop workspace column */
    --pw-sidenav-w: 232px;
    --pw-aside-w: 272px;

    /* -- breakpoints (documented here; media queries below still need
       literal px since CSS custom properties can't be used inside
       @media conditions) --
       --pw-bp-sm   480px  large phone
       --pw-bp-md   768px  tablet — wider content, richer control grouping
       --pw-bp-lg  1024px  desktop — persistent nav + optional aside
       --pw-bp-xl  1440px  large desktop — roomier workspace + aside
    */

    /* -- focus ring -- */
    --pw-focus-ring: 0 0 0 3px rgba(255,176,32,0.45);

    /* ============================================================
       "FUTURE RETAIL OS" VISUAL LANGUAGE (Phase F)
       Additive layer, same rule as Phase B: nothing above is renamed or
       removed. A third depth step for surfaces that sit on top of a
       card (the nav dock, the aside), a translucent hairline distinct
       from --line (which some components still use as a solid
       `:active` tap-feedback fill, so it can't become see-through), and
       a soft glow for "this is currently active/energised" states.
       ============================================================ */
    --pw-surface-3: #23262C;
    --pw-hairline: rgba(255,255,255,0.08);
    --pw-hairline-strong: rgba(255,255,255,0.14);
    --pw-glow-accent: rgba(255,168,61,0.20);
    --pw-pick-transition: 180ms cubic-bezier(.3,.9,.4,1); /* pick, progress, relocate-slide */
    --pw-motion-quick: 160ms ease; /* nav active state, chips/tags, switches */
    --pw-motion-pop: 180ms cubic-bezier(.16,1,.3,1); /* modal/toast entrance */
    --pw-dock-h: 58px; /* content height of the mobile/tablet nav dock, excluding its own safe-area padding */
    --pw-aisle-gap: 28px; /* gutter between two aisle panels while the carousel is mid-swipe */

    /* ---- glass: the material of the persistent chrome ----
       The audit that started this found zero elements with backdrop-filter
       and one translucent background out of 871, which is why "liquid in
       glass" was not visible: there was no glass anywhere. It belongs on the
       surfaces that are always there and always have content moving beneath
       them — the top bar, the summary bar, the dock — and nowhere else.
       Putting it on cards would be the generic glassmorphism the brief asks
       us to avoid, and would cost legibility on every row.

       The alpha is high on purpose, and the dock's is higher than the top
       bar's. Over this near-black page the composite sits very close to the
       solid colour it replaces, so text contrast barely moves; what it buys
       is that amber chips, green ticks and product rows passing underneath
       tint the chrome as they go. That tinting is the effect — the chrome
       reads as a material with the run happening behind it, rather than an
       opaque strip laid on top.

       The dock and summary bar carry small text directly on the surface, so
       they were measured rather than guessed. At 0.72 the dock's muted label
       fell to 2.59:1 against a worst case of solid amber passing beneath —
       under AA, and under 3:1. 0.86 keeps it legible even in that case, which
       is itself pessimistic: a 20px blur averages what is behind, so a chip
       sliding past contributes far less than a solid field of it. */
    --pw-glass-blur: blur(20px) saturate(155%);
    --pw-glass-dock: rgba(35,38,44,0.86);   /* --pw-surface-3 */
    --pw-glass-top: rgba(11,12,13,0.74);    /* --bg */
    --pw-glass-edge: inset 0 1px 0 rgba(255,255,255,0.055);
    /* The settle duration is owned by js (ANIM_MS), which writes it here at
       startup — the rail and the preview build their transitions inline from
       the same number, so a literal repeated in this file would be a second
       copy free to drift. The 260ms below is only a fallback for the instant
       before that runs. */
    --pw-settle: 260ms;
    --pw-settle-ease: cubic-bezier(.22,.61,.36,1);

    /* ============================================================
       SPATIAL DEPTH & TACTILE POLISH
       Perceived depth, not literal 3D: a small, reusable elevation
       ladder built from what Phase F already introduced above
       (--pw-hairline(-strong), --pw-surface-3, --pw-shadow-card/float)
       rather than a parallel token set. Conceptually:
         depth-0  page background            --pw-bg, no border/shadow
         depth-1  operational surface         --pw-surface + --pw-hairline
                  (item rows, inputs — a crisp edge only; near-black
                  doesn't want a shadow repeated down every row)
         depth-2  interactive/elevated control --pw-surface-2 + --pw-hairline-strong
                  + --pw-shadow-card + --pw-inset-highlight (buttons,
                  the product search row, elevated chips)
         depth-3  dock / persistent floating   --pw-surface-3 + --pw-shadow-dock
         depth-4  modal / sheet                --pw-surface-3 + --pw-shadow-float
                  (already exists — named here for completeness)
       Only the pieces that were actually missing are added below. */
    --pw-inset-highlight: inset 0 1px 0 rgba(255,255,255,0.05); /* a surface "catching light" along its top edge — not a gradient, one hairline-thin highlight */
    --pw-shadow-recessed: inset 0 1px 3px rgba(0,0,0,0.4); /* progress tracks, qty well — reads as a groove the fill sits inside rather than a bar painted on top */
    --pw-shadow-dock: 0 -10px 18px -14px rgba(0,0,0,0.55); /* cast upward onto the content just above the dock, not a boxed drop-shadow */
    --pw-press-scale: 0.99;      /* list rows — barely perceptible, never interrupts rapid tapping */
    --pw-press-scale-btn: 0.97;  /* primary buttons/controls — a touch more pronounced, still small */
    --pw-motion-press: 90ms cubic-bezier(.3,.9,.4,1); /* press response — matches --pw-pick-transition's curve, faster */

    /* ============================================================
       PREMIUM FLUID MATERIAL SYSTEM (Phase G)
       Additive, same rule as every layer above: nothing renamed or
       removed. This pass isn't about new shadows — it's about using
       what's already here differently:
         - fewer hard 1px borders around chips/pills/fields. Depth and
           selection read through a material step (this layer's
           --pw-mat-embed, or the existing surface/surface-2/surface-3
           ladder) and typography, not an outline traced around every
           object.
         - "selected/active" stops meaning a 1px amber ring around a
           pill (see section 22 of the brief this implements) — amber
           now shows up as text/icon colour and a soft background wash
           (the existing --amber-dim, already exactly this) with no
           ring on top of it.
         - one more tonal step below --pw-surface for things that should
           read as pressed IN to the material (search fields, the qty
           stepper's track, the pick control at rest) rather than sitting
           on top of it — always paired with an inset shadow, never used
           as a flat fill on its own.
       ============================================================ */
    --pw-mat-embed: #101216; /* recessed/embedded fill — one hair off canvas, always with an inset shadow */
    --pw-motion-glide: 220ms cubic-bezier(.22,.8,.32,1); /* sliding/gliding selection surfaces (segmented control thumb, category indicator) — slower and softer than a press response, this is a material moving, not a tap reacting */
  }

  *:focus-visible{
    outline:2px solid var(--pw-accent);
    outline-offset:2px;
    border-radius:4px;
  }
  @media (prefers-reduced-motion: reduce){
    *, *::before, *::after{
      animation-duration:0.01ms !important;
      animation-iteration-count:1 !important;
      transition-duration:0.01ms !important;
      scroll-behavior:auto !important;
    }
    /* ...with one deliberate exception.

       The blanket above is the right default: it removes every bit of
       travel, spin and slide in the app. But read literally it also removes
       the app's ability to say anything happened at all — switching screens
       becomes an instantaneous swap with no acknowledgement, which is
       disorienting rather than calm, and is not what the setting asks for.
       Reduce Motion means reduce MOTION; a cross-fade is a change of
       opacity, and nothing on screen travels.

       So navigation keeps a short fade. Only opacity — navigateWithTransition
       sets no transform on this path — so there is still nothing moving. */
    #app.pw-crossfade, #pw-screen-preview.pw-crossfade{
      transition-property:opacity !important;
      transition-duration:150ms !important;
      transition-timing-function:ease !important;
    }
  }
  *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
  /* 100% (≈100vh) is the LAYOUT viewport on mobile Safari — taller than
     what's actually visible whenever the address bar is showing, since
     it's sized as if the bar were already hidden. Everything anchored to
     that height (#app's min-height below, and therefore any flex:1 child
     like <main>) would then reserve space for rows of pixels that aren't
     currently on screen, which is exactly what read as "empty black
     space" and as a difference in dock/summary-bar spacing between a
     short (flex-stretched) list and a long (naturally scrolling) one.
     100dvh tracks the CURRENT visible viewport instead, in every browser
     that supports it; 100% stays as the fallback for the few that don't
     (same progressive-enhancement pattern already used for #boot-splash). */
  html,body{height:100%; height:100dvh;}
  body{
    margin:0;
    background:var(--pw-bg);
    color:var(--pw-text);
    font-family:var(--pw-font);
    padding-top:env(safe-area-inset-top);
    /* No padding-bottom here on purpose — it would double-count safe-area
       spacing that's already reserved exactly once, by whichever of these
       actually needs it: .pw-dock (its own height), main (dock + safe-area
       clearance for scrollable content), or .summary-bar (sits above the
       dock). The one screen with neither a dock nor a summary-bar
       (.auth-wrap) accounts for the safe area itself instead. */
    overscroll-behavior-y:none;
  }
  [hidden]{ display:none !important; }
  h1,h2,h3{margin:0; font-weight:800; letter-spacing:0.02em;}
  .eyebrow{
    text-transform:uppercase;
    letter-spacing:0.14em;
    font-size:11px;
    font-weight:700;
    color:var(--muted);
  }
  button{font-family:inherit;}
  #app{max-width:var(--app-max); margin:0 auto; min-height:100%; display:flex; flex-direction:column;}

  /* ---- sticky top (header + progress bar) ---- */
  .sticky-top{
    position:sticky; top:env(safe-area-inset-top); z-index:50;
    background:var(--bg);
  }

  .sticky-top .progress-wrap{ padding:0 16px; margin:10px 0 8px; max-height:90px; transition:margin .2s ease, padding .2s ease, opacity .2s ease, max-height .2s ease; }
  /* Scroll-driven, not scroll-JACKED: applyStickyScrollState() (already
     wired to one passive `scroll` listener — see boot()) just toggles
     this one class past a small threshold. Everything below is a plain
     CSS transition reacting to it — no scroll-position math in CSS, no
     layout recalculated per frame, nothing that could fight iOS
     overscroll or the Aisle swipe (neither touches vertical scroll at
     all). Store/shop/progress collapse into one line (.hdr-compact)
     instead of disappearing, so "which cash & carry, which shop, how
     far through" stays answerable at a glance even mid-scroll. */
  .sticky-top.compact header{ padding:8px 16px; }
  .sticky-top.compact header .eyebrow, .sticky-top.compact header h1{
    display:none;
  }
  .sticky-top.compact .icon-btn{ width:34px; height:34px; }
  .sticky-top.compact .progress-wrap{ max-height:0; margin:0; opacity:0; overflow:hidden; pointer-events:none; }
  .hdr-compact{
    display:none;
    align-items:baseline; gap:10px;
    font-weight:700; font-size:14.5px; color:var(--text);
    white-space:nowrap; overflow:hidden;
  }
  .hdr-compact-name{ overflow:hidden; text-overflow:ellipsis; }
  .hdr-compact-sep{ color:var(--pw-text-muted); font-weight:400; }
  .hdr-compact-progress{
    flex:0 0 auto; color:var(--muted); font-weight:700;
    font-variant-numeric:tabular-nums; font-size:13px;
  }
  .sticky-top.compact .hdr-compact{ display:flex; }

  /* ---- header ---- */
  header{
    padding:18px 16px 12px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:12px;
    border-bottom:1px solid var(--pw-hairline);
    transition:padding .2s ease;
  }
  /* The eyebrow/h1 now carry the active shop/store — real names, not a
     fixed label — so they need room to truncate instead of wrapping
     into the icon buttons on a long "Shop A + Shop B" selection. */
  header .brand{ min-width:0; }
  header .brand-text{ min-width:0; }
  header .brand .eyebrow{
    color:var(--pw-accent-text); transition:font-size .2s ease;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  header h1{
    font-size:22px; text-transform:uppercase; letter-spacing:-0.01em; transition:font-size .2s ease;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .icon-btn{
    position:relative;
    width:var(--pw-tap-min); height:var(--pw-tap-min);
    border-radius:10px;
    border:none;
    background:var(--pw-surface-raised);
    color:var(--text);
    display:flex; align-items:center; justify-content:center;
    transition:width .2s ease, height .2s ease, font-size .2s ease, transform var(--pw-motion-press), background-color var(--pw-motion-press);
  }
  .icon-btn svg{ width:19px; height:19px; }
  .icon-btn:active{background:var(--line); transform:scale(var(--pw-press-scale-btn));}
  /* Share belongs to the header, not beside it — no resting fill of its
     own, just the icon in amber (a meaningful action, not a neutral
     one) picking up a background only on press, the same way a plain
     header icon-button would in a native app chrome. Still the full
     44px target underneath. */
  .hdr-share-btn{ background:none; color:var(--pw-accent-text); }
  .hdr-share-btn:active{ background:var(--pw-mat-embed); }

  /* ---- search bar ---- */
  .search-wrap{ position:relative; padding:8px 16px 0; }
  .search-input{
    width:100%;
    background:var(--pw-mat-embed);
    border:none;
    border-radius:11px;
    padding:8px 32px 8px 11px;
    color:var(--text);
    font-size:13.5px;
    box-shadow:var(--pw-shadow-recessed);
    transition:box-shadow var(--pw-motion-quick);
  }
  .search-input:focus{ outline:none; box-shadow:var(--pw-shadow-recessed), 0 0 0 1.5px var(--pw-hairline-strong); }
  /* Quieter at rest, full strength once you are in it. */
  .search-input:not(:focus){ opacity:0.88; }
  .search-input::placeholder{ color:var(--muted); }
  .search-clear{
    position:absolute; right:22px; top:50%; transform:translateY(-50%);
    color:var(--muted); font-size:16px; font-weight:700; padding:4px 6px;
  }
  /* All / Remaining / Collected is one control, not three. Three separate
     pills each carried their own rim, fill and shadow, so at rest the row
     read as three things you could press rather than one setting with
     three positions — and it sat directly above the Shops and Cash & carry
     strips, which did the same. The track is the object; the segments are
     regions of it, and only the chosen one is lit. Deliberately the same
     shape and depth as .ovs-track below, because List/Aisle is the same
     kind of choice and the two should not look like different mechanisms. */
  .filter-chips{
    display:flex;
    margin:7px 16px 0;
    padding:3px;
    border-radius:var(--pw-radius-pill);
    background:var(--pw-mat-embed);
    box-shadow:var(--pw-shadow-recessed);
  }
  .filter-chip{
    flex:1;
    border:none;
    background:none;
    color:var(--muted);
    font-size:12px; font-weight:700;
    padding:6px 11px;
    border-radius:var(--pw-radius-pill);
    transition:color var(--pw-motion-quick), background-color var(--pw-motion-quick), box-shadow var(--pw-motion-quick);
  }
  /* The lit segment: a raised lens with the accent burning inside it,
     matching .ovs-thumb so both segmented controls read as one idea. */
  .filter-chip.on{
    color:var(--pw-accent-text);
    background-color:var(--pw-surface-raised);
    background-image:var(--pw-g-seg-wash), var(--pw-g-spec);
    box-shadow:var(--pw-g-seg-lens);
  }

  /* ---- store tabs (compact selector chips — Refinement: strong
     selected state, minimal visual weight at rest; no dashed border,
     no bullet dot) ---- */
  /* The scrollbar is deliberately hidden here, which left horizontal
     overflow with no affordance at all: at 390px the last chip simply
     clipped mid-word at the viewport edge, reading as broken rather than
     as "there is more this way".

     The trailing fade is a paint-only mask — no blur, no glass, no extra
     element — so it cannot interfere with the horizontal gesture this
     strip relies on (.store-tabs is already excluded from the page-swipe
     guard, see attachSwipe). The extra right padding is what keeps it from
     obscuring anything: it reserves a strip of empty space after the last
     chip, so when the content fits, the fade falls entirely on that empty
     space and is invisible, and when the strip is scrolled fully to the
     end, the fade lands on the padding rather than on the final chip. It
     is visible only in the one state where it means something: more chips
     off-screen. */
  .store-tabs{
    display:flex; gap:6px; overflow-x:auto;
    padding:8px 40px 4px 16px;
    scrollbar-width:none;
    -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 26px), transparent 100%);
    mask-image:linear-gradient(to right, #000 calc(100% - 26px), transparent 100%);
  }
  .store-tabs::-webkit-scrollbar{display:none;}
  .tag{
    flex:0 0 auto;
    position:relative;
    padding:6px 12px;
    border-radius:var(--pw-radius-pill);
    background:var(--surface-2);
    border:1px solid transparent;
    color:var(--muted);
    font-weight:600;
    font-size:12.5px;
    white-space:nowrap;
    transition:background-color var(--pw-motion-quick), color var(--pw-motion-quick), box-shadow var(--pw-motion-quick);
  }
  .tag.active{
    background:var(--amber-dim);
    color:var(--pw-accent-text);
    font-weight:700;
  }

  /* ---- main ---- */
  main{flex:1; padding:6px 16px calc(100px + var(--pw-dock-h) + env(safe-area-inset-bottom));}
  /* Admin is the one screen this generic dock-aware padding is wrong
     for — the dock hides itself there, so reserving its height (plus
     the 100px meant for a summary-bar that also never appears here)
     would just be dead space at the bottom. */
  main.admin-main{ padding-bottom:calc(24px + env(safe-area-inset-bottom)); }
  /* Order's one screen with a dock but no summary-bar (nothing set up
     yet) — clears the dock without the extra ~100px main otherwise
     reserves for a summary-bar that isn't there. */
  main.no-summary-bar{ padding-bottom:calc(24px + var(--pw-dock-h) + env(safe-area-inset-bottom)); }
  .empty{
    margin-top:60px;
    text-align:center;
    color:var(--muted);
    padding:0 20px;
  }
  .empty h3{color:var(--text); font-size:17px; margin-bottom:8px;}
  .empty button{margin-top:16px;}

  .section-label{
    font-size:11.5px; font-weight:700; letter-spacing:0.1em;
    text-transform:uppercase; color:var(--muted);
    margin:16px 0 6px;
    display:flex; justify-content:space-between; align-items:center;
  }
  .section-label span.count{color:var(--text); background:var(--surface-2); padding:2px 8px; border-radius:20px; font-size:11px;}
  .category-label{
    font-size:11px; font-weight:700; letter-spacing:0.08em;
    text-transform:uppercase; color:var(--pw-category);
    margin:10px 0 4px;
    display:flex; align-items:center; gap:8px;
    padding:5px 4px;
    border-radius:6px;
  }
  .category-label:active{background:var(--surface);}
  .category-label .cat-name{flex:1; min-width:0;}
  .category-label .cat-count{
    background:var(--surface-2); color:var(--muted);
    padding:2px 8px; border-radius:20px; font-size:10px; font-weight:700;
    letter-spacing:0; font-variant-numeric:tabular-nums;
  }
  .category-label .cat-arrow{
    font-size:10px; color:var(--muted);
    transition:transform 0.15s ease;
    display:inline-block;
  }
  .category-label.collapsed .cat-arrow{transform:rotate(-90deg);}
  .category-label:first-of-type{margin-top:6px;}
  .collapse-all{
    display:flex; justify-content:flex-end; padding:2px 0 0;
  }
  /* A quiet utility, not a primary action — amber is reserved for
     things that move the run forward; this just tidies the view. */
  .collapse-all button{
    background:none; border:none; color:var(--pw-text-muted);
    font-size:12px; font-weight:600; padding:6px 4px;
  }
  /* Shared minor-action treatment — "Clear" on the Add Product draft
     and anywhere else a secondary text action needs to sit quietly
     under a primary button rather than matching its full-width weight. */
  .quiet-link{
    background:none; border:none; color:var(--pw-text-muted);
    font-size:13px; font-weight:600; padding:6px 10px;
  }
  .quiet-link:active{ color:var(--text); }

  /* ---- item row: an operational list item, not a bordered card. A
     hairline under each row is the only separator; the row itself has
     no background/border box of its own, so a long list reads as one
     continuous surface rather than a stack of tiles. */
  .item-row{
    display:flex;
    align-items:center;
    gap:8px;
    background:transparent;
    border:none;
    border-bottom:1px solid var(--pw-hairline);
    border-radius:0;
    padding:8px 4px 8px 8px;
    margin-bottom:0;
    transition:transform var(--pw-motion-press);
  }
  /* Recedes so outstanding items dominate the screen, but "completed"
     was never meant to mean "hard to read" — reviewing what you've
     already collected is a real thing people do mid-run. Dimming now
     targets the name/qty only (opacity .2s ease, unchanged) and stops
     well short of the old 0.55: still clearly de-emphasised next to an
     unpicked row, still comfortably legible on its own. The check stays
     at full opacity — its green fill is the strongest completion signal
     in the row and dimming it only muddied that. */
  .item-row.picked .item-info,
  .item-row.picked .qty-controls{
    opacity:0.78;
    transition:opacity .2s ease;
  }
  /* Barely-there compression on touch — a physical control responding,
     not an HTML row being clicked. Row-level so it fires from a tap
     anywhere on the row, not just the checkbox. Left unguarded by
     :not(.picked) etc. on purpose — the FLIP relocate in app.js only
     ever touches row.style.transform ~90ms after the tap has already
     resolved (see relocateRowInPlace), well after :active has ended
     on a normal tap, and inline styles win over this rule regardless. */
  .item-row:active{ transform:scale(var(--pw-press-scale)); }
  .check{
    /* The single most-tapped control in the whole app — every item,
       every run — which is exactly why it's sized to var(--pw-tap-compact)
       rather than the one-off 44px used for header/settings buttons:
       at list-row density, repeated 44px circles read as oversized, not
       more accessible. Still a full 34px circular target.
       At rest it's a soft recessed dimple pressed INTO the row's
       material — no drawn stroke — rather than an outline circle
       waiting to be filled in; picking it fills that dimple with the
       success surface. */
    width:var(--pw-tap-compact); height:var(--pw-tap-compact);
    border-radius:50%;
    border:none;
    background:var(--pw-mat-embed);
    box-shadow:inset 0 1px 3px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.05);
    flex:0 0 auto;
    display:flex; align-items:center; justify-content:center;
    font-size:13px; font-weight:900;
    color:transparent;
    cursor:pointer;
    transition:background-color var(--pw-pick-transition), box-shadow var(--pw-pick-transition), transform var(--pw-pick-transition), color 120ms ease;
  }
  /* Instant tactile feedback on touch itself — separate from (and faster
     to *start* than) the picked-state fill above, so a tap reads as an
     immediate response rather than something that only appears once
     JS/render catches up. */
  .check:active{ transform:scale(0.88); }
  .item-row.picked .check{
    background:var(--green);
    box-shadow:0 1px 2px rgba(0,0,0,0.3);
    color:#0F1B14;
    transform:scale(1.06);
  }
  .item-info{flex:1; min-width:0;}
  .item-name{font-size:14.5px; font-weight:650; letter-spacing:-0.005em; line-height:1.25; transition:color .2s ease, opacity .2s ease;}
  .item-row.picked .item-name{color:var(--muted); text-decoration:line-through;}
  .item-unit{font-size:11.5px; color:var(--muted); margin-top:1px;}
  /* One joined, hardware-quality stepper — a single recessed track
     holding minus/value/plus, not three independent square buttons
     sitting next to each other. Same .qty-btn/.qty-val/.qty-input DOM
     app.js already renders; only the container now carries the material
     and the children go transparent inside it. */
  .qty-controls{
    display:flex; align-items:center; gap:0; flex:0 0 auto;
    background:var(--pw-mat-embed);
    border-radius:var(--pw-radius-pill);
    box-shadow:var(--pw-shadow-recessed);
  }
  .qty-btn{
    /* Full var(--pw-tap-compact) hit target, same as .check — the pill
       look comes from the shared container behind these, not from
       shrinking the buttons themselves; each stays transparent until
       pressed, so the whole thing reads as one joined stepper rather
       than three separate boxes. */
    width:var(--pw-tap-compact); height:var(--pw-tap-compact);
    border-radius:50%;
    border:none;
    background:transparent;
    color:var(--text);
    font-size:15px;
    font-weight:700;
    display:flex; align-items:center; justify-content:center;
    transition:background-color var(--pw-motion-press), transform var(--pw-motion-press);
  }
  .qty-btn:active{background:var(--pw-surface-raised); transform:scale(0.94);}
  .qty-val{
    width:28px;
    text-align:center;
    font-weight:800;
    font-size:14px;
    font-variant-numeric:tabular-nums;
  }
  .qty-input{
    width:28px;
    text-align:center;
    font-weight:800;
    font-size:14px;
    font-variant-numeric:tabular-nums;
    background:transparent;
    border:none;
    color:var(--text);
    font-family:inherit;
    padding:0;
  }
  .qty-input:focus{outline:none;}
  .qty-input::-webkit-outer-spin-button,
  .qty-input::-webkit-inner-spin-button{-webkit-appearance:none; margin:0;}
  /* A number changing instantly with nothing else happening read as an
     abrupt state swap rather than a control responding — this is the
     "settle" half of press→settle, restarted on every tap (see
     applyQtyInPlace in app.js). Colour, not scale: a number visibly
     growing/shrinking next to fixed +/- buttons looked like layout
     jitter, not tactile response. */
  .qty-input.qty-bump{ animation:pw-qty-bump 220ms ease; }
  @keyframes pw-qty-bump{ from{ color:var(--pw-accent-text); } to{ color:var(--text); } }

  /* ---- floating summary bar ---- */
  /* summary-bar + .pw-dock together are ONE bottom command surface with
     two hierarchy levels (section 15), not two stacked bars — same
     material (surface-3), contiguous with no seam between them, and a
     single shadow cast from the true outer edge (summary-bar's top).
     The dock below carries no border/shadow of its own any more — see
     its rule further down. */
  .summary-bar{
    position:fixed;
    /* Sits directly above the mobile/tablet nav dock (hidden ≥1024px,
       where this is reset back to 0 — see the 1024px block below). */
    bottom:calc(var(--pw-dock-h) + env(safe-area-inset-bottom));
    left:0; right:0;
    max-width:var(--app-max);
    margin:0 auto;
    padding:10px 16px;
    background:var(--pw-surface-3);
    border-top:1px solid var(--pw-hairline-strong);
    box-shadow:var(--pw-shadow-dock), var(--pw-inset-highlight);
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:8px;
  }
  .summary-bar .stat{font-size:13px; color:var(--muted); flex:0 0 auto; font-variant-numeric:tabular-nums;}
  .summary-bar .stat b{color:var(--text); font-size:15px;}
  .summary-bar .actions{display:flex; gap:6px;}
  .summary-bar .btn{padding:10px 10px; font-size:12.5px; white-space:nowrap;}
  /* ---- progress bar (kept linear per approved Refinement 1 — a small
     ring supplements this in the desktop aside only; this thin bar +
     count + percentage stays the primary mobile run cue) ----
     This is the one number that matters most during a run, so it gets
     the biggest, boldest text on the whole sticky header — everything
     else here is secondary to "how many, and what's left". */
  .progress-wrap{ margin:16px 0 2px; }
  .progress-label{ display:flex; justify-content:space-between; align-items:baseline; font-size:13.5px; color:var(--muted); margin-bottom:7px; }
  .progress-label b{ color:var(--text); font-size:15px; font-variant-numeric:tabular-nums; }
  .progress-pct{ font-weight:800; color:var(--text); font-size:15px; font-variant-numeric:tabular-nums; transition:color var(--pw-pick-transition); }
  .progress-pct.done{ color:var(--pw-success-text); }
  .progress-track{ height:3px; border-radius:2px; background:var(--surface-2); overflow:hidden; box-shadow:inset 0 1px 1px rgba(0,0,0,0.45); }
  .progress-fill{ height:100%; background:var(--amber); border-radius:2px; transition:width var(--pw-pick-transition), background-color var(--pw-pick-transition); }
  .progress-fill.done{ background:var(--green); }
  .run-complete-banner{
    display:flex; align-items:center; gap:8px;
    background:var(--pw-success-soft); color:var(--pw-success-text);
    border:none; box-shadow:inset 0 0 0 1px var(--pw-success); border-radius:var(--pw-radius-md);
    padding:12px 14px; margin:4px 0 16px;
    font-size:14px; font-weight:700;
  }

  /* ---- Order view switcher (List / Aisle) ----
     One embedded track with a single material "thumb" that glides
     between the two positions via transform — not two buttons each
     independently flipping their own background/border. The thumb is a
     plain sibling <div> (see renderOrder in app.js), purely
     presentational: which position it's in is driven by the
     .order-view-switcher.is-aisle class, set from the same
     orderViewMode the buttons' own .on classes already read from, so
     the two can never disagree. Respects reduced-motion for free — the
     glide is just a transition, covered by the app's blanket override. */
  .order-view-switcher{
    position:relative;
    display:flex;
    padding:8px 16px 0;
  }
  .ovs-track{
    position:relative;
    flex:1; display:flex;
    background:var(--pw-mat-embed);
    border-radius:var(--pw-radius-pill);
    box-shadow:var(--pw-shadow-recessed);
    padding:3px;
  }
  .ovs-thumb{
    position:absolute;
    top:3px; left:3px;
    width:calc(50% - 3px); height:calc(100% - 6px);
    background-color:var(--pw-surface-raised);
    background-image:var(--pw-g-seg-wash), var(--pw-g-spec);
    border-radius:var(--pw-radius-pill);
    box-shadow:var(--pw-g-seg-lens);
    transition:transform var(--pw-motion-glide);
  }
  .order-view-switcher.is-aisle .ovs-thumb{ transform:translateX(100%); }
  .ovs-btn{
    position:relative; z-index:1;
    flex:1;
    display:flex; align-items:center; justify-content:center; gap:6px;
    background:none;
    border:none;
    color:var(--muted);
    font-size:12.5px; font-weight:700;
    padding:7px 10px;
    border-radius:var(--pw-radius-pill);
    transition:color var(--pw-motion-quick), transform var(--pw-motion-press);
  }
  .ovs-ic{ width:15px; height:15px; flex:0 0 auto; }
  .ovs-btn.on{ color:var(--pw-accent-text); }
  .ovs-btn:active{ transform:scale(var(--pw-press-scale-btn)); }

  /* ---- Aisle View ----
     One category from the existing order, worked through at a time.
     Reuses .item-row/.check/.qty-btn exactly as List View renders them
     (see itemRowHtml() in app.js) — only the chrome around the rows is
     new here. */
  /* A rail, not a row of badges — inactive categories are just muted
     type with generous spacing between them; the active one gets a
     soft material wash, not every one getting its own bordered pill.
     The edge mask is the overflow affordance (section 8): a category
     partly cut off at either edge visibly fades rather than stopping
     with a hard clip, which is what actually communicates "there's
     more this way" — a plain overflow:auto strip doesn't say that on
     its own. */
  .aisle-strip{
    display:flex; gap:22px; overflow-x:auto;
    /* Full-bleed to the true screen edge (main's own 16px gutter is
       cancelled here on purpose) so the edge fade below reads against
       the actual edge of the screen, the way a native horizontal rail
       does, rather than fading a few pixels early against empty gutter. */
    margin:0 -16px; padding:8px 16px 4px;
    scrollbar-width:none;
    -webkit-mask-image:linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
    mask-image:linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
  }
  .aisle-strip::-webkit-scrollbar{ display:none; }
  .aisle-chip{
    flex:0 0 auto;
    padding:6px 4px;
    border-radius:var(--pw-radius-sm);
    background:none;
    border:none;
    color:var(--pw-text-muted);
    font-weight:600;
    font-size:12.5px;
    white-space:nowrap;
    transition:color var(--pw-motion-quick), transform var(--pw-motion-press);
  }
  .aisle-chip.on{ color:var(--pw-accent-text); font-weight:800; font-size:13.5px; }
  .aisle-chip.done:not(.on){ color:var(--pw-success-text); }
  .aisle-chip:active{ transform:scale(var(--pw-press-scale-btn)); }

  /* The active category is the composition's centre of gravity — large,
     confident type, nothing boxed around it. Previous/next stay
     discoverable but visually secondary: plain chevrons with no
     background of their own at rest, so the name is what the eye lands
     on first (section 7 of the brief). */
  .aisle-nav{ display:flex; align-items:center; gap:4px; padding:8px 0 0; }
  .aisle-arrow{
    flex:0 0 auto;
    width:var(--pw-tap-min); height:var(--pw-tap-min);
    border-radius:50%;
    background:none;
    border:none;
    color:var(--pw-text-muted);
    display:flex; align-items:center; justify-content:center;
    transition:background-color var(--pw-motion-press), color var(--pw-motion-quick), transform var(--pw-motion-press), opacity var(--pw-motion-quick);
  }
  .aisle-arrow svg{ width:20px; height:20px; }
  .aisle-arrow:active{ background:var(--pw-mat-embed); color:var(--text); transform:scale(var(--pw-press-scale-btn)); }
  .aisle-arrow:disabled{ opacity:0.25; }
  .aisle-current{ flex:1; min-width:0; text-align:center; padding:0 4px; }
  .aisle-cat-name{
    font-size:26px; font-weight:800; color:var(--text);
    letter-spacing:-0.01em; line-height:1.1;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .aisle-cat-progress{ font-size:12.5px; color:var(--muted); margin-top:5px; font-variant-numeric:tabular-nums; }
  /* A short, local progress thread directly under the category name —
     its own read on "how far through this aisle", distinct from (and a
     lot smaller than) the page's overall progress bar above. */
  .aisle-mini-track{
    width:88px; height:3px; margin:7px auto 0; border-radius:2px;
    background:var(--pw-mat-embed); box-shadow:var(--pw-shadow-recessed);
    overflow:hidden;
  }
  .aisle-mini-fill{ height:100%; background:var(--amber); border-radius:2px; transition:width var(--pw-pick-transition), background-color var(--pw-pick-transition); }
  .aisle-mini-fill.done{ background:var(--green); }
  .aisle-position{ text-align:center; font-size:11.5px; color:var(--pw-text-muted); margin:14px 0 0; font-variant-numeric:tabular-nums; }

  /* Completion is a small, precise moment — a check and a line of green
     text, not a full-width bordered rectangle (section 23). The "Next
     aisle" step stays a quiet inline link, never automatic. */
  .aisle-complete-banner{
    display:flex; align-items:center; justify-content:center; gap:10px;
    background:none; border:none; box-shadow:none;
    padding:10px 6px 2px;
    font-size:13.5px; font-weight:700;
    color:var(--pw-success-text);
  }
  .aisle-complete-banner svg{ width:15px; height:15px; flex:0 0 auto; }
  .aisle-next-btn{
    flex:0 0 auto;
    background:none; border:none; color:var(--pw-success-text);
    font-weight:800; font-size:13px;
    text-decoration:underline; text-underline-offset:2px;
  }

  /* The workspace itself: rows sit directly on the page material, no
     bordered card holding them — spacing and the item-row hairlines
     already carry the structure (section 7: "product rows should feel
     like part of the workspace surface rather than rows inside a
     bordered card"). */
  /* The track clips the rail and owns the horizontal axis.

     touch-action:pan-y is the load-bearing line: it tells the browser to keep
     handling vertical scrolling natively and to stop interpreting horizontal
     drags itself. That hands the horizontal axis to JS WITHOUT any listener
     needing preventDefault, so every touch listener on this screen stays
     passive. Cancelling a native gesture with a non-passive preventDefault is
     precisely what made the product-lookup panel unscrollable on iOS; this
     avoids that whole class of bug rather than re-navigating it. */
  /* The incoming screen during a swipe. Inert by construction: it is never
     bound, never interactive, and carries no ids. Fixed rather than in flow
     because navigation lands at the top of a screen anyway, so the top is
     exactly what should be arriving. */
  #pw-screen-preview{
    position:fixed;
    top:0; left:0; right:0; bottom:0;
    overflow:hidden;
    background:var(--pw-bg);
    pointer-events:none;
    z-index:5;
    will-change:transform;
  }
  /* will-change ONLY while a swipe is actually in progress.
     As a permanent declaration this was a live bug, not an optimisation.
     will-change:transform makes an element a containing block for its
     position:fixed descendants, and .summary-bar — "N / M picked", PDF,
     Finish run, Clear — is one of them. Measured on v1.19.0 at 390x844:
     the bar resolved against #app's 8873px-tall box instead of the viewport
     and sat at y=8758, off screen at every scroll position. On the commit
     before the swipe layer landed it sat at y=729 and stayed there. So the
     Order screen's main command surface was being positioned off the bottom
     of the document for the whole of v1.19.0.

     Scoped to the gesture, the hint is also doing what a hint is for:
     announcing a transform that is about to happen, rather than holding a
     compositor layer for the entire app for the entire session. */
  #app.pw-swiping{ will-change:transform; }
  .aisle-track{
    margin:14px 0 0;
    overflow:hidden;
    touch-action:pan-y;
    /* The arrows are laid over the panel head, so they need an origin. */
    position:relative;
  }
  /* Fixed chrome over a moving surface: the chevrons stay exactly where they
     have always been while the aisle's own heading travels beneath them. */
  .aisle-arrow-prev, .aisle-arrow-next{
    position:absolute;
    top:0;
    z-index:2;
  }
  .aisle-arrow-prev{ left:0; }
  .aisle-arrow-next{ right:0; }
  /* ...but "beneath them" was the artifact. Measured mid-drag, a travelling
     category name overlapped each chevron by 44px across and 28px down, and a
     chevron has no background of its own (deliberately — section 7 wants the
     name to be what the eye lands on). So a glyph sat directly on top of
     moving words, which is exactly the "pieces of DOM" read.

     The chevrons are chrome for the screen at rest: they exist to offer a tap
     to someone not dragging. While a finger IS dragging, the gesture is doing
     their job, so they get out of the way of the surface they point at.
     Opacity only — no layout, no reflow, nothing that could disturb the rail's
     1:1 tracking — and pointer-events go with them so a release cannot land on
     a chevron that has faded out. Under prefers-reduced-motion the blanket
     override at the top of this file makes it an instant swap, which is the
     correct behaviour rather than a degraded one. */
  .aisle-track.dragging .aisle-arrow-prev,
  .aisle-track.dragging .aisle-arrow-next{
    opacity:0;
    pointer-events:none;
  }
  /* The aisle's identity, now part of the panel rather than stationary above
     it. Padded clear of the chevrons so the title never sits under one. */
  .aisle-panel-head{
    padding:0 46px 10px;
    text-align:center;
  }
  .aisle-panel-rows{ min-width:0; }
  /* The rail is the only thing that moves. transform on its own compositor
     layer — no width/left animation, nothing that forces layout per frame. */
  .aisle-rail{
    display:flex;
    align-items:flex-start;
    will-change:transform;
    /* The gutter between two aisles. Without it the panels abutted at exactly
       one pixel column: a product name cut off mid-word sat immediately
       against a whole product name from the next aisle, and the two sheets
       read as one torn surface rather than two.

       Wider than the 16px page gutter on purpose, so the space reads as
       "between two sheets" and not as "the edge of the page". The rail's step
       is therefore a panel plus this gap, and app.js reads the used value back
       off the element rather than keeping its own copy of the number — this
       declaration stays the single place it is decided. */
    gap:var(--pw-aisle-gap);
  }
  /* Each panel is exactly one track wide, so a neighbour sits precisely
     off-screen (plus the gutter) until the drag brings it in. */
  .aisle-rail > .aisle-panel{ flex:0 0 100%; min-width:0; }
  .aisle-panel .item-row:last-child{ border-bottom:none; }
  /* A fresh .aisle-panel node is swapped in on every category change
     (see renderAisleMain in app.js) — animation, not transition, is
     what plays on a newly-inserted node. Quick, non-directional fade +
     tiny rise: reinforces "a new aisle just arrived" without carousel
     theatrics, and collapses to nothing under prefers-reduced-motion
     via the same blanket animation-duration override every other
     animation in the app already obeys (see the top of this file). */
  /* No entrance animation by default any more. With the carousel the movement
     itself is the feedback: a neighbour built mid-drag must not fade in while
     the finger is dragging it, and the panel that render() produces after a
     commit must not replay anything — the rail has already carried it into
     place, so a fade there would read as a flash. The directional classes
     below remain for the fallback path, where the rail is unavailable and the
     change still has to show a direction. */
  .aisle-panel{ }
  /* Moving between aisles travels sideways, in the direction you went: swipe
     left for the next aisle and it arrives from the right, and the reverse
     going back. The plain fade above still covers repaints that are not a
     move — a pick, a background sync, first paint — so only an actual change
     of aisle slides. Entrance only: render() replaces the panel outright, so
     there is no outgoing node left to animate out. Both collapse under
     prefers-reduced-motion via the blanket override at the top of this file. */
  .aisle-panel.enter-next{ animation:pw-aisle-in-next var(--pw-pick-transition); }
  .aisle-panel.enter-prev{ animation:pw-aisle-in-prev var(--pw-pick-transition); }
  @keyframes pw-aisle-in-next{
    from{ opacity:0; transform:translateX(24px); }
    to{ opacity:1; transform:translateX(0); }
  }
  @keyframes pw-aisle-in-prev{
    from{ opacity:0; transform:translateX(-24px); }
    to{ opacity:1; transform:translateX(0); }
  }
  @keyframes pw-aisle-enter{
    from{ opacity:0; transform:translateY(6px); }
    to{ opacity:1; transform:translateY(0); }
  }

  /* ---- mobile/tablet bottom nav dock ----
     Compact and operational, not a floating glass slab: fixed height,
     flush with the viewport edge, one hairline top border. Hidden
     entirely ≥1024px (see that media block) where the sidenav already
     covers the same five destinations — the two never both render. */
  /* On screens without a summary-bar (everywhere except a populated
     Order), the dock is the only thing down here and the colour step
     against the page above it already reads as a distinct surface — no
     border/shadow needed to say so. On Order, it sits directly under
     the summary-bar with the same background and no seam between them,
     so the two read as one bottom command surface with two hierarchy
     levels (section 15) rather than two independently-edged bars; the
     single shadow/top-edge for that whole assembly lives on
     .summary-bar above, since that's its true outer boundary. */
  .pw-dock{
    display:flex; align-items:stretch; justify-content:space-around;
    position:fixed; left:0; right:0; bottom:0; z-index:55;
    height:calc(var(--pw-dock-h) + env(safe-area-inset-bottom));
    padding-bottom:env(safe-area-inset-bottom);
    max-width:var(--app-max); margin:0 auto;
    background:var(--pw-surface-3);
  }
  .pw-dock-item{
    flex:1; min-width:0;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:3px;
    background:none; border:none;
    color:var(--muted);
    font-size:10.5px; font-weight:600;
    position:relative;
    transition:color var(--pw-motion-quick);
  }
  .pw-dock-ic{ width:20px; height:20px; display:flex; position:relative; }
  .pw-dock-ic svg{ width:100%; height:100%; }
  .pw-dock-label{ line-height:1; }
  .pw-dock-item.on{ color:var(--pw-accent-text); }
  /* Same "something needs checking" dot the old header gear used to
     carry, now on the dock's Settings icon since that's the only way
     to reach Settings. */
  .pw-dock-item.has-sync-issue .pw-dock-ic::after{
    content:"";
    position:absolute; top:-2px; right:-2px;
    width:7px; height:7px; border-radius:50%;
    background:var(--pw-danger);
    border:1.5px solid var(--pw-surface-3);
  }
  /* The glow now exists on every item and is revealed by opacity, rather
     than existing only on the active one. Same resting appearance — 0 on the
     others, 1 on .on — but it gives the swipe something continuous to move,
     which a present/absent pseudo-element cannot offer. */
  .pw-dock-ic::before{
    content:""; position:absolute; top:-3px; left:50%; transform:translateX(-50%);
    width:30px; height:30px; border-radius:50%;
    background:radial-gradient(circle, var(--pw-glow-accent) 0%, transparent 72%);
    z-index:-1;
    opacity:0;
    transition:opacity var(--pw-motion-quick);
  }
  .pw-dock-item.on .pw-dock-ic::before{ opacity:1; }

  /* ---- the indicator while a screen swipe is in progress ----

     The dock itself never moves: it is fixed furniture, and sliding it would
     say the whole app is travelling when only the content is. What moves is
     the emphasis, handed from the screen you are leaving to the one you are
     pulling in, in step with how far across the destination actually is.

     --pw-emph is that hand-off, 0..1, written per item by the gesture. Both
     the colour and the glow read from it, so they cannot drift apart.

     Two things deliberately do NOT change mid-drag. aria-current stays on the
     origin until the navigation actually happens — a screen reader must not
     be told you are on a page you have not arrived at, and a half-finished
     drag is not a page change. And .on stays where it is, so if the gesture
     is cancelled there is nothing to put back.

     color-mix is the one modern feature here. Where it is unsupported the
     whole declaration is dropped and the discrete .on colour underneath still
     applies, which is exactly today's behaviour — so this degrades to "the
     indicator changes at commit" rather than to anything broken. */
  .pw-dock.pw-dock-tracking .pw-dock-item{
    color:color-mix(in oklab, var(--amber) calc(var(--pw-emph, 0) * 100%), var(--muted));
    transition:none;
  }
  .pw-dock.pw-dock-tracking .pw-dock-item .pw-dock-ic::before{
    opacity:var(--pw-emph, 0);
    transition:none;
  }
  /* The settle is the one part that animates: on release the emphasis runs
     the rest of the way it was going, at the same speed the screens do. */
  .pw-dock.pw-dock-tracking.pw-dock-settling .pw-dock-item,
  .pw-dock.pw-dock-tracking.pw-dock-settling .pw-dock-item .pw-dock-ic::before{
    transition:color var(--pw-settle, 260ms) var(--pw-settle-ease), opacity var(--pw-settle, 260ms) var(--pw-settle-ease);
  }

  /* The glass itself, applied only where a surface is permanent and has the
     run moving underneath it. Guarded: without backdrop-filter support the
     solid backgrounds declared above and below stand, which is exactly
     today's appearance rather than a broken one. Both the prefixed and
     unprefixed properties are set — iOS Safari wanted the prefix for a long
     time and plenty of installed phones still do. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
    /* The blur lives on a pseudo-element BEHIND the bar's content, not on the
       bar itself.

       Reported from a real iPhone: the text across the top of the Order screen
       looked soft. It is a WebKit compositing behaviour rather than anything
       wrong with the type — an element carrying `backdrop-filter` is promoted
       to its own layer, and text rendered into that layer loses subpixel
       antialiasing and comes out slightly blurred. The header, the search box,
       the filter switch and the progress figures all sit inside `.sticky-top`,
       so all of them were being composited that way; the dock's labels and the
       summary bar's count had exactly the same problem at the other end of the
       screen.

       Putting the blur on `::before` means only the pseudo-element is promoted.
       The frosted appearance is unchanged — same colour, same blur radius, same
       saturation, same stacking order — but the text is composited normally and
       renders sharp. Chromium never had the defect, so in a headless render this
       change is a pixel-for-pixel no-op; that is the check it has to pass. */
    .sticky-top, .pw-dock, .summary-bar{
      background:transparent;
    }
    .sticky-top::before, .pw-dock::before, .summary-bar::before{
      content:"";
      position:absolute;
      inset:0;
      z-index:-1;
      pointer-events:none;
      -webkit-backdrop-filter:var(--pw-glass-blur);
      backdrop-filter:var(--pw-glass-blur);
    }
    /* Each bar keeps its own tint; only the blur is shared. */
    .sticky-top::before{ background:var(--pw-glass-top); }
    .pw-dock::before,
    .summary-bar::before{ background:var(--pw-glass-dock); }
    /* The dock's lit top edge stays on the element: it is a border, not part
       of the backdrop, and moving it would put it behind the labels. */
    .pw-dock{ box-shadow:var(--pw-glass-edge); }
    /* The summary bar's top hairline needs moving too, and the reason is worth
       stating because two obvious fixes are both wrong.

       `inset:0` spans the PADDING box while an element's own background paints
       across the BORDER box, so with the tint on the pseudo the translucent
       hairline (--pw-hairline-strong) stopped compositing over frosted glass
       and started compositing over the bare list behind it — that row went from
       light grey to near-black. Extending the pseudo to `top:-1px` then hid the
       hairline completely, because a negative-z-index pseudo paints AFTER its
       element's background and border, not before them.

       So the hairline moves onto the pseudo as well. The element keeps the
       border for layout and makes it transparent; the pseudo spans the border
       box and draws the visible line, over the glass, where it always was. */
    .summary-bar{ border-top-color:transparent; }
    .summary-bar::before{
      top:-1px;
      box-sizing:border-box;
      border-top:1px solid var(--pw-hairline-strong);
    }
  }

  /* ---- history: mobile timeline (desktop keeps the table below) ----
     Colour AND shape/magnitude both carry completion — the node colour
     (green/amber/neutral) plus the fill bar's length, never colour alone. */
  .hist-timeline{ position:relative; }
  .hist-timeline::before{
    content:""; position:absolute; left:15px; top:4px; bottom:4px; width:1px;
    background:var(--pw-hairline-strong);
  }
  .hist-item{ display:flex; gap:12px; padding:0 4px 18px 4px; position:relative; }
  .hist-item:last-child{ padding-bottom:2px; }
  .hist-node{
    width:12px; height:12px; border-radius:50%; flex:0 0 auto; margin-top:5px;
    position:relative; z-index:1;
    box-shadow:0 0 0 3px var(--bg);
  }
  .hist-node.done{ background:var(--green); }
  .hist-node.partial{ background:var(--amber); }
  .hist-node.zero{ background:var(--pw-surface-3); box-shadow:0 0 0 3px var(--bg), inset 0 0 0 1.5px var(--pw-border-strong); }
  .hist-body{ flex:1; min-width:0; }
  .hist-top{ display:flex; justify-content:space-between; align-items:baseline; gap:10px; }
  .hist-store{ font-size:14.5px; font-weight:700; }
  .hist-pct{ font-size:12.5px; font-weight:800; font-variant-numeric:tabular-nums; flex:0 0 auto; white-space:nowrap; }
  .hist-pct.done{ color:var(--pw-success-text); }
  .hist-pct.partial{ color:var(--pw-accent-text); }
  .hist-pct.zero{ color:var(--muted); }
  .hist-meta{ font-size:12px; color:var(--muted); margin-top:2px; }
  .hist-bar{ height:3px; border-radius:2px; background:var(--surface-2); overflow:hidden; margin-top:9px; max-width:220px; box-shadow:inset 0 1px 1px rgba(0,0,0,0.45); }
  .hist-fill{ height:100%; border-radius:2px; }
  .hist-fill.done{ background:var(--green); }
  .hist-fill.partial{ background:var(--amber); }
  .hist-fill.zero{ background:var(--pw-border-strong); }
  /* A proper badge for the desktop table's completion column — same
     visual language as everywhere else "done" is communicated. */
  .hist-badge{
    flex:0 0 auto; font-size:12px; font-weight:700;
    padding:3px 10px; border-radius:var(--pw-radius-pill);
    font-variant-numeric:tabular-nums;
  }
  .hist-badge.done{ background:var(--pw-success-soft); color:var(--pw-success-text); }
  .hist-badge.partial{ background:var(--pw-accent-soft); color:var(--pw-accent-text); }
  .hist-badge.zero{ background:var(--pw-surface-3); color:var(--muted); }

  /* Table only appears >=1024px (see the desktop media query below);
     cards are what mobile/tablet always see. Both read the identical
     filtered list, so they never disagree. */
  .history-table-wrap{ display:none; }
  .history-table{ width:100%; border-collapse:collapse; font-size:13.5px; }
  .history-table thead th{
    text-align:left; font-size:11px; font-weight:700; text-transform:uppercase;
    letter-spacing:0.06em; color:var(--pw-text-muted);
    padding:8px 10px; border-bottom:1px solid var(--pw-border-strong);
  }
  .history-table tbody td{ padding:10px; border-bottom:1px solid var(--pw-border); }
  .history-table tbody tr:last-child td{ border-bottom:none; }
  .history-table td:nth-child(4), .history-table td:nth-child(5){ font-variant-numeric:tabular-nums; }
  .history-table tbody tr.hist-row:hover{ background:var(--surface-2); }
  .hist-expand-ic{ display:inline-block; width:12px; color:var(--muted); font-size:11px; }
  .hist-detail-row td{ padding:0 10px 14px 10px; }
  @media (min-width:1024px){
    .history-cards{ display:none; }
    .history-table-wrap{ display:block; overflow-x:auto; }
  }

  /* Per-run picked/missed item breakdown — expanded inline under a
     timeline card (mobile/tablet) or a table row (desktop). Same
     markup either way; see renderHistoryItemsDetail(). */
  .hist-detail{ margin-top:12px; padding-top:12px; border-top:1px solid var(--pw-border); }
  .hist-detail-empty{ font-size:12.5px; color:var(--muted); }
  .hist-detail-group + .hist-detail-group{ margin-top:10px; }
  .hist-detail-label{
    font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.05em;
    margin-bottom:5px;
  }
  .hist-detail-label.missed{ color:var(--pw-accent-text); }
  .hist-detail-label.picked{ color:var(--pw-success-text); }
  .hist-detail-list{ list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:4px; }
  .hist-detail-list li{
    font-size:13px; display:flex; justify-content:space-between; gap:8px;
    padding:3px 0;
  }
  .hist-detail-list.picked-list li{ color:var(--pw-text-muted); text-decoration:line-through; text-decoration-color:var(--pw-border-strong); }
  .hist-detail-unit{ color:var(--muted); font-size:12px; }
  .hist-detail-qty{ flex:0 0 auto; font-variant-numeric:tabular-nums; color:var(--muted); font-size:12.5px; }
  .hist-detail-add{ margin-top:12px; }

  /* ---- barcode scanner ---- */
  .scan-overlay{
    position:fixed; inset:0; background:#000; z-index:500;
    display:flex; flex-direction:column;
  }
  .scan-overlay #scan-video-box{ flex:1; position:relative; overflow:hidden; }
  .scan-overlay #scan-video-box video{ width:100%; height:100%; object-fit:cover; }
  #scan-reader{ width:100%; height:100%; }
  #scan-reader video{ width:100% !important; height:100% !important; object-fit:cover; }
  .scan-top{
    position:fixed; top:0; left:0; right:0;
    /* The scanning library draws its own overlay on top of the video
       with a z-index we don't control — this pins the close button
       above absolutely everything, on its own layer, guaranteed. */
    z-index:2147483647;
    padding-top:calc(14px + env(safe-area-inset-top));
    padding-left:16px; padding-right:16px; padding-bottom:14px;
    display:flex; justify-content:flex-end; align-items:center;
    background:linear-gradient(rgba(0,0,0,0.55), transparent);
    pointer-events:none;
  }
  .scan-top .close-btn{
    pointer-events:auto;
    width:44px; height:44px; border-radius:10px;
    background:rgba(0,0,0,0.6); color:#fff; border:1px solid rgba(255,255,255,0.35); font-size:20px; font-weight:800;
  }
  .scan-hint{
    color:#fff; text-align:center; font-size:13px;
    padding:14px 16px calc(14px + env(safe-area-inset-bottom));
    background:#000;
    transition:color .15s ease;
  }
  .scan-hint.err{ color:var(--pw-danger); }
  .scan-hint.ok{ color:var(--pw-success-text); font-weight:700; }
  /* Only non-empty in the error state (see updateScanHint()) — when
     empty it takes no space at all, so the normal "Point the camera…"
     hint above still owns the safe-area edge on its own. */
  #scan-hint-actions:not(:empty){
    display:flex; gap:8px; justify-content:center;
    padding:0 16px calc(14px + env(safe-area-inset-bottom));
    background:#000;
  }
  #scan-hint-actions .btn{ flex:1; max-width:200px; }
  .btn{
    border:none;
    border-radius:8px;
    padding:11px 16px;
    font-weight:700;
    font-size:14px;
    transition:transform var(--pw-motion-press), opacity var(--pw-motion-press);
  }
  .btn-primary{background:var(--amber); color:#241A05;}
  /* Secondary — a filled step of material, not an outlined box next to
     a filled one (the two used to read as unrelated shapes). */
  .btn-ghost{background:var(--pw-surface-raised); color:var(--pw-text-soft); border:none;}
  /* Destructive but quiet (section 15): red text carries the meaning on
     its own, no ringed box drawing extra attention to it. */
  /* A destructive button was background-less and border-less, which made
     "Remove all" — it wipes the entire catalogue — render as bare red text
     directly above "Minimise all", a benign action with near-identical
     weight and position. Bounding it is what separates the two; it is
     deliberately a soft tint and a hairline rather than a filled red slab,
     because the goal is "this is a discrete, considered action", not a
     louder warning. Applies to every .btn-danger so destructive controls
     read consistently. Confirmation gates are unchanged. */
  .btn-danger{
    background:var(--pw-danger-soft);
    color:var(--pw-danger);
    border:1px solid rgba(226,96,79,0.32);
  }
  .btn-danger:active{ background:rgba(226,96,79,0.20); }
  /* Small downward compression + a lighter dim than before — the scale
     now carries most of the "pressed" feeling, so opacity only needs to
     support it rather than do the whole job alone. Fast, controlled
     return on release via the transition above. */
  .btn:active{opacity:0.85; transform:scale(var(--pw-press-scale-btn));}
  .btn-block{width:100%;}

  /* ---- manage view ---- */
  .manage-section{margin-bottom:28px;}
  .manage-section h2{font-size:14px; text-transform:uppercase; letter-spacing:0.08em; color:var(--pw-text-soft); margin-bottom:10px;}
  .field{
    background:var(--pw-mat-embed);
    border:none;
    border-radius:10px;
    padding:9px 11px;
    color:var(--text);
    font-size:14px;
    width:100%;
    margin-bottom:8px;
    box-shadow:var(--pw-shadow-recessed);
    transition:box-shadow var(--pw-motion-quick);
  }
  .field:focus{ outline:none; box-shadow:var(--pw-shadow-recessed), 0 0 0 1.5px var(--pw-hairline-strong); }
  .field::placeholder{color:var(--muted);}
  .select-field{
    -webkit-appearance:none;
    appearance:none;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' stroke='%238F9891' stroke-width='2' fill='none' fill-rule='evenodd'/%3E%3C/svg%3E");
    background-repeat:no-repeat;
    background-position:right 12px center;
    padding-right:34px;
  }
  .select-field option, .select-field optgroup{background:var(--surface-2); color:var(--text);}
  .row{display:flex; gap:8px;}
  .row .field{flex:1;}
  .chip-select{display:flex; flex-wrap:wrap; gap:6px; margin:8px 0;}
  .chip{
    padding:6px 11px;
    border-radius:20px;
    border:1px solid transparent;
    background:var(--surface-2);
    color:var(--muted);
    font-size:12.5px;
    font-weight:600;
    transition:background-color var(--pw-motion-quick), color var(--pw-motion-quick), box-shadow var(--pw-motion-quick);
  }
  .chip.on{background:var(--amber-dim); color:var(--pw-accent-text);}
  .chip.shop-chip.on{background:var(--green-dim); color:var(--pw-success-text);}
  /* An operational list item, not a bordered card — same hairline
     idiom as .item-row (Order). Used for My Shops, Connected People,
     the category picker trigger, and the product catalogue below. */
  .list-row{
    display:flex; align-items:center; justify-content:space-between;
    background:transparent;
    border:none;
    border-bottom:1px solid var(--pw-hairline);
    border-radius:0;
    padding:12px 4px;
    margin-bottom:0;
    min-height:var(--pw-tap-min);
  }
  .list-row .meta{font-size:12px; color:var(--muted); margin-top:2px;}
  .list-row .right{display:flex; flex-direction:column; align-items:flex-end; gap:6px; flex:0 0 auto;}
  .list-row .right .qty-controls{gap:0;}
  /* Per-row destructive action. At list density there is one of these on
     every row — 120 of them on a real catalogue — so the goal is calm, not
     loud: the label still says exactly what it does and keeps the danger
     hue, but it no longer competes with the product name for attention
     (600 rather than 700, a step down in size). Contrast is unchanged —
     the hue is not dimmed, because a destructive control must stay legible.
     It gains a real compact tap target and a pressed/focused fill, so it
     reads as a bounded control rather than loose red text. Behaviour is
     untouched: this still triggers the same removal, with the same undo. */
  .del{
    color:var(--pw-danger);
    font-size:12.5px;
    font-weight:600;
    padding:0 10px;
    min-height:var(--pw-tap-compact);
    display:flex;
    align-items:center;
    border-radius:var(--pw-radius-sm);
    transition:background var(--pw-motion-quick);
  }
  .del:active{ background:var(--pw-danger-soft); }
  .del:focus-visible{ background:var(--pw-danger-soft); }
  /* ---- identity marks: generated initials, never a fake photo ---- */
  .id-avatar{
    width:36px; height:36px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    font-weight:700; font-size:13px; letter-spacing:0.01em;
    flex:0 0 auto;
  }
  .id-avatar-0{ background:rgba(255,168,61,0.16); color:var(--pw-accent-text); }
  .id-avatar-1{ background:rgba(55,196,140,0.16); color:var(--pw-success-text); }
  .id-avatar-2{ background:rgba(127,169,214,0.18); color:#9CC2E8; }
  .id-avatar-3{ background:rgba(199,146,234,0.16); color:#D9A9F5; }
  .id-avatar-4{ background:rgba(240,152,152,0.16); color:#F3A6A6; }
  .id-avatar-5{ background:rgba(140,198,207,0.16); color:#9FD6DE; }
  /* Neutral connection-state language — never "online"/"live"; sync
     happening in the background isn't the same as a person being present. */
  .conn-status{
    font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:0.05em;
    padding:3px 9px; border-radius:var(--pw-radius-pill);
    flex:0 0 auto; white-space:nowrap;
  }
  .conn-status.connected, .conn-status.owner{ background:var(--green-dim); color:var(--pw-success-text); }
  .conn-status.pending{ background:var(--surface-2); color:var(--muted); }
  .hint{font-size:12px; color:var(--muted); margin-top:-2px; margin-bottom:10px; line-height:1.4;}
  /* Replaces the old back-row + page-tabs + swipe-hint stack — the
     bottom dock is now the one visible way to navigate between Order/
     Products/Shops/History/Settings on mobile/tablet, so this is just a
     minimal branded top strip (also gives these screens the same
     safe-area-aware sticky top edge Order's header has). Swiping
     between Order/Products/Settings still works; it just isn't
     advertised here anymore since the dock already shows where you are. */
  .mobile-topbar{
    padding:12px 16px; display:flex; align-items:center; justify-content:center;
    position:sticky; top:env(safe-area-inset-top); z-index:50;
    background:var(--bg);
    border-bottom:1px solid var(--pw-hairline);
  }
  .mobile-topbar .logo-mark{ width:26px; height:26px; flex:0 0 auto; }
  /* Admin is the one screen the dock doesn't cover (it hides itself
     there — see renderDock in js/app.js), so unlike Products/Shops/
     History/Settings it still needs a real, explicit way back. */
  .back-row{
    padding:12px 16px; display:flex; align-items:center; justify-content:space-between; gap:10px;
    position:sticky; top:env(safe-area-inset-top); z-index:50;
    background:var(--bg);
    border-bottom:1px solid var(--pw-hairline);
  }
  .back-row .logo-mark{ width:26px; height:26px; flex:0 0 auto; }
  .back-btn{
    color:var(--pw-accent-text); font-weight:700; font-size:14px;
    display:flex; align-items:center; gap:4px;
    background:none; border:none; padding:0;
  }
  /* ---- in-app dialogs (native alert/confirm are unreliable in iOS PWAs) ---- */
  .modal-backdrop{
    position:fixed; inset:0;
    background:rgba(0,0,0,0.6);
    display:flex; align-items:center; justify-content:center;
    padding:24px;
    z-index:100;
  }
  .modal{
    background:var(--pw-surface-3);
    border:none;
    border-radius:var(--pw-radius-lg);
    padding:20px 18px 16px;
    max-width:360px; width:100%;
    /* Reads as material that rose up out of the surface behind it, not a
       boxed dialog dropped on top — the shadow does the separating, the
       inset highlight along the top edge does the "catching light". */
    box-shadow:var(--pw-shadow-float), var(--pw-inset-highlight);
  }
  .modal .msg{font-size:15.5px; font-weight:600; line-height:1.45; margin-bottom:18px;}
  .modal .modal-actions{display:flex; gap:8px;}
  .modal .modal-actions .btn{flex:1;}
  .modal .modal-actions.stacked{flex-direction:column; align-items:stretch; gap:14px;}
  .modal .modal-actions.stacked .btn{flex:none; width:100%;}
  /* Groups each choice with its own explanatory hint as one visual unit,
     so picking between "Reset list" and "Remove list" reads as two
     distinct, self-contained decisions rather than a single button list. */
  .choice-option{display:flex; flex-direction:column; gap:6px;}
  .choice-option .hint{margin:0;}
  .modal .modal-actions.stacked > [data-modal-cancel]{ margin-top:2px; }
  .toast{
    position:fixed;
    left:0; right:0;
    /* Clears the dock, and — on Order, where the summary bar also sits
       above the dock — the summary bar too. The dock-only screens get a
       bit more clearance than strictly needed; harmless. Reset on
       desktop below, where neither fixed bar exists. */
    bottom:calc(var(--pw-dock-h) + env(safe-area-inset-bottom) + 80px);
    margin:0 auto;
    background:var(--pw-surface-3);
    border:none;
    color:var(--pw-text-soft);
    padding:12px 15px;
    border-radius:13px;
    font-size:13.5px;
    font-weight:600;
    line-height:1.4;
    max-width:min(320px, calc(var(--app-max) - 40px));
    width:calc(100% - 40px);
    z-index:120;
    box-shadow:var(--pw-shadow-float), var(--pw-inset-highlight);
  }
  .toast-undo{
    display:flex; align-items:center; justify-content:space-between; gap:12px;
  }
  .toast-undo button{
    background:none; border:none; color:var(--pw-accent-text);
    font-weight:800; font-size:14px; padding:2px 4px; flex:0 0 auto;
  }
  /* ---- product name suggestions ---- */
  .suggest-wrap{position:relative;}
  .suggest-list{
    position:absolute;
    left:0; right:0; top:100%;
    margin-top:6px;
    background:var(--pw-surface-3);
    border:none;
    border-radius:12px;
    max-height:260px;   /* a cap, not the final value — sizeSuggestList()
                           narrows it when the keyboard leaves less room */
    overflow-y:auto;
    /* Reaching the end of this list must not start dragging the page
       underneath it: that scroll-chaining is what makes a bounded panel feel
       like it "isn't scrolling" on a phone. */
    overscroll-behavior:contain;
    -webkit-overflow-scrolling:touch;   /* momentum inside the panel on iOS */
    z-index:60;
    box-shadow:0 16px 32px -12px rgba(0,0,0,0.55), var(--pw-inset-highlight);
  }
  .suggest-item{
    padding:11px 12px;
    border-bottom:1px solid var(--pw-hairline);
    font-size:15px;
    display:flex; align-items:center; justify-content:space-between; gap:10px;
  }
  .suggest-item:last-child{border-bottom:none;}
  .suggest-item:active{background:var(--surface-2);}
  /* Keyboard-highlighted suggestion (Up/Down) — same visual language as
     everywhere else a currently-selected row gets marked. */
  .suggest-item.active{background:var(--pw-surface-raised);}
  .suggest-item .s-name{flex:1; min-width:0; font-weight:600;}
  .suggest-item .s-meta{display:flex; align-items:center; gap:6px; flex:0 0 auto;}
  .suggest-item .s-cat{
    font-size:11px; color:var(--muted);
    white-space:nowrap;
    text-transform:uppercase; letter-spacing:0.05em;
  }
  /* Where a suggestion is genuinely known — not internal jargon like
     "tier 0/1", just what it means to the person picking it. */
  .suggest-item .s-source{
    font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.04em;
    padding:2px 7px; border-radius:var(--pw-radius-pill); white-space:nowrap;
  }
  .suggest-item .s-source.s-own{ background:var(--pw-accent-soft); color:var(--pw-accent-text); }
  .suggest-item .s-source.s-shared{ background:var(--pw-linked-soft); color:var(--pw-linked); }

  /* ---- Add Products: search is the command surface, scan lives inside
     it rather than as a second bordered control beside it ---- */
  .search-row{
    display:flex; align-items:stretch; gap:0;
    background:var(--pw-mat-embed);
    border:none;
    border-radius:14px;
    margin-bottom:8px;
    /* A shallow well the input sits inside, not a flat panel painted on
       top of the page — restrained on purpose, this is typed in and
       tapped constantly. One recessed shadow for the whole row; the
       field/button inside stay flat so it doesn't double up. */
    box-shadow:var(--pw-shadow-recessed);
  }
  .search-row .product-search-field{
    flex:1; min-width:0; font-size:16px;
    background:none; border:none; border-radius:14px 0 0 14px;
    margin-bottom:0; box-shadow:none;
  }
  .search-row .scan-btn{
    flex:0 0 auto;
    border:none;
    border-radius:0 14px 14px 0;
    background:none;
    color:var(--pw-accent-text);
    transition:background-color var(--pw-motion-press), transform var(--pw-motion-press);
  }
  /* A soft internal seam instead of a hard divider line — the scan
     button still reads as its own control without partitioning the row
     into two boxes. */
  .search-row .scan-btn::before{
    content:""; position:absolute; left:0; top:20%; bottom:20%; width:1px;
    background:var(--pw-hairline);
  }
  .search-row .scan-btn{ position:relative; }
  /* A touch of "illuminated hardware button" on press — a warm fill
     from the same accent it's already coloured, not a pulsing glow. */
  .search-row .scan-btn:active{ background:var(--amber-dim); transform:scale(var(--pw-press-scale-btn)); }
  .field-label{
    font-size:11px; font-weight:700; letter-spacing:0.04em;
    text-transform:uppercase; color:var(--pw-text-muted);
    margin:0 0 5px;
  }
  /* Separates the search "hero" from the secondary details cluster below
     it (unit/qty/category/store) — hierarchy through space + a quiet
     label, not by hiding any of those fields. */
  .details-divider{
    display:flex; align-items:center; gap:10px;
    margin:16px 0 10px;
    color:var(--pw-text-muted);
    font-size:10.5px; font-weight:700; letter-spacing:0.1em; text-transform:uppercase;
  }
  .details-divider::after{
    content:""; flex:1; height:1px; background:var(--pw-hairline);
  }

  /* ---- Settings: quiet rows instead of a stack of full-width buttons —
     a row is either information (value on the right), a disclosure
     (chevron), or a switch. Real <button>s throughout, just restyled,
     so click/keyboard behaviour needs no extra wiring. ---- */
  .settings-row{
    display:flex; align-items:center; justify-content:space-between; gap:14px;
    width:100%; text-align:left;
    background:none; border:none;
    padding:13px 2px;
    border-bottom:1px solid var(--pw-hairline);
    color:var(--text);
    font-size:14.5px;
    min-height:var(--pw-tap-min);
  }
  .manage-section .settings-row:last-of-type{ border-bottom:none; }
  .settings-row .sr-label{ min-width:0; }
  .settings-row .sr-label small{
    display:block; color:var(--muted); font-size:11.5px; font-weight:400;
    margin-top:2px; line-height:1.35; text-transform:none; letter-spacing:0;
  }
  .settings-row .sr-value{ color:var(--muted); font-size:13px; flex:0 0 auto; text-align:right; }
  .settings-row .sr-action{ color:var(--pw-accent-text); font-weight:700; font-size:13.5px; white-space:nowrap; flex:0 0 auto; }
  .settings-row.danger .sr-label{ color:var(--red); }
  .settings-inline-form{ padding:4px 2px 8px; display:flex; flex-direction:column; gap:0; }
  .settings-inline-form .btn{ align-self:flex-start; margin-top:4px; }

  .switch-track{
    width:40px; height:23px; border-radius:20px; background:var(--surface-2);
    position:relative; flex:0 0 auto;
    transition:background-color var(--pw-motion-quick), box-shadow var(--pw-motion-quick);
  }
  .switch-track::after{
    content:""; position:absolute; width:17px; height:17px; border-radius:50%;
    background:var(--muted); top:3px; left:3px;
    transition:transform var(--pw-motion-quick), background-color var(--pw-motion-quick);
  }
  .switch-track.on{ background:var(--amber); }
  .switch-track.on::after{ background:#241A05; transform:translateX(17px); }

  /* The one action allowed to stay a full, isolated, unmissable button —
     everything else in Settings is a quiet row. */
  .danger-zone{
    margin-top:20px; padding:16px; border-radius:var(--pw-radius-lg);
    background:var(--pw-danger-soft);
    box-shadow:inset 0 0 0 1px rgba(226,96,79,0.35);
  }
  .danger-zone .field-label{ margin-bottom:4px; }
  .danger-zone .btn-danger{ margin-top:10px; }
  .field-label .optional{
    text-transform:none; font-weight:500; letter-spacing:0; opacity:0.85;
  }
  /* ---- filter summary rows (Shops, Cash & carry) ----
     One row standing in for a strip of chips. At rest it answers the two
     questions the strip answered by shouting: what is selected, and is
     there more. Tapping reveals the original strip unchanged.

     The row is deliberately quiet — no accent fill, no lit rim. It is a
     statement of current state with a way in, not an action competing with
     Finish run. That is the whole point of the redesign: Liquid Glass stays
     strongest on the things that move the run forward. */
  .pw-picker{ padding:8px 16px 0; }
  .pw-picker-head{
    width:100%;
    display:flex; align-items:center; gap:10px;
    padding:10px 12px;
    border:none;
    border-radius:var(--pw-radius-lg);
    background-color:var(--pw-g-fill);
    background-image:var(--pw-g-spec);
    box-shadow:var(--pw-g-rim-soft);
    text-align:left;
    min-height:var(--pw-tap-compact);
    transition:box-shadow var(--pw-motion-quick), background-color var(--pw-motion-quick);
  }
  .pw-picker-ic{
    flex:0 0 auto; display:flex; color:var(--pw-label-on-glass);
  }
  .pw-picker-ic svg{ width:18px; height:18px; display:block; }
  .pw-picker-text{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:1px; }
  .pw-picker-label{
    font-size:10px; font-weight:700; letter-spacing:0.14em;
    text-transform:uppercase; color:var(--pw-label-on-glass);
  }
  /* The selection can be several names; it truncates rather than wrapping,
     because a row that changes height as you select things makes the list
     below jump under your thumb mid-run. */
  .pw-picker-value{
    font-size:13.5px; font-weight:700; color:var(--text);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .pw-picker-more{
    flex:0 0 auto;
    font-size:11.5px; font-weight:600; color:var(--pw-label-on-glass);
    white-space:nowrap;
  }
  .pw-picker-chev{
    flex:0 0 auto; width:16px; height:16px; color:var(--pw-label-on-glass);
    transition:transform var(--pw-motion-glide);
  }
  .pw-picker.open .pw-picker-chev{ transform:rotate(180deg); }
  .pw-picker.open .pw-picker-head{
    background-color:var(--pw-g-fill-raised, var(--pw-g-fill));
    box-shadow:var(--pw-g-rim);
  }
  /* Collapsed by height rather than display:none so the strip inside keeps
     real computed styles, and `inert` (set in the markup) keeps its chips
     out of the tab order while they are hidden. */
  .pw-picker-panel{
    max-height:0; overflow:hidden; opacity:0;
    transition:max-height var(--pw-motion-glide), opacity var(--pw-motion-quick);
  }
  .pw-picker.open .pw-picker-panel{ max-height:220px; opacity:1; }
  .pw-picker-panel .store-tabs{ padding-left:2px; padding-right:40px; }

  /* ---- shops ---- */
  .tabs-label{
    font-size:10px; font-weight:700; letter-spacing:0.14em;
    text-transform:uppercase; color:var(--muted);
    padding:8px 16px 0;
  }
  .tag.shop-tag.active{
    background:var(--green-dim);
    color:var(--pw-success-text);
  }
  .shop-header{
    display:flex; align-items:center; justify-content:space-between;
    background:var(--surface-2);
    border-left:3px solid var(--green);
    border-radius:6px;
    padding:8px 12px;
    margin:18px 0 8px;
    font-size:14px; font-weight:800;
    letter-spacing:0.04em;
  }
  .shop-header .count{
    background:var(--bg); color:var(--muted);
    padding:2px 8px; border-radius:20px; font-size:11px; font-weight:700;
  }
  .item-shop{
    font-size:11px; color:var(--pw-success-text); font-weight:700;
    margin-top:2px; letter-spacing:0.04em;
  }
  .item-shop.linked{color:#7FA9D6;}
  .tag.shop-tag.linked.active{
    background:rgba(127,169,214,0.16); color:#9CC2E8;
  }
  .req-row{
    display:flex; align-items:center; justify-content:space-between; gap:8px;
    background:transparent; border:none;
    border-bottom:1px solid var(--pw-hairline);
    border-radius:0; padding:11px 4px; margin-bottom:0;
  }
  /* Genuinely needs your action (incoming) vs. just waiting (outgoing) —
     same left-accent idiom .shop-header already uses for "this belongs
     to something", not a new visual pattern. */
  .req-row.incoming{ border-left:3px solid var(--pw-accent); padding-left:9px; }
  .req-row .btn{padding:7px 12px; font-size:13px;}
  .req-actions{display:flex; gap:6px; flex:0 0 auto;}
  .row-actions{display:flex; align-items:center; gap:4px; flex:0 0 auto;}
  .row-actions .btn{padding:7px 10px; font-size:13px;}
  /* Reorder arrows: square, so two of them read as one pair of controls
     rather than two more word-buttons competing with Rename and Remove.
     At the ends of the list they stay in place and go quiet instead of
     disappearing, so the row never reflows as you move an item. */
  .row-actions .supplier-move{
    padding:7px 0; width:34px; text-align:center;
    font-size:11px; line-height:1.2;
  }
  .row-actions .supplier-move[disabled]{ opacity:0.35; }
  /* ---- share code: a digital access pass, not a settings value ---- */
  .share-pass{
    position:relative; overflow:hidden;
    border-radius:var(--pw-radius-lg);
    background:linear-gradient(155deg, var(--pw-surface-3), var(--surface) 72%);
    box-shadow:var(--pw-inset-highlight);
    padding:18px 18px 16px;
    margin-bottom:10px;
  }
  .share-pass::after{
    content:""; position:absolute; right:-34px; top:-34px;
    width:150px; height:150px; border-radius:50%;
    background:radial-gradient(circle, var(--pw-glow-accent) 0%, transparent 70%);
  }
  .share-pass-label{
    font-size:10.5px; letter-spacing:0.12em; text-transform:uppercase;
    color:var(--muted); font-weight:700;
  }
  .code-box{
    font-family:ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", "Courier New", monospace;
    color:var(--pw-accent-text);
    text-align:left;
    font-size:28px;
    font-weight:600;
    letter-spacing:0.14em;
    margin:8px 0 0;
    font-variant-numeric:tabular-nums;
    position:relative;
  }
  .share-pass-divider{
    height:1px; margin:14px 0 12px;
    background:repeating-linear-gradient(90deg, var(--pw-hairline-strong) 0 6px, transparent 6px 13px);
  }
  .badge{
    background:var(--amber); color:#241A05;
    border-radius:20px; padding:1px 7px;
    font-size:11px; font-weight:800; margin-left:6px;
  }
  /* ---- page tabs ----
     Now 4 destinations (Products/Shops/History/Settings) instead of 2 —
     wraps to a horizontally scrolling strip rather than squeezing labels
     below a comfortable size on narrow phones. */
  .page-tabs{
    display:flex; gap:6px;
    padding:8px 16px 4px;
    overflow-x:auto;
    scrollbar-width:none;
  }
  .page-tabs::-webkit-scrollbar{ display:none; }
  .page-tab{
    flex:1 1 0;
    min-width:76px;
    min-height:var(--pw-tap-min);
    padding:11px 6px;
    border-radius:20px;
    border:1px solid transparent;
    background:var(--surface-2);
    color:var(--muted);
    font-size:13.5px; font-weight:700;
    white-space:nowrap;
    transition:background-color var(--pw-motion-quick), color var(--pw-motion-quick), box-shadow var(--pw-motion-quick);
  }
  .page-tab.on{
    background:var(--amber-dim);
    color:var(--pw-accent-text);
  }
  /* ---- auth screen ---- */
  .auth-wrap{
    max-width:420px; margin:0 auto;
    /* The one screen with no dock and no summary-bar, so — unlike Order/
       Products/Shops/History/Settings — it accounts for the bottom safe
       area itself rather than relying on the dock/main to do it. */
    padding:60px 20px calc(24px + env(safe-area-inset-bottom));
  }
  .auth-brand{ text-align:center; margin-bottom:28px; }
  .auth-brand .logo-mark{ width:64px; height:64px; margin:0 auto 10px; display:block; }
  header .brand{display:flex; flex-direction:row; align-items:center; gap:10px;}
  header .logo-mark{ width:34px; height:34px; flex:0 0 auto; transition:width .2s ease, height .2s ease; }
  header .brand-text{display:flex; flex-direction:column; gap:2px;}
  .sticky-top.compact header .logo-mark{ width:26px; height:26px; }
  .auth-tabs{
    display:flex; gap:8px;
    padding:0 0 14px;
  }
  .auth-tabs .page-tab{ padding:12px 8px; }
  .otp-inputs{ display:flex; gap:8px; justify-content:center; margin:14px 0; }
  .otp-digit{
    width:44px; height:54px;
    background:var(--pw-mat-embed);
    border:none;
    border-radius:8px;
    color:var(--text);
    font-size:22px;
    font-weight:800;
    text-align:center;
    box-shadow:var(--pw-shadow-recessed);
    transition:box-shadow var(--pw-motion-quick);
  }
  .otp-digit:focus{ outline:none; box-shadow:var(--pw-shadow-recessed), 0 0 0 1.5px var(--amber); }

  /* ---- loading / transition polish ----
     Used to skip the Order screen entirely — that view re-rendered on
     every single pick/qty tap, and fading on every rapid tap would have
     made the core picking workflow feel laggy. Pick and quantity both
     now update their row in place without a full render() (see
     attemptTargetedPickToggle/attemptTargetedAislePickToggle and
     applyQtyInPlace in app.js) — the only things left that still
     rebuild Order's <main> are occasional, deliberate actions
     (navigating here, List⇄Aisle, a filter/search change, expanding a
     category, adding/finishing a run), so the fade is safe here too now,
     and is what actually smooths the List⇄Aisle switch. */
  @keyframes pw-fade-in{ from{opacity:0;} to{opacity:1;} }
  @keyframes pw-slide-up-fade{ from{opacity:0; transform:translate(-50%, 10px);} to{opacity:1; transform:translate(-50%,0);} }
  /* A small rise-into-place rather than a pure zoom-pop — reads as the
     sheet physically settling onto the surface above it, not scaling
     up from nothing. Still just transform+opacity, still this cheap. */
  @keyframes pw-modal-pop{ from{opacity:0; transform:translateY(10px) scale(0.97);} to{opacity:1; transform:translateY(0) scale(1);} }
  @keyframes pw-spin{ to{ transform:rotate(360deg); } }
  @keyframes pw-pulse{ 0%,100%{opacity:1;} 50%{opacity:0.35;} }

  .page-fade{ animation: pw-fade-in var(--pw-motion-pop); }
  .toast{ animation: pw-slide-up-fade var(--pw-motion-pop); }
  .modal-backdrop{ animation: pw-fade-in var(--pw-motion-quick); }
  .modal{ animation: pw-modal-pop var(--pw-motion-pop); }

  .sync-dot{
    display:inline-block; width:6px; height:6px; border-radius:50%;
    background:currentColor; margin-right:5px;
    animation:pw-pulse 1.1s ease-in-out infinite;
  }

  #boot-splash{
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:16px; padding:24px;
    min-height:100vh; /* fallback for browsers without dvh */
    min-height:100dvh; /* accounts for mobile browser chrome so this
      one-time splash doesn't measure taller than what's actually
      visible and jump when the address bar collapses */
  }
  #boot-splash .logo-mark{ width:56px; height:56px; }
  #boot-splash .boot-label{
    color:var(--muted); font-size:12px; letter-spacing:0.14em; text-transform:uppercase; font-weight:700;
  }
  #boot-splash .spinner{
    width:22px; height:22px; border-radius:50%;
    border:3px solid var(--line); border-top-color:var(--pw-accent-text);
    animation:pw-spin 0.8s linear infinite;
  }
  #app.pw-ready{ animation: pw-fade-in 0.25s ease; }

  /* ================================================================
     RESPONSIVE SHELL (Phase B2)
     Three intentional tiers, decided from the layouts themselves
     rather than arbitrary device names:
       < 768px   compact/mobile   — unchanged single column
       768–1023px wide mobile/tablet — same column, more breathing room
       >= 1024px desktop          — persistent nav + optional aside
     #pw-sidenav and #pw-aside are populated by JS (renderSidenav /
     renderAside in js/app.js) but structurally start `hidden`; below
     1024px they stay hidden regardless of their content, so nothing
     here changes the mobile/tablet experience.
     ================================================================ */
  #pw-shell{ min-height:100%; }
  #pw-workspace{ min-width:0; }
  /* Below 1024px these never take layout, full stop — regardless of the
     `hidden` attribute (which JS toggles purely for >=1024px purposes:
     signed-out state, or an aside with nothing to show). Without this
     unconditional rule, JS clearing `hidden` on sign-in would make the
     nav render inline above the page on mobile/tablet, since neither
     #pw-shell nor #pw-sidenav are flex/none there otherwise. */
  #pw-sidenav, #pw-aside{ display:none; }

  /* ================================================================
     LIGHT THEME

     Driven entirely off data-theme, which js resolves — "system" becomes
     "light" or "dark" before this file is ever consulted. That is why there
     is no prefers-color-scheme block here: resolving in one place means the
     palette is written once instead of duplicated into a media query and a
     data-attribute block that are free to drift apart.

     Dark remains Pikwise's identity and the untouched default. Nothing above
     this line changed to make light work; every value here is an override.

     The glass needed genuine rethinking rather than inversion. On the dark
     page a piece is a WHITE film catching light on a black ground. On a light
     page a white film on near-white is invisible, so a piece becomes a
     slightly denser, cooler pane with a soft dark edge — still a shaped
     object with a lit top and a grounded bottom, but the contrast now runs
     the other way. The specular stays, weakened: there is far less headroom
     above a light surface before a highlight turns into a white blur.
     ================================================================ */
  :root[data-theme="light"]{
    /* Not pure white. A hair of warmth keeps the amber accent from looking
       acidic and stops large areas glaring under shop lighting. */
    /* Deeper than the first attempt at #F4F5F3. A white pane on that ground
       separated from it by 1.09:1 while the same pieces separate from the dark
       page by 1.29-1.46:1, so the chips and ghost buttons had no body at all —
       only a rim. #E6E8E4 restores 1.23:1 and still leaves muted text at
       4.70:1 against it. This is the dark theme's own lesson inverted: a white
       film on a near-white page is as invisible as a white film on black. */
    --bg:#E6E8E4;
    --surface:#FFFFFF;
    --surface-2:#F2F4F1;
    --line:#D2D6D0;
    --text:#14171A;
    /* Re-measured after the ground was deepened: at #5F6670 muted text fell to
       4.20:1 inside the segmented control's recess and #C0392B to 4.41:1 on
       the page. Both now clear AA on the darkest ground they are painted on. */
    --muted:#575E68;
    --red:#B33325;

    /* Accent FILLS stay bright — a primary button is still amber with dark
       ink on it, and that pairing already measures well. */
    --amber:#FFA83D;
    --green:#1FA06E;
    --amber-dim:rgba(255,168,61,0.20);
    --green-dim:rgba(31,160,110,0.16);

    /* Accent TEXT darkens, because the fill colours do not survive on a
       light ground: #FFA83D on #F4F5F3 is about 1.8:1. */
    /* Measured, not picked by eye. #9A5B02 came to 4.35:1 on the amber wash
       under an active chip and #0E7A52 to 4.13:1 on the green "Owner" pill —
       both under AA on the exact grounds the app actually paints them on. */
    --pw-accent-text:#8A5102;
    --pw-success-text:#0B6845;

    --pw-text-soft:#3A4149;
    --pw-text-muted:#575E68;
    --pw-category:#4A6076;
    --pw-border-strong:#BCC2BA;
    --pw-accent-strong:#B86D02;
    --pw-accent-ink:#241A05;
    --pw-success-ink:#FFFFFF;
    --pw-danger:#B33325;
    --pw-danger-soft:#FAE7E4;
    --pw-linked:#2F6296;
    --pw-linked-soft:#E4EDF6;
    --pw-surface-3:#FFFFFF;
    --pw-mat-embed:#DCE0DA;
    --pw-overlay:rgba(24,28,32,0.42);

    /* Hairlines go dark: a white hairline is invisible on white. */
    --pw-hairline:rgba(18,22,26,0.10);
    --pw-hairline-strong:rgba(18,22,26,0.17);
    --pw-glow-accent:rgba(255,168,61,0.28);
    --pw-inset-highlight:inset 0 1px 0 rgba(255,255,255,0.9);

    /* Shadows soften. A light UI with dark-page shadow values looks dirty. */
    --pw-shadow-card:0 1px 2px rgba(20,24,28,0.07), 0 2px 8px -4px rgba(20,24,28,0.10);
    --pw-shadow-float:0 12px 32px -14px rgba(20,24,28,0.26);
    --pw-shadow-dock:0 -10px 18px -16px rgba(20,24,28,0.22);
    --pw-shadow-recessed:inset 0 1px 2px rgba(20,24,28,0.10);

    /* ---- glass, light ---- */
    --pw-g-fill:rgba(255,255,255,0.92);
    --pw-g-fill-raised:rgba(255,255,255,0.95);
    --pw-g-fill-sunken:rgba(20,24,28,0.055);
    --pw-g-fill-float:rgba(255,255,255,0.78);

    --pw-g-spec:linear-gradient(to bottom,
        rgba(255,255,255,0.85) 0%,
        rgba(255,255,255,0.35) 46%,
        rgba(255,255,255,0.06) 47%,
        rgba(255,255,255,0.30) 100%);
    --pw-g-spec-corner:radial-gradient(128% 86% at 16% -8%,
        rgba(255,255,255,0.75) 0%, rgba(255,255,255,0) 58%);

    /* The rim reverses: still lit on top, but the piece is now defined by a
       soft dark edge against a pale page rather than a bright one against
       black. */
    --pw-g-rim:
      inset 0 1px 0 rgba(255,255,255,0.95),
      inset 0 -1px 0 rgba(20,24,28,0.09),
      inset 1px 0 0 rgba(255,255,255,0.55),
      inset -1px 0 0 rgba(255,255,255,0.55),
      0 0 0 1px rgba(20,24,28,0.11);
    --pw-g-rim-soft:
      inset 0 1px 0 rgba(255,255,255,0.8),
      inset 0 -1px 0 rgba(20,24,28,0.07),
      0 0 0 1px rgba(20,24,28,0.09);

    --pw-g-depth:0 2px 8px -4px rgba(20,24,28,0.18);
    --pw-g-depth-float:0 20px 44px -22px rgba(20,24,28,0.32);

    --pw-g-amber-inner:
      inset 0 1px 0 rgba(255,255,255,0.85),
      inset 0 -12px 20px -10px rgba(255,150,20,0.42),
      inset 0 -1px 0 rgba(140,80,0,0.16),
      0 0 0 1px rgba(184,109,2,0.34),
      0 4px 14px -7px rgba(255,150,20,0.40);
    --pw-g-amber-wash:radial-gradient(95% 130% at 50% 132%,
        rgba(255,168,61,0.45) 0%, rgba(255,168,61,0) 72%);
    --pw-g-green-inner:
      inset 0 1px 0 rgba(255,255,255,0.85),
      inset 0 -12px 20px -10px rgba(31,160,110,0.36),
      inset 0 -1px 0 rgba(0,70,45,0.14),
      0 0 0 1px rgba(14,122,82,0.34),
      0 4px 14px -7px rgba(31,160,110,0.36);
    --pw-g-green-wash:radial-gradient(95% 130% at 50% 132%,
        rgba(31,160,110,0.32) 0%, rgba(31,160,110,0) 72%);

    /* The light head is near-white, so the page's muted already clears AA
       on it (6.28:1) — light needs the mapping, not a new colour. */
    --pw-label-on-glass: var(--pw-text-muted);

    /* The segmented lens inverts like everything else: it is a pale pane
       lifted out of a pale dip, so it is defined by a soft dark edge below
       and a white lit edge above, not by a black drop shadow. */
    --pw-g-seg-wash:radial-gradient(100% 140% at 50% 138%, rgba(255,168,61,0.34) 0%, rgba(255,168,61,0) 74%);
    --pw-g-seg-lens:
      inset 0 1px 0 rgba(255,255,255,0.95),
      inset 0 -1px 0 rgba(20,24,28,0.07),
      0 0 0 1px rgba(20,24,28,0.09),
      0 2px 5px -2px rgba(20,24,28,0.20);

    --pw-g-focus-ring:0 0 0 2px rgba(154,91,2,0.55), 0 0 0 6px rgba(255,168,61,0.22);

    /* The chrome from v1.20.0, restated for a pale ground. */
    --pw-glass-dock:rgba(255,255,255,0.80);
    --pw-glass-top:rgba(230,232,228,0.80);
    --pw-glass-edge:inset 0 1px 0 rgba(255,255,255,0.95);
  }

  /* The identity avatars are the one palette with no token behind them: six
     hard-coded pastels chosen to glow on a black page. On white they are all
     but invisible (the blue measured 1.48:1), so light gets its own set at the
     same hues, taken down to the depth a pale tint needs. */
  :root[data-theme="light"] .id-avatar-2{ background:rgba(42,88,127,0.14);  color:#2A587F; }
  :root[data-theme="light"] .id-avatar-3{ background:rgba(122,59,158,0.13); color:#7A3B9E; }
  :root[data-theme="light"] .id-avatar-4{ background:rgba(163,59,59,0.12);  color:#A33B3B; }
  :root[data-theme="light"] .id-avatar-5{ background:rgba(32,110,120,0.13); color:#206E78; }

  /* Wells invert with the page: a recess in a light surface is a soft dark
     dip, not a black hole. Stated as rules rather than tokens because the
     shapes are written inline in the glass section above. */
  :root[data-theme="light"] .field,
  :root[data-theme="light"] .search-input,
  :root[data-theme="light"] .search-row,
  :root[data-theme="light"] .qty-controls,
  :root[data-theme="light"] .check{
    background-image:linear-gradient(to bottom,
        rgba(20,24,28,0.06) 0%, rgba(255,255,255,0.10) 78%, rgba(255,255,255,0.65) 100%);
    box-shadow:
      inset 0 2px 4px rgba(20,24,28,0.10),
      inset 0 -1px 0 rgba(255,255,255,0.85),
      0 0 0 1px rgba(20,24,28,0.10);
  }
  /* Focus has to outrank the wells rule above. `.search-input:focus` is
     (0,2,0) and `:root[data-theme="light"] .search-input` is (0,3,0), so the
     resting recess was winning and the focus ring never painted in light —
     the same specificity trap as the switch, and this one costs keyboard
     users the only cue they get. Stated at (0,4,0). */
  :root[data-theme="light"] .field:focus,
  :root[data-theme="light"] .search-input:focus,
  :root[data-theme="light"] .search-row:focus-within{
    outline:none;
    box-shadow:inset 0 2px 4px rgba(20,24,28,0.10), var(--pw-g-focus-ring);
  }
  :root[data-theme="light"] .btn-primary{
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.7),
      inset 0 -1px 0 rgba(150,90,0,0.30),
      0 0 0 1px rgba(184,109,2,0.30),
      0 5px 16px -8px rgba(255,150,20,0.55),
      var(--pw-g-depth);
  }
  :root[data-theme="light"] .btn-danger{
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.75),
      inset 0 -1px 0 rgba(120,40,30,0.12),
      0 0 0 1px rgba(179,51,37,0.30),
      var(--pw-g-depth);
  }
  /* A picked tick stays a filled green disc; only its lighting changes. */
  :root[data-theme="light"] .item-row.picked .check{
    background-color:var(--green);
    background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.75),
      inset 0 -1px 0 rgba(0,70,45,0.30),
      0 0 0 1px rgba(14,122,82,0.35),
      0 4px 12px -6px rgba(31,160,110,0.45);
  }
  /* The switch is stated on its own rather than folded into the wells group
     above: that group's selector outranks `.switch-track.on`, so grouping it
     painted the ON state with the OFF state's recess and the toggle stopped
     saying anything. Off is a pale dip with a white knob; on is a solid amber
     fill with dark ink on it, which is the one pairing already measured. */
  :root[data-theme="light"] .switch-track{
    background-color:rgba(20,24,28,0.10);
    background-image:none;
    box-shadow:
      inset 0 2px 4px rgba(20,24,28,0.14),
      inset 0 -1px 0 rgba(255,255,255,0.85),
      0 0 0 1px rgba(20,24,28,0.10);
  }
  :root[data-theme="light"] .switch-track::after{
    background:#FFFFFF;
    box-shadow:0 1px 3px rgba(20,24,28,0.30);
  }
  :root[data-theme="light"] .switch-track.on{
    background-color:var(--amber);
    background-image:var(--pw-g-spec);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.65),
      inset 0 -1px 0 rgba(140,80,0,0.28),
      0 0 0 1px rgba(184,109,2,0.36);
  }
  :root[data-theme="light"] .switch-track.on::after{
    background:var(--pw-accent-ink);
    box-shadow:0 1px 3px rgba(80,45,0,0.35);
  }

  /* Pressed: the piece dips into the page rather than going dark. */
  :root[data-theme="light"] .btn:active,
  :root[data-theme="light"] .chip:active,
  :root[data-theme="light"] .filter-chip:active,
  :root[data-theme="light"] .store-tabs .tag:active,
  :root[data-theme="light"] .aisle-strip [data-aisle-cat]:active,
  :root[data-theme="light"] .icon-btn:active,
  :root[data-theme="light"] .qty-btn:active{
    box-shadow:
      inset 0 2px 6px rgba(20,24,28,0.16),
      inset 0 -1px 0 rgba(255,255,255,0.7),
      0 0 0 1px rgba(20,24,28,0.11);
  }

  /* ---- the segmented Appearance control ---- */
  .settings-row-stack{
    display:flex; flex-direction:column; align-items:stretch; gap:10px;
    text-align:left;
  }
  .seg-control{
    display:flex; gap:4px;
    padding:4px;
    border-radius:var(--pw-radius-pill, 999px);
    background-color:var(--pw-g-fill-sunken);
    box-shadow:inset 0 2px 5px rgba(0,0,0,0.35), 0 0 0 1px var(--pw-hairline);
  }
  .seg-option{
    flex:1; min-width:0;
    padding:8px 10px;
    border:none; border-radius:var(--pw-radius-pill, 999px);
    background:none;
    color:var(--pw-text-muted);
    font-size:13px; font-weight:700;
    min-height:var(--pw-tap-compact);
    transition:color var(--pw-motion-quick);
  }
  .seg-option.on{
    color:var(--pw-accent-text);
    background-color:rgba(255,255,255,0.09);
    background-image:var(--pw-g-amber-wash), var(--pw-g-spec);
    box-shadow:var(--pw-g-amber-inner);
  }
  :root[data-theme="light"] .seg-control{
    box-shadow:inset 0 2px 4px rgba(20,24,28,0.10), 0 0 0 1px var(--pw-hairline);
  }
  :root[data-theme="light"] .seg-option.on{
    background-color:rgba(255,255,255,0.92);
  }


  /* ================================================================
     LIQUID GLASS MATERIAL SYSTEM  (revision 2)

     Revision 1 gave all 27 component families a translucent fill and a 1px
     accent border, and the review was right that it still read as "a dark
     button with an orange border" rather than a shaped piece of glass. The
     arithmetic says exactly why: a 0.055 white fill over an #0B0C0D page
     composites to rgb(24,25,26). Thirteen points off the background. It is
     genuinely translucent and completely invisible, because on a near-black
     page there is nothing behind a chip to show through it.

     So transparency cannot be what sells this, and raising it further would
     only wash out text. What makes something read as glass is how light
     behaves ON it:

       · a RIM that is directional — bright along the top where light lands,
         dark along the bottom where the piece meets the surface, dim at the
         sides. A uniform 1px border is a drawn outline; this is a lit edge.
       · a SPECULAR TERMINATOR — the hard step partway down the pane where
         the highlight stops. That single discontinuity is the strongest cue
         that a surface is curved and glossy rather than flat.
       · INTERNAL ILLUMINATION for accents. Colour glows from inside the
         piece instead of tracing its outline, which is the difference the
         review asked for: colour should illuminate the glass, not become it.
       · DEPTH — the piece casts a shadow, so it sits above the page.

     Tier A (blurred, floating) and Tier B (unblurred, in-content) are kept
     from revision 1 for the same reason: a sixty-item run would otherwise
     put a hundred blurred layers on screen.

     ON COLOUR. Pikwise uses amber for "do this" and green for "done"; the
     logo carries both. Nothing is recoloured. What changed from revision 1
     is where the accent lives — the rim's amber drops from 0.42 to a
     hairline, and the colour moves inside the piece as a glow.
     ================================================================ */
  :root{
    /* Base fills. Higher than revision 1 so a piece separates from the page
       at all, but the lighting below does the real work. */
    --pw-g-fill:        rgba(255,255,255,0.10);
    --pw-g-fill-raised: rgba(255,255,255,0.14);
    --pw-g-fill-sunken: rgba(0,0,0,0.30);
    --pw-g-fill-float:  rgba(32,35,42,0.70);

    --pw-g-blur:        blur(14px) saturate(150%);
    --pw-g-blur-strong: blur(26px) saturate(165%);

    /* The specular sweep. The step between 46% and 47% is the terminator —
       the edge of the highlight — and it is what stops this reading as a
       flat gradient. */
    --pw-g-spec: linear-gradient(to bottom,
        rgba(255,255,255,0.155) 0%,
        rgba(255,255,255,0.065) 46%,
        rgba(255,255,255,0.012) 47%,
        rgba(255,255,255,0.055) 100%);
    /* A second, softer light source catching the top-left corner. */
    --pw-g-spec-corner: radial-gradient(128% 86% at 16% -8%,
        rgba(255,255,255,0.20) 0%, rgba(255,255,255,0) 58%);

    /* The dimensional rim, as one reusable stack. */
    --pw-g-rim:
      inset 0 1px 0 rgba(255,255,255,0.28),
      inset 0 -1px 0 rgba(0,0,0,0.42),
      inset 1px 0 0 rgba(255,255,255,0.07),
      inset -1px 0 0 rgba(255,255,255,0.07),
      0 0 0 1px rgba(255,255,255,0.10);
    --pw-g-rim-soft:
      inset 0 1px 0 rgba(255,255,255,0.18),
      inset 0 -1px 0 rgba(0,0,0,0.34),
      0 0 0 1px rgba(255,255,255,0.075);

    --pw-g-depth:       0 4px 12px -6px rgba(0,0,0,0.68);
    --pw-g-depth-float: 0 24px 50px -24px rgba(0,0,0,0.85);

    /* Accents: a hairline of colour on the rim, and the rest of it burning
       inside the piece. */
    --pw-g-amber-inner:
      inset 0 1px 0 rgba(255,214,160,0.40),
      inset 0 -12px 20px -10px rgba(255,168,61,0.52),
      inset 0 -1px 0 rgba(0,0,0,0.34),
      0 0 0 1px rgba(255,168,61,0.26),
      0 4px 18px -7px rgba(255,168,61,0.42);
    --pw-g-amber-wash: radial-gradient(95% 130% at 50% 132%,
        rgba(255,168,61,0.40) 0%, rgba(255,168,61,0) 72%);
    --pw-g-green-inner:
      inset 0 1px 0 rgba(180,245,215,0.40),
      inset 0 -12px 20px -10px rgba(55,196,140,0.52),
      inset 0 -1px 0 rgba(0,0,0,0.34),
      0 0 0 1px rgba(55,196,140,0.28),
      0 4px 18px -7px rgba(55,196,140,0.42);
    --pw-g-green-wash: radial-gradient(95% 130% at 50% 132%,
        rgba(55,196,140,0.40) 0%, rgba(55,196,140,0) 72%);

    --pw-g-focus-ring: 0 0 0 2px rgba(255,168,61,0.6), 0 0 0 6px rgba(255,168,61,0.15);
  }

  /* ---- Tier B: shaped glass, no blur ---- */
  .btn-ghost,
  .chip,
  .store-tabs .tag,
  .aisle-strip [data-aisle-cat],
  .icon-btn,
  .aisle-arrow,
  .pw-aside-card{
    background-color:var(--pw-g-fill);
    background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
    border:none;
    box-shadow:var(--pw-g-rim), var(--pw-g-depth);
  }

  /* The primary action is a piece of amber glass, not a flat amber slab:
     the same lighting, over the accent rather than over the page. */
  .btn-primary{
    background-color:var(--amber);
    background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
    border:none;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.45),
      inset 0 -1px 0 rgba(120,70,0,0.40),
      0 0 0 1px rgba(255,190,110,0.35),
      0 6px 20px -8px rgba(255,168,61,0.55),
      var(--pw-g-depth);
  }
  .btn-danger{
    background-color:var(--pw-danger-soft);
    background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
    border:none;
    box-shadow:
      inset 0 1px 0 rgba(255,190,180,0.30),
      inset 0 -1px 0 rgba(0,0,0,0.34),
      0 0 0 1px rgba(226,96,79,0.34),
      var(--pw-g-depth);
  }

  /* Wells are the same material seen from the other side — light enters at
     the bottom lip rather than the top edge, so they read as pressed into
     the surface instead of resting on it. */
  .field,
  .search-input,
  .search-row,
  .qty-controls,
  .check{
    background-color:var(--pw-g-fill-sunken);
    background-image:linear-gradient(to bottom,
        rgba(0,0,0,0.26) 0%, rgba(255,255,255,0.02) 78%, rgba(255,255,255,0.07) 100%);
    border:none;
    box-shadow:
      inset 0 2px 5px rgba(0,0,0,0.55),
      inset 0 -1px 0 rgba(255,255,255,0.10),
      0 0 0 1px rgba(255,255,255,0.075);
  }
  .field:focus,
  .search-input:focus{
    outline:none;
    box-shadow:inset 0 2px 5px rgba(0,0,0,0.55), var(--pw-g-focus-ring);
  }
  .search-row:focus-within{
    box-shadow:inset 0 2px 5px rgba(0,0,0,0.55), var(--pw-g-focus-ring);
  }
  .search-row .scan-btn:active{
    background-color:rgba(255,168,61,0.18);
    box-shadow:inset 0 2px 8px rgba(0,0,0,0.35);
  }

  /* Selected: the piece lights up FROM WITHIN. The rim keeps only a
     hairline of colour — revision 1 put 0.42 there and that outline was
     what made these read as bordered pills. */
  .chip.on,
  .store-tabs .tag.active,
  .aisle-strip [data-aisle-cat].on,
  .aisle-strip [data-aisle-cat].active{
    background-color:rgba(255,255,255,0.09);
    background-image:var(--pw-g-amber-wash), var(--pw-g-spec-corner), var(--pw-g-spec);
    box-shadow:var(--pw-g-amber-inner), var(--pw-g-depth);
    color:var(--pw-accent-text);
  }
  .chip.shop-chip.on{
    background-color:rgba(255,255,255,0.09);
    background-image:var(--pw-g-green-wash), var(--pw-g-spec-corner), var(--pw-g-spec);
    box-shadow:var(--pw-g-green-inner), var(--pw-g-depth);
    color:var(--pw-success-text);
  }
  .item-row.picked .check{
    background-color:var(--green);
    background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
    box-shadow:
      inset 0 1px 0 rgba(210,255,235,0.55),
      inset 0 -1px 0 rgba(0,70,45,0.45),
      0 0 0 1px rgba(150,240,205,0.35),
      0 5px 16px -7px rgba(55,196,140,0.6);
  }

  /* Pressed: the highlight collapses and the piece sinks. Removing the top
     specular is what sells the compression — a glass surface stops catching
     the light when you push it below the plane. */
  .btn:active,
  .chip:active,
  .filter-chip:active,
  .store-tabs .tag:active,
  .aisle-strip [data-aisle-cat]:active,
  .icon-btn:active,
  .qty-btn:active{
    background-image:none;
    box-shadow:
      inset 0 2px 7px rgba(0,0,0,0.5),
      inset 0 -1px 0 rgba(255,255,255,0.06),
      0 0 0 1px rgba(255,255,255,0.07);
  }

  .btn:disabled,
  .btn[disabled],
  .aisle-arrow:disabled{
    background-image:none;
    box-shadow:0 0 0 1px rgba(255,255,255,0.05);
  }

  /* A switch is the one control whose whole job is showing its state. */
  .switch-track{
    background-color:var(--pw-g-fill-sunken);
    border:none;
    box-shadow:inset 0 2px 5px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.07);
  }
  .switch-track.on,
  .switch-track[data-on="1"],
  input:checked + .switch-track{
    background-color:rgba(255,255,255,0.09);
    background-image:var(--pw-g-amber-wash), var(--pw-g-spec);
    box-shadow:var(--pw-g-amber-inner);
  }

  /* ---- Tier A: floating surfaces, with the blur ----
     The review called the modal the closest thing to the target, so it
     keeps its depth and gains the same lit rim and specular as everything
     else, rather than being left as the one piece with a different logic. */
  @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
    .suggest-list,
    .modal{
      background-color:var(--pw-g-fill-float);
      background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
      -webkit-backdrop-filter:var(--pw-g-blur-strong);
      backdrop-filter:var(--pw-g-blur-strong);
      border:none;
      box-shadow:var(--pw-g-rim), var(--pw-g-depth-float);
    }
    .modal-backdrop{
      -webkit-backdrop-filter:blur(7px) saturate(118%);
      backdrop-filter:blur(7px) saturate(118%);
    }
    .qty-controls{
      -webkit-backdrop-filter:var(--pw-g-blur);
      backdrop-filter:var(--pw-g-blur);
    }
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
    .suggest-list,
    .modal{
      background-color:rgba(32,35,42,0.97);
      background-image:var(--pw-g-spec-corner), var(--pw-g-spec);
      border:none;
      box-shadow:var(--pw-g-rim), var(--pw-g-depth-float);
    }
  }

  /* Rows inside a glass surface stay transparent or they paint over the
     translucency the surface just gained. */
  .suggest-item{ background-color:transparent; }
  .suggest-item:active,
  .suggest-item.sel,
  .suggest-item[aria-selected="true"]{
    background-image:var(--pw-g-amber-wash);
    box-shadow:inset 0 1px 0 rgba(255,255,255,0.10);
  }

  /* The selected dock item sits on a lit pane of the same glass. */
  .pw-dock-item.on::after{
    content:"";
    position:absolute;
    left:50%; top:50%;
    width:64px; height:40px;
    transform:translate(-50%,-50%);
    border-radius:14px;
    background-color:rgba(255,255,255,0.055);
    /* A gentler wash than the chips use. The dock label sits directly on
       this pane and at full strength the amber-on-amber contrast measured
       4.43:1 — under AA. Chips carry their label clear of the brightest
       part of the glow, so they keep the stronger wash; this one does not,
       so it gets a dimmer light rather than the whole effect being
       weakened everywhere to suit the smallest case. */
    background-image:radial-gradient(95% 130% at 50% 138%,
        rgba(255,168,61,0.24) 0%, rgba(255,168,61,0) 72%), var(--pw-g-spec);
    box-shadow:
      inset 0 1px 0 rgba(255,214,160,0.26),
      inset 0 -1px 0 rgba(0,0,0,0.30),
      0 0 0 1px rgba(255,168,61,0.24),
      0 3px 14px -7px rgba(255,168,61,0.38);
    z-index:-1;
    pointer-events:none;
  }

  @media (min-width:768px){
    /* Wider mobile / tablet tier — same single-column workflow, more
       breathing room and slightly roomier controls now that there's
       width to spare. Nothing here changes structure or behaviour. */
    main{ padding:var(--pw-sp-2) var(--pw-sp-6) calc(100px + var(--pw-dock-h) + env(safe-area-inset-bottom)); }
    .item-row{ padding:var(--pw-sp-3) var(--pw-sp-3) var(--pw-sp-3) var(--pw-sp-4); gap:var(--pw-sp-3); }
    .item-name{ font-size:var(--pw-fs-body-lg); }
    .category-label{ font-size:12px; }
    .chip, .filter-chip{ padding:9px 14px; }
    .tag{ padding:10px 18px; }
  }

  @media (min-width:768px) and (max-width:1023.98px){
    #app{ max-width:var(--pw-content-max); }
  }

  @media (min-width:1024px){
    body{ background:var(--pw-bg); }
    #pw-shell{
      display:flex; align-items:stretch; min-height:100vh;
      max-width:1600px; margin:0 auto;
    }
    #pw-sidenav{
      display:flex; flex-direction:column;
      width:var(--pw-sidenav-w); flex:0 0 auto;
      background:var(--pw-surface);
      border-right:1px solid var(--pw-hairline);
      position:sticky; top:0; height:100vh; overflow-y:auto;
    }
    #pw-sidenav[hidden]{ display:none; }
    #pw-workspace{
      flex:1; min-width:0; display:flex; justify-content:center;
      background:var(--pw-bg);
    }
    #app{
      max-width:var(--pw-content-max-lg);
      width:100%;
      border-left:1px solid var(--pw-hairline);
      border-right:1px solid var(--pw-hairline);
    }
    /* Fixed-to-viewport + margin:auto only centers correctly when #app
       IS the viewport's centered column (true on mobile/tablet). With a
       sidenav pushing the workspace off-center, the summary bar must
       instead sit at the bottom of its own #app column: sticky keeps it
       pinned while scrolling, same as fixed did, but sized/positioned
       by its normal position inside #app rather than the full viewport. */
    .summary-bar{ position:sticky; max-width:none; width:100%; bottom:0; }
    /* The sidenav covers the same five destinations the dock covers on
       mobile/tablet — never both at once. */
    #pw-dock{ display:none; }
    main{ padding-bottom:100px; }

    /* The persistent sidenav already provides Order/Products/Shops &
       Sharing/History/Settings navigation, so the mobile-topbar's logo
       strip would just be a second, redundant way to get back to Order
       — same reasoning the mobile page-tabs/swipe-hint and the Order
       header's settings-gear were removed outright rather than hidden
       (see renderOrder/renderManage in js/app.js). .back-row (Admin
       only — the one screen the sidenav's own Admin entry still needs a
       real "back" for) is hidden here for the same reason. */
    .mobile-topbar, .back-row{ display:none; }
    #pw-aside{
      display:flex; flex-direction:column;
      width:var(--pw-aside-w); flex:0 0 auto;
      background:var(--pw-surface);
      border-left:1px solid var(--pw-hairline);
      position:sticky; top:0; height:100vh; overflow-y:auto;
    }
    #pw-aside[hidden]{ display:none; }

    /* A button that means "do the one thing this card is for" doesn't
       need to span a 700px-wide card just because it did on a 350px
       phone. Scoped to .manage-section (Shops/History/Settings/Add
       Products) — the Finish Run modal's stacked buttons live outside
       #app entirely and are unaffected, correctly staying full-width
       inside their own much narrower modal. */
    .manage-section .btn-block{
      display:inline-flex; width:auto; min-width:200px;
    }

    /* Toasts/modals are viewport-fixed and centre via left:0/right:0 +
       margin:auto, which is only really "centred" when #app itself is
       the viewport's centred column (true on mobile/tablet). With the
       sidenav now taking real width, shift that centring zone to start
       after it — not a perfect centre on the workspace alone (the
       aside's width still isn't accounted for), but far closer than
       centring on the full 1440px+ viewport, and with zero change to
       how either one actually behaves. */
    .toast{ left:var(--pw-sidenav-w); bottom:24px; }
    .modal-backdrop{ left:var(--pw-sidenav-w); }
  }

  @media (min-width:1440px){
    #app{ max-width:840px; }
    #pw-aside{ width:312px; }
  }

  /* ---- desktop sidenav (Phase B3) ---- */
  .pw-nav-brand{ display:flex; align-items:center; gap:10px; padding:var(--pw-sp-4) var(--pw-sp-4) var(--pw-sp-5); }
  .pw-nav-brand .logo-mark{ width:28px; height:28px; flex:0 0 auto; }
  .pw-nav-brand span{ font-weight:800; letter-spacing:-0.005em; text-transform:uppercase; font-size:14px; color:var(--pw-text); }
  .pw-nav-list{ display:flex; flex-direction:column; gap:2px; padding:0 var(--pw-sp-3); }
  /* Restrained illumination, same idiom as the mobile dock's active glow —
     a left accent + soft tint, not a bordered box. */
  .pw-nav-item{
    position:relative;
    display:flex; align-items:center; text-align:left; width:100%;
    padding:11px var(--pw-sp-3) 11px calc(var(--pw-sp-3) + 3px);
    border-radius:var(--pw-radius-sm);
    border:none; background:transparent;
    color:var(--pw-text-soft); font-size:14.5px; font-weight:600;
    min-height:var(--pw-tap-min);
    transition:background-color var(--pw-motion-quick), color var(--pw-motion-quick);
  }
  .pw-nav-item:hover{ background:var(--pw-surface-raised); }
  .pw-nav-item.on{ background:var(--pw-accent-soft); color:var(--pw-accent-text); }
  .pw-nav-item.on::before{
    content:""; position:absolute; left:0; top:9px; bottom:9px; width:3px;
    border-radius:2px; background:var(--pw-accent);
  }
  .pw-nav-divider{ height:1px; background:var(--pw-hairline); margin:var(--pw-sp-3); }
  .pw-nav-admin{ color:var(--pw-text-muted); }
  .pw-nav-admin.on{ background:var(--pw-surface-raised); color:var(--pw-text); }
  .pw-nav-admin.on::before{ background:var(--pw-border-strong); }
  .pw-nav-footer{ margin-top:auto; padding:var(--pw-sp-4) var(--pw-sp-3); border-top:1px solid var(--pw-hairline); font-size:12px; color:var(--pw-text-muted); }
  /* Was unstyled beyond the footer's own base text colour — plain
     sync-status text with no visual weight of its own at the bottom of
     an otherwise considered sidenav. */
  .sync-chip{ display:flex; align-items:center; font-size:11.5px; font-weight:600; letter-spacing:0.01em; }

  /* ---- contextual aside — structural styles now, content wired in B5 ---- */
  .pw-aside-card{
    padding:var(--pw-sp-4) var(--pw-sp-3);
    /* One step up from the aside's own flat surface, no traced border —
       an instrument panel built into the sidebar, not a card sitting on
       top of it. The top highlight is the only edge treatment. */
    background:var(--pw-surface-2);
    border:none;
    border-radius:var(--pw-radius-md);
    box-shadow:var(--pw-inset-highlight);
  }
  .pw-aside-eyebrow{
    font-size:var(--pw-fs-caption); text-transform:uppercase; letter-spacing:0.1em;
    color:var(--pw-text-muted); font-weight:700; margin-bottom:var(--pw-sp-3);
  }
  .pw-aside-row{ display:flex; justify-content:space-between; gap:var(--pw-sp-2); font-size:var(--pw-fs-small); color:var(--pw-text-muted); padding:5px 0; }
  .pw-aside-row b{ color:var(--pw-text); font-weight:700; text-align:right; }
  .pw-aside-progress{ height:8px; border-radius:4px; background:var(--pw-surface-raised); overflow:hidden; margin:var(--pw-sp-3) 0 4px; }
  .pw-aside-fill{ height:100%; background:var(--pw-accent); border-radius:4px; transition:width var(--pw-pick-transition); }
  .pw-aside-pct{ font-size:12px; font-weight:800; color:var(--pw-text); text-align:right; margin-bottom:var(--pw-sp-2); }
