/* Only for the "Hand-Drawn / Sketch" style variant (data-style="sketch"),
   loaded unconditionally up front since @import must precede other rules —
   costs nothing when that style isn't selected (the browser still fetches
   it lazily-ish, but there's no per-style CSS file splitting in this
   project, so a single shared stylesheet is the existing convention). */
@import url('https://fonts.googleapis.com/css2?family=Architects+Daughter&display=swap');

/* OneClickPOSOrder — hand-written, no framework, mobile-first.

   Design direction: cool blue-grey neutrals + brand blue accent, sampled from the
   real One Click Solutions mark (the tapping-hand icon) — soft NEUTRAL shadows,
   generous whitespace, confident calm typography. Not reusing OneClickPOSWeb's SB
   Admin 2 theme (admin dashboard chrome, documented mobile scrollbar bug);
   StockEntry.cshtml already proves working around the shared theme with page-scoped
   tokens is the right path in this codebase. */

:root {
  --ocp-page: #F3F6F8;
  --ocp-surface: #FFFFFF;
  --ocp-ink: #16202A;
  --ocp-ink-soft: #47576A;
  --ocp-muted: #7E8FA0;
  --ocp-line: #E1E8ED;

  --ocp-brand: #0B6E9E;
  --ocp-brand-dark: #075277;
  --ocp-brand-ink: #FFFFFF;
  --ocp-brand-soft: #DCEEF6;

  --ocp-ok: #10B981;
  --ocp-ok-soft: #DCFCE7;
  --ocp-warn: #B45309;
  --ocp-warn-soft: #FEF3C7;
  --ocp-bad: #B91C1C;
  --ocp-bad-soft: #FDECEC;

  --ocp-r: 18px;
  --ocp-r-sm: 12px;
  --ocp-r-pill: 999px;
  /* Neutral, low-opacity shadows only — no brand-colored glow anywhere. */
  --ocp-shadow: 0 1px 2px rgba(26, 25, 23, .04), 0 3px 10px rgba(26, 25, 23, .05);
  --ocp-shadow-float: 0 2px 6px rgba(26, 25, 23, .16);
  --ocp-ripple: rgba(26, 25, 23, .12);
  --ocp-tap: 46px;
  --ocp-cartbar-h: 82px;
}

/* Dark mode: respects the OS preference by default, overridable by an explicit
   toggle (wwwroot/js/theme.js sets data-theme on <html>, persisted per-device via
   localStorage — never assumed shared with any Claude Artifact viewer, this is a
   plain web page). Kept in the same cool blue-grey family as light mode (a deep
   navy, not a warm near-black), same brand accent role, shadows swapped for a touch
   more opacity since a soft shadow barely reads against a dark surface. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ocp-page: #0E1620;
    --ocp-surface: #17222E;
    --ocp-ink: #EAF0F5;
    --ocp-ink-soft: #B9C6D3;
    --ocp-muted: #7E8FA0;
    --ocp-line: #263542;

    --ocp-brand: #4FA8D8;
    --ocp-brand-dark: #0B6E9E;
    --ocp-brand-soft: #0F2C3A;

    --ocp-ok-soft: #103524;
    --ocp-warn-soft: #3A2A08;
    --ocp-bad-soft: #3A1512;

    --ocp-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 3px 12px rgba(0, 0, 0, .32);
    --ocp-shadow-float: 0 2px 8px rgba(0, 0, 0, .5);
    --ocp-ripple: rgba(255, 255, 255, .14);
  }
}
:root[data-theme="dark"] {
  --ocp-page: #0E1620;
  --ocp-surface: #17222E;
  --ocp-ink: #EAF0F5;
  --ocp-ink-soft: #B9C6D3;
  --ocp-muted: #7E8FA0;
  --ocp-line: #263542;

  --ocp-brand: #4FA8D8;
  --ocp-brand-dark: #0B6E9E;
  --ocp-brand-soft: #0F2C3A;

  --ocp-ok-soft: #103524;
  --ocp-warn-soft: #3A2A08;
  --ocp-bad-soft: #3A1512;

  --ocp-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 3px 12px rgba(0, 0, 0, .32);
  --ocp-shadow-float: 0 2px 8px rgba(0, 0, 0, .5);
  --ocp-ripple: rgba(255, 255, 255, .14);
}

/* Selectable color scheme — independent of the theme/style picker
   (wwwroot/js/colorswitcher.js), and independent of light/dark. Applies on
   TOP OF whichever of the 12 looks is active, not just "Original": this is
   how a client's own brand color (e.g. their own brown/teal/whatever) threads
   through every look, the same way "Blue" already is One Click's own brand
   color layered onto the default Original look. Each rule below is written
   twice — once scoped `:not([data-style])` for Original, once scoped
   `[data-style]` for "any of the other 11 looks" — rather than one bare
   `[data-color="x"]` selector, so specificity (2 attribute selectors) always
   beats a style file's own single-attribute `[data-style="x"]` default block
   regardless of which stylesheet happens to load later in the cascade.
   "Blue" is simply the tokens already defined above / each style's own
   built-in default (no override needed — it's "use whatever's already
   there"). "Warm" restores this project's original terracotta/paper palette
   from before the blue rebrand, so picking it back is a real choice with a
   real identity, not an arbitrary alternate. */
html[data-color="warm"]:not([data-style]), html[data-style][data-color="warm"] {
  --ocp-page: #F5F3EE !important;
  --ocp-surface: #FFFFFF !important;
  --ocp-ink: #1A1917 !important;
  --ocp-ink-soft: #4A4844 !important;
  --ocp-muted: #8C887E !important;
  --ocp-line: #E7E2D8 !important;
  --ocp-brand: #C2410C !important;
  --ocp-brand-dark: #9A3412 !important;
  --ocp-brand-ink: #FFFFFF !important;
  --ocp-brand-soft: #F3E3D3 !important;
}
html[data-color="warm"]:not([data-style]):not([data-theme="light"]),
html[data-color="warm"]:not([data-style])[data-theme="dark"],
html[data-style][data-color="warm"]:not([data-theme="light"]),
html[data-style][data-color="warm"][data-theme="dark"] {
  --ocp-page: #18140F !important;
  --ocp-surface: #221D16 !important;
  --ocp-ink: #F3EFE7 !important;
  --ocp-ink-soft: #CBC4B7 !important;
  --ocp-muted: #938C7D !important;
  --ocp-line: #372F24 !important;
  --ocp-brand: #E2711D !important;
  --ocp-brand-dark: #C2410C !important;
  --ocp-brand-ink: #FFFFFF !important;
  --ocp-brand-soft: #3A2415 !important;
}

/* The other eight — accent-only swaps (--ocp-brand* only, page/surface/ink left
   at whatever the active look already set, unlike Warm above which restores a
   whole distinct historical palette). Each pair's dark-mode --ocp-brand is a
   brighter/lighter tint of its light-mode value for contrast against a dark
   surface — the same relationship already used for the default Blue (light
   #0B6E9E -> dark #4FA8D8) — and dark mode's --ocp-brand-dark is simply the
   light mode value, matching Blue's own pattern too. Same Original/any-style
   selector-doubling as Warm above, for the same specificity reason. */
/* Full palettes (Original look only, :not([data-style])) — same idea as Warm above:
   each color is a complete, distinct identity (page/surface/ink too, not just the
   accent), because that's the actual point of this feature per your own framing —
   "blue is the One Click Solutions palette, the others are like different palettes
   a client's own brand color becomes." Page background is that color's own light/
   dark "soft" tint (already existed as --ocp-brand-soft); surface/ink/muted stay
   close to Warm's own neutral-near-black/near-cream values (legible against any
   hue, not fighting the accent for attention) so these read as *considered*
   variations on one system, not eleven unrelated color experiments.
   When a non-Original LOOK is active instead ([data-style], any of the other 11),
   only the accent (--ocp-brand*) is overridden, same as before — those styles each
   already have their own deliberately-designed page/surface/ink, and swapping those
   too would fight the style's own palette rather than layer on top of it. */
/* Black — deliberately ASYMMETRIC, per your own spec: light mode only swaps the
   accent (buttons go black on the existing light background, same as every other
   color option); dark mode ALSO replaces the whole neutral scale with a true
   grayscale black (not the Original look's navy-tinted dark), with the accent
   inverted to white — "dark mode is actually the dark color used by the light
   mode" (a real black, not blue) with buttons flipped to white the same way
   light mode's buttons are black. Full neutral override is Original-look-only
   (:not([data-style])), matching Warm/Red/etc.'s own convention of never
   touching another look's own designed dark palette — [data-style] variants of
   Black stay accent-only in both themes, like the other eight colors. */
html[data-color="black"]:not([data-style]) { --ocp-brand: #18181B !important; --ocp-brand-dark: #000000 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #E4E4E7 !important; }
html[data-color="black"]:not([data-style]):not([data-theme="light"]), html[data-color="black"]:not([data-style])[data-theme="dark"] { --ocp-page: #000000 !important; --ocp-surface: #18181B !important; --ocp-ink: #FAFAFA !important; --ocp-ink-soft: #D4D4D8 !important; --ocp-muted: #A1A1AA !important; --ocp-line: #27272A !important; --ocp-brand: #FAFAFA !important; --ocp-brand-dark: #D4D4D8 !important; --ocp-brand-ink: #18181B !important; --ocp-brand-soft: #27272A !important; }
html[data-style][data-color="black"] { --ocp-brand: #18181B !important; --ocp-brand-dark: #000000 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #E4E4E7 !important; }
html[data-style][data-color="black"]:not([data-theme="light"]), html[data-style][data-color="black"][data-theme="dark"] { --ocp-brand: #D4D4D8 !important; --ocp-brand-dark: #18181B !important; --ocp-brand-ink: #18181B !important; --ocp-brand-soft: #27272A !important; }

html[data-color="red"]:not([data-style]) { --ocp-page: #FDE2E2 !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #F6C7C7 !important; --ocp-brand: #DC2626 !important; --ocp-brand-dark: #B91C1C !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDE2E2 !important; }
html[data-color="red"]:not([data-style]):not([data-theme="light"]), html[data-color="red"]:not([data-style])[data-theme="dark"] { --ocp-page: #3A1414 !important; --ocp-surface: #472222 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #4F2323 !important; --ocp-brand: #F87171 !important; --ocp-brand-dark: #DC2626 !important; --ocp-brand-ink: #2A0A0A !important; --ocp-brand-soft: #3A1414 !important; }
html[data-style][data-color="red"] { --ocp-brand: #DC2626 !important; --ocp-brand-dark: #B91C1C !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDE2E2 !important; }
html[data-style][data-color="red"]:not([data-theme="light"]), html[data-style][data-color="red"][data-theme="dark"] { --ocp-brand: #F87171 !important; --ocp-brand-dark: #DC2626 !important; --ocp-brand-ink: #2A0A0A !important; --ocp-brand-soft: #3A1414 !important; }

html[data-color="green"]:not([data-style]) { --ocp-page: #DCFCE7 !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #BBF0CE !important; --ocp-brand: #16A34A !important; --ocp-brand-dark: #15803D !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #DCFCE7 !important; }
html[data-color="green"]:not([data-style]):not([data-theme="light"]), html[data-color="green"]:not([data-style])[data-theme="dark"] { --ocp-page: #0F2D1A !important; --ocp-surface: #1B3B26 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #1E4028 !important; --ocp-brand: #4ADE80 !important; --ocp-brand-dark: #16A34A !important; --ocp-brand-ink: #06230F !important; --ocp-brand-soft: #0F2D1A !important; }
html[data-style][data-color="green"] { --ocp-brand: #16A34A !important; --ocp-brand-dark: #15803D !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #DCFCE7 !important; }
html[data-style][data-color="green"]:not([data-theme="light"]), html[data-style][data-color="green"][data-theme="dark"] { --ocp-brand: #4ADE80 !important; --ocp-brand-dark: #16A34A !important; --ocp-brand-ink: #06230F !important; --ocp-brand-soft: #0F2D1A !important; }

html[data-color="purple"]:not([data-style]) { --ocp-page: #EDE4FD !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #D7C6FA !important; --ocp-brand: #7C3AED !important; --ocp-brand-dark: #6D28D9 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #EDE4FD !important; }
html[data-color="purple"]:not([data-style]):not([data-theme="light"]), html[data-color="purple"]:not([data-style])[data-theme="dark"] { --ocp-page: #241A3D !important; --ocp-surface: #31254E !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #332851 !important; --ocp-brand: #A78BFA !important; --ocp-brand-dark: #7C3AED !important; --ocp-brand-ink: #1D1533 !important; --ocp-brand-soft: #241A3D !important; }
html[data-style][data-color="purple"] { --ocp-brand: #7C3AED !important; --ocp-brand-dark: #6D28D9 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #EDE4FD !important; }
html[data-style][data-color="purple"]:not([data-theme="light"]), html[data-style][data-color="purple"][data-theme="dark"] { --ocp-brand: #A78BFA !important; --ocp-brand-dark: #7C3AED !important; --ocp-brand-ink: #1D1533 !important; --ocp-brand-soft: #241A3D !important; }

html[data-color="pink"]:not([data-style]) { --ocp-page: #FCE4EF !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #F7C7DE !important; --ocp-brand: #DB2777 !important; --ocp-brand-dark: #BE185D !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FCE4EF !important; }
html[data-color="pink"]:not([data-style]):not([data-theme="light"]), html[data-color="pink"]:not([data-style])[data-theme="dark"] { --ocp-page: #3A1526 !important; --ocp-surface: #472232 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #4E2536 !important; --ocp-brand: #F472B6 !important; --ocp-brand-dark: #DB2777 !important; --ocp-brand-ink: #2E0D1E !important; --ocp-brand-soft: #3A1526 !important; }
html[data-style][data-color="pink"] { --ocp-brand: #DB2777 !important; --ocp-brand-dark: #BE185D !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FCE4EF !important; }
html[data-style][data-color="pink"]:not([data-theme="light"]), html[data-style][data-color="pink"][data-theme="dark"] { --ocp-brand: #F472B6 !important; --ocp-brand-dark: #DB2777 !important; --ocp-brand-ink: #2E0D1E !important; --ocp-brand-soft: #3A1526 !important; }

html[data-color="teal"]:not([data-style]) { --ocp-page: #D7F5F1 !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #B7E9E2 !important; --ocp-brand: #0D9488 !important; --ocp-brand-dark: #0F766E !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #D7F5F1 !important; }
html[data-color="teal"]:not([data-style]):not([data-theme="light"]), html[data-color="teal"]:not([data-style])[data-theme="dark"] { --ocp-page: #0B2C29 !important; --ocp-surface: #163936 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #1B3F3B !important; --ocp-brand: #2DD4BF !important; --ocp-brand-dark: #0D9488 !important; --ocp-brand-ink: #052622 !important; --ocp-brand-soft: #0B2C29 !important; }
html[data-style][data-color="teal"] { --ocp-brand: #0D9488 !important; --ocp-brand-dark: #0F766E !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #D7F5F1 !important; }
html[data-style][data-color="teal"]:not([data-theme="light"]), html[data-style][data-color="teal"][data-theme="dark"] { --ocp-brand: #2DD4BF !important; --ocp-brand-dark: #0D9488 !important; --ocp-brand-ink: #052622 !important; --ocp-brand-soft: #0B2C29 !important; }

html[data-color="orange"]:not([data-style]) { --ocp-page: #FDE6D3 !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #F9CFA6 !important; --ocp-brand: #EA580C !important; --ocp-brand-dark: #C2410C !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDE6D3 !important; }
html[data-color="orange"]:not([data-style]):not([data-theme="light"]), html[data-color="orange"]:not([data-style])[data-theme="dark"] { --ocp-page: #3A2008 !important; --ocp-surface: #472C14 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #4E2E12 !important; --ocp-brand: #FB923C !important; --ocp-brand-dark: #EA580C !important; --ocp-brand-ink: #2E1503 !important; --ocp-brand-soft: #3A2008 !important; }
html[data-style][data-color="orange"] { --ocp-brand: #EA580C !important; --ocp-brand-dark: #C2410C !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDE6D3 !important; }
html[data-style][data-color="orange"]:not([data-theme="light"]), html[data-style][data-color="orange"][data-theme="dark"] { --ocp-brand: #FB923C !important; --ocp-brand-dark: #EA580C !important; --ocp-brand-ink: #2E1503 !important; --ocp-brand-soft: #3A2008 !important; }

html[data-color="indigo"]:not([data-style]) { --ocp-page: #E4E3FC !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #CBC9F8 !important; --ocp-brand: #4F46E5 !important; --ocp-brand-dark: #4338CA !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #E4E3FC !important; }
html[data-color="indigo"]:not([data-style]):not([data-theme="light"]), html[data-color="indigo"]:not([data-style])[data-theme="dark"] { --ocp-page: #211D4A !important; --ocp-surface: #2B2758 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #2E2A54 !important; --ocp-brand: #818CF8 !important; --ocp-brand-dark: #4F46E5 !important; --ocp-brand-ink: #171441 !important; --ocp-brand-soft: #211D4A !important; }
html[data-style][data-color="indigo"] { --ocp-brand: #4F46E5 !important; --ocp-brand-dark: #4338CA !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #E4E3FC !important; }
html[data-style][data-color="indigo"]:not([data-theme="light"]), html[data-style][data-color="indigo"][data-theme="dark"] { --ocp-brand: #818CF8 !important; --ocp-brand-dark: #4F46E5 !important; --ocp-brand-ink: #171441 !important; --ocp-brand-soft: #211D4A !important; }

html[data-color="amber"]:not([data-style]) { --ocp-page: #FDECC8 !important; --ocp-surface: #FFFFFF !important; --ocp-ink: #1A1917 !important; --ocp-ink-soft: #4A4844 !important; --ocp-muted: #8C887E !important; --ocp-line: #F9DFA0 !important; --ocp-brand: #D97706 !important; --ocp-brand-dark: #B45309 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDECC8 !important; }
html[data-color="amber"]:not([data-style]):not([data-theme="light"]), html[data-color="amber"]:not([data-style])[data-theme="dark"] { --ocp-page: #3A2A04 !important; --ocp-surface: #473512 !important; --ocp-ink: #F3EFE7 !important; --ocp-ink-soft: #CBC4B7 !important; --ocp-muted: #938C7D !important; --ocp-line: #4E3A10 !important; --ocp-brand: #FBBF24 !important; --ocp-brand-dark: #D97706 !important; --ocp-brand-ink: #2E2004 !important; --ocp-brand-soft: #3A2A04 !important; }
html[data-style][data-color="amber"] { --ocp-brand: #D97706 !important; --ocp-brand-dark: #B45309 !important; --ocp-brand-ink: #FFFFFF !important; --ocp-brand-soft: #FDECC8 !important; }
html[data-style][data-color="amber"]:not([data-theme="light"]), html[data-style][data-color="amber"][data-theme="dark"] { --ocp-brand: #FBBF24 !important; --ocp-brand-dark: #D97706 !important; --ocp-brand-ink: #2E2004 !important; --ocp-brand-soft: #3A2A04 !important; }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--ocp-page);
  color: var(--ocp-ink);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
}

.ocp-money { font-variant-numeric: tabular-nums; }

a { color: inherit; }

.ocp-page {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 1rem calc(var(--ocp-cartbar-h) + 1.75rem);
  /* Named container so .ocp-grid's column count (below) can respond to the actual
     available width of THIS box, not the viewport. On desktop the cart side panel
     eats ~350px out of the viewport — a viewport-based media query has no way to
     know that and would offer more columns than actually fit, cramming cards up
     against (and visually reading as "under") the panel. A container query can't
     make that mistake: it only ever sees the width this element truly has. */
  container: ocp-page / inline-size;
}

/* Tablet/desktop: the cart becomes a persistent side panel instead of something you
   navigate away to see — there's room for it, so use it. The breakpoint is
   width-based, not device-based, so it responds to orientation changes for free: a
   phone rotated to landscape that crosses 900px gets the panel too, which is exactly
   the "enough room now" signal we actually care about. Below this width, `.ocp-layout`
   is a plain block container and everything behaves exactly as before. */
.ocp-cart-panel { display: none; }
@media (min-width: 900px) {
  .ocp-layout {
    display: flex;
    align-items: flex-start;
    gap: 1.75rem;
    max-width: 1280px;
    margin: 0 auto;
    padding: 0 1.5rem;
  }
  .ocp-layout .ocp-page {
    flex: 1;
    min-width: 0;
    max-width: none;
    margin: 0;
    padding: 0 0 2rem;
  }
  .ocp-cart-panel {
    display: block;
    width: 400px;
    flex: 0 0 400px;
    position: sticky;
    top: 1.5rem;
    background: var(--ocp-surface);
    border: 1px solid var(--ocp-line);
    border-radius: var(--ocp-r);
    padding: 1.25rem;
    box-shadow: var(--ocp-shadow);
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
  }
  .ocp-cart-panel h3 { margin: 0 0 0.9rem; font-size: 0.95rem; color: var(--ocp-ink-soft); }
  /* Scoped to .ocp-layout (Menu.cshtml only) — the side panel replaces the floating
     bar there. Cart.cshtml's OWN checkout bar uses the same .ocp-cartbar class but
     has no side-panel replacement, so it must stay visible at every width; hiding it
     globally here would leave desktop shoppers on /cart with no way to check out. */
  .ocp-layout .ocp-cartbar { display: none !important; }
  .ocp-scan-fab { right: calc(400px + 1.5rem + 1.1rem); }

  /* Staff Quick.cshtml — the exact same side-panel treatment as above, but only
     while Browse mode is active (staffquick.js's applyMode toggles this class on
     #ocp-quick-layout). Quick Search mode never gets this class, so it keeps its
     own single-column layout + bottom bar at every width — a side panel only makes
     sense for Browse mode's "this is the customer view" cart. */
  #ocp-quick-layout.ocp-browse-active {
    display: flex;
    align-items: flex-start;
    gap: 1.75rem;
    max-width: 1280px;
    margin: 0 auto;
    padding: 0 1.5rem;
  }
  #ocp-quick-layout.ocp-browse-active .ocp-page {
    flex: 1;
    min-width: 0;
    max-width: none;
    margin: 0;
    padding: 0 0 2rem;
  }
  #ocp-quick-layout.ocp-browse-active .ocp-cart-panel {
    display: block;
    width: 400px;
    flex: 0 0 400px;
    position: sticky;
    top: 1.5rem;
    background: var(--ocp-surface);
    border: 1px solid var(--ocp-line);
    border-radius: var(--ocp-r);
    padding: 1.25rem;
    box-shadow: var(--ocp-shadow);
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
  }
  #ocp-quick-layout.ocp-browse-active .ocp-cart-panel h3 { margin: 0 0 0.9rem; font-size: 0.95rem; color: var(--ocp-ink-soft); }
  /* #ocp-quick-bar and #ocp-quick-fab are siblings of #ocp-quick-layout, not
     descendants (they sit outside it, same as Menu.cshtml's own FAB) — the general
     sibling combinator, not a descendant selector, is what actually matches here. */
  #ocp-quick-layout.ocp-browse-active ~ #ocp-quick-bar { display: none !important; }
  #ocp-quick-layout.ocp-browse-active ~ #ocp-quick-fab { right: calc(400px + 1.5rem + 1.1rem); }
  /* The base `.ocp-cart-panel { display: block; }` rule above is NOT scoped to
     Menu.cshtml — it makes every .ocp-cart-panel-classed element visible at this
     width regardless of page or mode, which was silently showing Quick.cshtml's
     panel even during Quick Search mode (no side panel/cart concept there at all —
     that mode is deliberately a StockEntry.cshtml-style dense list, not a browsing
     surface). Two IDs beats one class, so this wins regardless of source order. */
  #ocp-quick-layout:not(.ocp-browse-active) #ocp-quick-cart-panel { display: none; }
  /* .ocp-layout .ocp-page's own desktop override just above (padding:0 0 2rem)
     assumes the fixed bottom bar is hidden at this width (true for Menu.cshtml,
     replaced by the side panel) — but Quick Search mode's bar stays visible at
     every width, so 2rem wasn't enough clearance and the fixed bar covered the
     bottom of the last product row/card. Restore the full bar-height clearance
     specifically while Search mode's bar is the visible one. */
  #ocp-quick-layout:not(.ocp-browse-active) .ocp-page {
    padding-bottom: calc(var(--ocp-cartbar-h) + 1.75rem);
  }

  /* TableOrder.cshtml (Restaurant) — same side-panel treatment as Quick.cshtml/
     Pos.cshtml's Browse mode above, for the exact same reason: there's room for
     it at this width, so the running order becomes a persistent panel instead of
     something scrolled past. No bottom cartbar exists on this page (order lines
     render inline, no fixed bar), so unlike Quick.cshtml there's no clearance
     override needed for non-Browse mode. */
  #ocp-to-layout.ocp-browse-active {
    display: flex;
    align-items: flex-start;
    gap: 1.75rem;
    max-width: 1280px;
    margin: 0 auto;
    padding: 0 1.5rem;
  }
  #ocp-to-layout.ocp-browse-active .ocp-page {
    flex: 1;
    min-width: 0;
    max-width: none;
    margin: 0;
    padding: 0 0 2rem;
  }
  #ocp-to-layout.ocp-browse-active .ocp-cart-panel {
    display: block;
    width: 400px;
    flex: 0 0 400px;
    position: sticky;
    top: 1.5rem;
    background: var(--ocp-surface);
    border: 1px solid var(--ocp-line);
    border-radius: var(--ocp-r);
    padding: 1.25rem;
    box-shadow: var(--ocp-shadow);
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
  }
  #ocp-to-layout.ocp-browse-active .ocp-cart-panel h3 { margin: 0 0 0.9rem; font-size: 0.95rem; color: var(--ocp-ink-soft); }
  /* Same reasoning as #ocp-quick-layout's own guard just above (2026-09) — the
     base `.ocp-cart-panel { display: block; }` rule at the top of this media
     query is NOT scoped to Menu.cshtml, so without this it would show
     TableOrder's panel unconditionally at this width regardless of mode. */
  #ocp-to-layout:not(.ocp-browse-active) #ocp-to-cart-panel { display: none; }
}

/* Top bar — flat, no gradient theatrics. A quiet chrome bar earns confidence from
   restraint and generous type, not decoration. */
.ocp-topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--ocp-page);
  border-bottom: 1px solid var(--ocp-line);
  padding: 1.15rem 1rem 1rem;
}

.ocp-topbar-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  max-width: 1120px;
  margin: 0 auto;
  /* Mobile-sizing bug fix (2026-09-04) — every icon button in the right-hand
     action group is flex:0 0 auto (deliberately never shrinks — accessibility
     touch-target size, not a bug), so a page with several icon groups (Tables/
     Pos: view/mode toggle + Goods Received + its refresh + board refresh/
     resync + menu = 5 groups, ~244px) has a hard floor that, alongside even a
     short branch name, genuinely exceeds a 320-375px phone's width (verified
     live: 54px real horizontal overflow at 320px before this fix). flex-wrap
     lets the action group drop to its own line instead of overflowing —
     harmless for every page whose action group already fits on one line
     (Login/Error/etc.), since wrap only activates when content doesn't fit. */
  flex-wrap: wrap;
}

.ocp-topbar h1 {
  font-size: 1.3rem;
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--ocp-ink);
}

.ocp-topbar .ocp-sub {
  font-size: 0.85rem;
  color: var(--ocp-muted);
  margin-top: 0.2rem;
}

.ocp-online-pill {
  font-size: 0.72rem;
  padding: 0.3rem 0.65rem;
  border-radius: var(--ocp-r-pill);
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  color: var(--ocp-muted);
  display: none;
  flex: 0 0 auto;
}
.ocp-online-pill.ocp-offline {
  display: inline-block;
  background: var(--ocp-bad-soft);
  border-color: transparent;
  color: var(--ocp-bad);
}
.ocp-online-pill.ocp-show { display: inline-block; }

/* Search — sits quietly in normal flow, neutral fill, no floating-over-gradient trick
   (there's no gradient to float over anymore). */
.ocp-search { margin: 1.1rem 0 0.9rem; }
.ocp-search-inner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-pill);
  padding: 0 0.4rem 0 1rem;
  height: 50px;
  /* Mobile-sizing bug fix (2026-09-04) — this class is used as a flex:1 child
     next to a fixed-size sibling (e.g. Menu.cshtml's #ocp-fav-filter star
     button) in several places. Without min-width:0, its default min-width:auto
     floors at the <input>'s own browser-default intrinsic minimum width
     (measured ~197px), which is wider than the space actually available once
     the fixed sibling is subtracted — the sibling then gets pushed past the
     row's own right edge instead of the input shrinking to make room.
     Verified live on Menu.cshtml at 320px: #ocp-fav-filter sat at x=319-355
     (fully off a 320px viewport) before this fix, causing 35px of real page
     horizontal overflow that every element below it (including the cart bar)
     inherited. min-width:0 lets the pill shrink to whatever room is left. */
  min-width: 0;
}
.ocp-search-inner svg { flex: 0 0 auto; color: var(--ocp-muted); }
/* .ocp-search-inner (not just .ocp-search) — several real usages of the pill
   (Browse mode's filter bar, Pos.cshtml's client-picker sheet filter) wrap the
   input directly in .ocp-search-inner with no .ocp-search ancestor, so the old
   .ocp-search-scoped selector never matched there at all — the input fell back
   to the browser's own default white background sitting on top of the
   properly-dark-styled pill container behind it (real bug, screenshotted). */
.ocp-search input[type="search"],
.ocp-search-inner input[type="search"] {
  flex: 1;
  height: 100%;
  border: none;
  background: transparent;
  font-size: 0.95rem;
  min-width: 0;
  color: var(--ocp-ink);
}
.ocp-search input[type="search"]:focus,
.ocp-search-inner input[type="search"]:focus { outline: none; }
.ocp-search-go {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: none;
  background: var(--ocp-ink);
  /* var(--ocp-page), not a hardcoded white — --ocp-ink flips to a light cream in
     dark mode, and white-on-light-cream is nearly unreadable. --ocp-page is always
     the opposite polarity of --ocp-ink in both themes, so this stays correct either
     way. Same fix applied to every other ink-filled active/pressed state below. */
  color: var(--ocp-page);
  font-size: 1rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity .12s ease;
}
.ocp-search-go:active { opacity: .7; }
/* staffpos.js/staffquick.js disable this while a triggerSearch() lookup is in
   flight (re-entrancy guard against a double-add on an exact-match scan) — the
   small-icon-button opacity idiom (matches .ocp-remove/.ocp-quick-row-icon's own
   :active dim) rather than a spinner, since this is a small 38px surface and the
   lookup is normally near-instant either way. Without this the disabled state
   was otherwise invisible (background is always the same --ocp-ink regardless
   of :disabled), giving no cue the go tap actually registered.
   :disabled, not :disabled:active — a disabled button can't BE active. */
.ocp-search-go:disabled { opacity: .5; cursor: not-allowed; }
/* Desktop/mouse only — see the file-wide (hover:hover)+(pointer:fine) note near
   .ocp-list-row/.ocp-card below. Ink-filled small button: same color-mix "nudge
   toward the opposite-polarity page token" idiom used for every other ink-filled
   control in this pass (reorder-add, chip/tab active state, mode-icon-btn active
   state) — a plain filter:brightness barely moves near-black/near-white ink, so a
   token-safe mix toward --ocp-page (guaranteed opposite polarity in both themes)
   is used instead. */
@media (hover: hover) and (pointer: fine) {
  .ocp-search-go:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}

/* Category picker (retail/barcode/pharmacy, when Branch.ProductOrderingStyle is
   Categorical) — deliberately a different pattern from the restaurant tab bar below:
   a retail catalog can have far more categories than a restaurant menu, where a
   horizontal tab strip would just overflow badly. A trigger pill + bottom sheet
   (the common Shopee/Shopify-storefront pattern) scales to any category count. */
.ocp-cat-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0 1rem;
  height: 42px;
  margin-bottom: 0.9rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  color: var(--ocp-ink);
  font-size: 0.86rem;
  font-weight: 600;
  cursor: pointer;
}
.ocp-cat-trigger svg { color: var(--ocp-muted); flex: 0 0 auto; }
/* Bordered/surface trigger idiom — same background swap .ocp-quick-back-btn's own
   desktop :hover already established (rest bg var(--ocp-surface) -> var(--ocp-page)
   on hover/press). Reused unmodified for every other bordered surface trigger/
   button below (.ocp-select-trigger, .ocp-print-btn, .ocp-btn-secondary,
   .ocp-settings-card-header, .ocp-stepper button, .ocp-chip/.ocp-tab's non-active
   state, .ocp-mixed-toggle's off state) — one idiom, not a bespoke effect per class. */
.ocp-cat-trigger:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-cat-trigger:hover { background: var(--ocp-page); }
}

.ocp-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(20, 18, 14, .45);
}
.ocp-sheet-backdrop[hidden] { display: none; }

.ocp-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 61;
  background: var(--ocp-surface);
  border-radius: 20px 20px 0 0;
  padding: 0.6rem 1.2rem calc(1.3rem + env(safe-area-inset-bottom));
  max-height: 70vh;
  overflow-y: auto;
  box-shadow: 0 -8px 24px rgba(0, 0, 0, .18);
}
.ocp-sheet[hidden] { display: none; }
/* Owner report, 2026-08-31 — Add Expense's sheet gained a 5th field (the
   manual-date picker, CanManuallyDate-gated) and started scrolling inside the
   generic 70vh cap above, cramping "Save expense" toward the bottom. Scoped to
   this one sheet, not the shared .ocp-sheet rule — every other sheet (Clear
   All, client/category pickers) still has plenty of headroom at 70vh and
   shouldn't grow just because this one needed to. */
#ocp-expense-sheet { max-height: 92vh; }
/* Product Search picker only — desktop gets a centered popup instead of a
   bottom sheet, matching StockEntry.cshtml's own actual reference (a
   Bootstrap modal there; this project doesn't use Bootstrap, so this
   reproduces the CONCEPT — centered dialog, not a literal Bootstrap modal —
   in this project's own CSS). Every other .ocp-sheet (Clear All confirm,
   category picker, color/style pickers) is untouched — only asked for this
   one. Same breakpoint StockEntry itself uses. */
@media (min-width: 992px) {
  #ocp-quick-sheet {
    left: 50%;
    right: auto;
    bottom: auto;
    top: 50%;
    transform: translate(-50%, -50%);
    width: min(560px, 90vw);
    max-height: 80vh;
    border-radius: var(--ocp-r);
    box-shadow: var(--ocp-shadow-float);
  }
  #ocp-quick-sheet .ocp-sheet-handle { display: none; }
}
.ocp-sheet-handle {
  width: 36px;
  height: 4px;
  border-radius: var(--ocp-r-pill);
  background: var(--ocp-line);
  margin: 0.5rem auto 0.9rem;
}
.ocp-sheet h3 { margin: 0 0 0.9rem; font-size: 0.95rem; color: var(--ocp-ink-soft); }
/* Multi-line wrap, not horizontal scroll — every category should be visible at a
   glance without needing to discover a scroll gesture, for both the retail sheet
   and the restaurant tab bar below. Bigger boxes: more padding, larger type, a
   sturdier touch target than a slim pill. */
.ocp-sheet-chips { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.ocp-chip {
  padding: 0.75rem 1.25rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ocp-ink-soft);
  cursor: pointer;
}
.ocp-chip.ocp-active { background: var(--ocp-ink); border-color: var(--ocp-ink); color: var(--ocp-page); }
.ocp-chip:not(.ocp-active):active { background: var(--ocp-page); }
.ocp-chip.ocp-active:active { opacity: .8; }
@media (hover: hover) and (pointer: fine) {
  .ocp-chip:not(.ocp-active):hover { background: var(--ocp-page); }
  .ocp-chip.ocp-active:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}

/* Category tabs (restaurant) — confident ink-filled active state, quiet outline
   otherwise. No colored glow. */
.ocp-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  padding: 0.15rem 0 1rem;
}
.ocp-tab {
  padding: 0.7rem 1.25rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ocp-ink-soft);
  text-decoration: none;
  white-space: nowrap;
  transition: transform .12s ease, background .12s ease, color .12s ease;
}
.ocp-tab:active { transform: scale(0.96); }
.ocp-tab.ocp-active {
  background: var(--ocp-ink);
  border-color: var(--ocp-ink);
  color: var(--ocp-page);
}
/* Owner request, 2026-09-04 — tablet/desktop has real room to spare, and a
   bigger tap target is genuinely easier to hit accurately there; mobile's own
   size is UNCHANGED (owner's explicit split — category size stays put on
   mobile, only the product card shrinks there). Same ≥760px container-width
   threshold the grid itself already steps up to 4 columns at. */
@container ocp-page (min-width: 760px) {
  .ocp-tab { padding: 1.15rem 1.6rem; font-size: 1rem; }
}
/* :not(.ocp-active) split, not one bare :hover — a bare .ocp-tab:hover would tie in
   specificity with .ocp-tab.ocp-active and (since this rule loads later) silently
   overwrite the active tab's ink fill with the inactive hover background while
   hovering it. */
@media (hover: hover) and (pointer: fine) {
  .ocp-tab:not(.ocp-active):hover { background: var(--ocp-page); }
  .ocp-tab.ocp-active:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}

/* Product grid — column count keyed off the CONTAINER (.ocp-page, named above), not
   the viewport, so it always matches the width actually available next to the
   desktop cart panel. This is what guarantees the grid never crowds/overlaps the
   panel: at any given container width it only ever offers as many columns as truly
   fit, leaving the rest of that row's space empty rather than squeezing in one more
   card — the same "reserve the space, don't fill it" effect requested, just derived
   from real geometry instead of a hardcoded rule. */
.ocp-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}
@container ocp-page (min-width: 480px) {
  .ocp-grid { grid-template-columns: repeat(3, 1fr); gap: 1.15rem; }
}
@container ocp-page (min-width: 760px) {
  .ocp-grid { grid-template-columns: repeat(4, 1fr); }
}

.ocp-card[hidden], .ocp-list-row[hidden] { display: none; }
.ocp-card {
  position: relative;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r);
  display: flex;
  flex-direction: column;
  box-shadow: var(--ocp-shadow);
  transition: transform .15s ease;
  cursor: pointer;
}
.ocp-card:active { transform: translateY(1px); }
.ocp-card:focus-visible, .ocp-list-row:focus-visible {
  outline: 2px solid var(--ocp-ink);
  outline-offset: 2px;
}
/* Opus review (2026-09-01, full-milestone pass) — the table/takeaway board's
   cards/rows are now keyboard-activatable (Tables.cshtml's tabindex/role,
   stafftables.js's Enter/Space handler); same focus treatment as the other
   card-shaped interactive elements above, applied here too. */
.ocp-table-card:focus-visible, .ocp-table-row:focus-visible {
  outline: 2px solid var(--ocp-ink);
  outline-offset: 2px;
}
/* UI/UX review fix (2026-09-02) — these are the board's primary click target
   (Opus review 2026-09-01 already made them keyboard-activatable and gave them
   a focus ring, but never a hover/press state) — same bordered-surface
   background-swap idiom .ocp-cat-trigger uses elsewhere in this file, not a
   bespoke effect. .ocp-table-card has no resting box-shadow of its own (unlike
   .ocp-card), so it gets the background swap rather than an elevate cue.
   Owner-flagged again (2026-09-07) — these were deliberately left on the plain
   var(--ocp-page) idiom during the 2026-09-06 .ocp-list-row pass (see that
   rule's own comment below), and the SAME "barely-there in light mode" defect
   that pass fixed there was still live here. Upgraded to the identical
   color-mix idiom now, closing the gap the 2026-09-06 comment explicitly
   called out rather than leaving these as the one remaining exception. */
@media (hover: hover) and (pointer: fine) {
  .ocp-table-card:hover, .ocp-table-row:hover { background: color-mix(in srgb, var(--ocp-ink) 8%, var(--ocp-surface)); }
}
.ocp-table-card:active, .ocp-table-row:active { background: color-mix(in srgb, var(--ocp-ink) 10%, var(--ocp-surface)); }
/* Tapping the whole card/row IS the add-to-cart action (see the ripple comment
   near the bottom of this file), so hover gets a real elevate cue here — not just
   a background tint — while :active still presses down a pixel as it already did.
   .ocp-list-row is a flush row inside a shared border (no shadow of its own to
   lift), so it gets a background swap instead — see its own rule below for
   what that swap actually is today (a color-mix tint since 2026-09-06, not the
   plain var(--ocp-page) idiom this comment used to describe). */
@media (hover: hover) and (pointer: fine) {
  .ocp-card:hover { box-shadow: var(--ocp-shadow-float); }
  /* Owner-flagged (2026-09-06) — plain var(--ocp-page) read as barely-there in
     light mode (#FFFFFF surface vs. #F3F6F8 page — too close to register as a
     real hover state), even though the identical idiom reads fine in dark mode
     (a much bigger #17222E-vs-#0E1620 swing). color-mix guarantees a real,
     visible tint in every theme/color combo regardless of how close that
     particular palette's own page/surface tokens happen to sit — same
     established idiom this file already uses elsewhere (e.g. .ocp-search-go's
     own hover). Matched by :active/.ocp-active below for one consistent
     component, not just the hover state alone. */
  .ocp-list-row:hover { background: color-mix(in srgb, var(--ocp-ink) 8%, var(--ocp-surface)); }
}

/* Picture-mode-off catalog: a dense text-first list instead of an image grid — see
   Branch.PictureModeEnabled and _ProductGrid.cshtml. */
.ocp-list { display: flex; flex-direction: column; border: 1px solid var(--ocp-line); border-radius: var(--ocp-r); overflow: hidden; background: var(--ocp-surface); }
.ocp-list-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--ocp-line);
  cursor: pointer;
  transition: background .1s ease;
}
.ocp-list-row:last-child { border-bottom: none; }
.ocp-list-row:active { background: color-mix(in srgb, var(--ocp-ink) 10%, var(--ocp-surface)); }
.ocp-list-row-name { flex: 1; min-width: 0; font-weight: 600; font-size: 0.92rem; color: var(--ocp-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ocp-color-swatch { flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--ocp-line); }
.ocp-list-row-price { font-weight: 700; font-size: 0.9rem; color: var(--ocp-ink); white-space: nowrap; }
/* Product Search sheet's Price/Stock columns — labeled like StockEntry.cshtml's own
   Cost/Price/Stock stats (minus Cost, never sent to staff). */
.ocp-sheet-stat-group { flex: 0 0 auto; display: flex; gap: 1.1rem; margin-left: 0.75rem; }
.ocp-sheet-stat { display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem; }
.ocp-sheet-stat label {
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ocp-muted);
}
.ocp-sheet-stat div { font-size: 0.85rem; font-weight: 700; color: var(--ocp-ink); white-space: nowrap; }
/* Lot/expiry chips on staff Quick Order rows (Pharmacy branches only) — same compact
   "tag" idiom StockEntry.cshtml already uses for this exact purpose. */
.ocp-tag { display: inline-flex; align-items: center; font-size: 0.7rem; font-weight: 600; color: var(--ocp-ink-soft); background: var(--ocp-page); border-radius: 6px; padding: 0.1rem 0.4rem; white-space: nowrap; }
.ocp-card-imgwrap {
  position: relative;
  border-radius: var(--ocp-r) var(--ocp-r) 0 0;
  overflow: hidden;
}
/* Owner request, 2026-09-04 — mobile-only: a square (aspect-ratio:1) image at a
   2-column grid's own column width (~160-180px) made the photo roughly TWICE the
   height of the name+price body beneath it (measured live: 161px image vs 82px
   body, 246px total card). First fix used a fixed height on the IMAGE only,
   close to matching the body's own natural height but not exact (a ~2px/2%
   drift, visible at larger render scales) — this version makes both EXACTLY
   equal by explicit construction: the same fixed height on both halves,
   overriding the body's own auto/content-driven height rather than just
   approximating it (.ocp-card-body's own padding/line-heights leave real
   headroom under 84px for a single-line name + price, so nothing clips).
   Category size is UNCHANGED here (owner's own explicit split: shrink the
   card, not the category tabs, on mobile). Reverts to the original full
   aspect-ratio square / auto body height at the same ≥480px container width
   the grid itself already steps up to 3 columns at — tablet/desktop keeps
   today's look exactly, only the narrow 2-column mobile case shrinks. */
.ocp-card-img {
  width: 100%;
  height: 84px;
  background: var(--ocp-brand-soft);
  object-fit: cover;
  display: block;
}
.ocp-card > .ocp-card-body:not(.ocp-card-body-noimg) {
  /* flex-basis:0% (part of the base .ocp-card-body's own "flex:1" shorthand)
     takes priority over `height` for sizing purposes in a column flex
     container — a plain `height:84px` here is silently ignored by the flex
     algorithm, which is why the first attempt at this fix measured no change
     at all (live-caught: computed height stayed exactly 82.39px, byte-
     identical to before, despite the rule visibly matching for `overflow`).
     `flex: 0 0 84px` fixes the main-axis size directly instead of fighting
     the shorthand it's overriding. */
  flex: 0 0 84px;
  overflow: hidden;
}
@container ocp-page (min-width: 480px) {
  .ocp-card-img { height: auto; aspect-ratio: 1; }
  .ocp-card > .ocp-card-body:not(.ocp-card-body-noimg) { flex: 1; overflow: visible; }
}

/* Letter-tile fallback when a product has no photo (see Helpers/CardVisuals.cs) — a
   small cohesive family of muted, warm-earthy tones (not a rainbow of unrelated
   hues), so a photo-less catalog still reads as designed rather than "broken". */
.ocp-card-mono {
  width: 100%;
  height: 84px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}
@container ocp-page (min-width: 480px) {
  .ocp-card-mono { height: auto; aspect-ratio: 1; }
}
.ocp-card-mono span {
  font-size: 2.3rem;
  font-weight: 700;
  color: rgba(255, 255, 255, .88);
  letter-spacing: -0.02em;
}

/* Favorite star — client-side only bookmark (localStorage, no server round trip), a
   personal marker while scrolling a big catalog, not a second CTA. Sits inside
   .ocp-card-imgwrap (already position:relative + overflow:hidden for the rounded
   photo corners) so it never needs the card itself to allow overflow. */
.ocp-fav-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 28px;
  height: 28px;
  /* var(--ocp-r-pill), not hardcoded 50% — see .ocp-stepper button's comment. */
  border-radius: var(--ocp-r-pill);
  border: none;
  background: var(--ocp-surface);
  opacity: 0.92;
  color: var(--ocp-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.ocp-fav-btn.ocp-fav-active { color: var(--ocp-brand); }
.ocp-fav-btn.ocp-fav-active svg { fill: currentColor; }
/* Quiet circular icon-button idiom — same as .ocp-theme-toggle/.ocp-scan-fab's own
   :active{transform:scale(...)} press feedback, plus a hover background swap for
   the desktop/mouse case those two don't otherwise need as much (this sits over
   product photos, so a hover cue helps confirm what's about to be tapped). Note:
   .ocp-list-row .ocp-fav-btn below overrides background to "none" at rest — this
   :hover rule still applies there too (tied specificity, later source order wins),
   which is an intentional/acceptable minor addition, not a bug. */
.ocp-fav-btn:active { transform: scale(0.9); }
@media (hover: hover) and (pointer: fine) {
  .ocp-fav-btn:hover { background: var(--ocp-page); }
}

/* "Show favorites only" toggle (next to search) + the pure-CSS filter it drives.
   :has() reacts live to the .ocp-fav-active class the star buttons already toggle —
   favoriting an item while the filter is on updates the visible set with no
   JS-driven re-render needed. */
.ocp-call-btn.ocp-fav-active { color: var(--ocp-brand); border-color: var(--ocp-brand); }
.ocp-call-btn.ocp-fav-active svg { fill: currentColor; }
#ocp-grid-container.ocp-fav-filter-on .ocp-card:not(:has(.ocp-fav-btn.ocp-fav-active)),
#ocp-grid-container.ocp-fav-filter-on .ocp-list-row:not(:has(.ocp-fav-btn.ocp-fav-active)) {
  display: none;
}
.ocp-fav-btn svg { pointer-events: none; }
.ocp-list-row .ocp-fav-btn {
  position: static;
  background: none;
  opacity: 1;
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}

.ocp-card-body {
  padding: 1.25rem 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  flex: 1;
}
/* Single line, truncated with an ellipsis — not a 2-line clamp. A name that's
   allowed to wrap to a second line makes every card a different height, so the
   price row lands at a different vertical position card to card; fixing the name
   to one line keeps every price aligned across the whole grid. */
.ocp-card-name {
  font-size: 0.89rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ocp-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ocp-card-price {
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--ocp-ink);
  margin-top: 0.1rem;
}
.ocp-card-price small { font-weight: 500; color: var(--ocp-muted); font-size: 0.72rem; }

/* PictureMode-off card (no .ocp-card-imgwrap at all — see _ProductGrid.cshtml's own
   comment) — the card becomes just its name+price body, no reserved image area.
   Height increased ~50% over the plain padding-driven height (measured live:
   79.78px -> 120px via min-height) per direct request, column count reverted to
   4 on desktop (an earlier 3-column-cap attempt was tried and un-done). Content
   inside was then scaled up too ("everything inside it should also become 50%
   bigger, to take up space") but that first pass (x1.5 on every value) came back
   "a bit too big... too in the face" — dialed back to x1.2 over the ORIGINAL
   no-image values below (not the with-photo card's own sizes, untouched
   throughout). justify-content:center (on .ocp-card-noimg, base .ocp-card is
   already display:flex/flex-direction:column) centers the block within the
   120px floor; min-height is a floor, not a cap. */
.ocp-card-noimg { min-height: 120px; justify-content: center; }
.ocp-card-body-noimg { padding: 1.2rem 1.08rem; gap: 0.42rem; }
/* Name up to 2 lines (not the base .ocp-card-name's single-line ellipsis truncation
   — no-image mode has room to give a long name real space instead of cutting it
   off), both name and price centered — "make it like 3 rows... 2 rows for name
   (centralize it) and price as 3rd row also centralize it... this way product
   with longer names have more space." -webkit-line-clamp is the standard
   cross-browser (Chrome/Safari/Firefox all support the -webkit- prefixed form,
   including on non-WebKit engines) 2-line-then-ellipsis technique — the same
   graceful "still truncates if it's REALLY long" safety net the 1-line version
   had, just with double the room first.

   min-height reserves the FULL 2-line height (2 x line-height 1.35 x font-size
   1.2rem = 3.24rem) always, even for a short 1-line name — found live ("prices
   are not aligned... i want prices to be where the Black Forest Cake has price")
   that a plain height-hugging name made a 1-line card's price sit higher than a
   2-line card's price, since the whole name+price block was centering around
   its OWN (variable) content height. With the name slot's height now constant
   regardless of actual text length, the name+price group's total height is
   constant too, so .ocp-card-noimg's own justify-content:center lands the price
   at the identical offset on every card in a row — a short name just has blank
   space below it inside its own reserved slot, which is the explicitly accepted
   tradeoff ("if name is not long no problem keep price fixed there"). */
.ocp-card-body-noimg .ocp-card-name {
  font-size: 1.2rem;
  font-weight: 700;
  text-align: center;
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 3.24rem;
}
.ocp-card-body-noimg .ocp-card-price { font-size: 0.98rem; font-weight: 600; color: var(--ocp-ink-soft); text-align: center; }
.ocp-card-body-noimg .ocp-card-price small { font-size: 0.86rem; }
/* Real bug, live-caught by review: .ocp-card-body-hasfav carried no CSS rule at
   all (removed during an earlier pass when the name switched to centered text,
   since a one-sided padding-right doesn't play well with true centering — but
   nothing replaced it), so the star had zero clearance and visibly overlapped a
   wrapped 2-line name's text, confirmed live at both mobile and desktop widths.
   Fixed with SYMMETRIC inset on both sides instead of one-sided padding-right —
   keeps the name genuinely centered (just within a slightly narrower centered
   zone) while still guaranteeing neither line's text can run under the star,
   which sits at a fixed 34px+10px offset from the card's top-right corner
   regardless of card width. */
.ocp-card-body-hasfav .ocp-card-name { padding: 0 1.7rem; }
/* Favorite star scaled the same x1.2 — 28px->34px button, 8px->10px offset (so it
   sits proportionally, not just bigger-in-the-same-corner), 15px->18px icon. CSS
   width/height here correctly overrides the .cshtml's inline svg width="15"
   height="15" attributes (a CSS property always wins over a presentational HTML
   attribute of the same name). */
.ocp-card-noimg .ocp-fav-btn { width: 34px; height: 34px; top: 10px; right: 10px; }
.ocp-card-noimg .ocp-fav-btn svg { width: 18px; height: 18px; }

/* Stepper */
.ocp-stepper {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}
.ocp-stepper button {
  width: 30px;
  height: 30px;
  /* var(--ocp-r-pill), not a hardcoded 50% — themes built entirely around sharp
     corners (Utility, Neobrutalism: --ocp-r-pill overridden to ~0-3px) otherwise
     ended up with a perfectly round button floating on an otherwise all-square
     card, a mismatch reported directly ("change it also be rectangular"). Themes
     that don't touch --ocp-r-pill (default 999px) render this exactly as round
     as before — no visual change there. */
  border-radius: var(--ocp-r-pill);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  color: var(--ocp-ink);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
}
/* Real gap, tapped extremely frequently in cart/checkout — same bordered-surface
   idiom as .ocp-cat-trigger above (rest bg var(--ocp-surface) -> var(--ocp-page)).
   Applies equally to .ocp-stepper-sm's smaller buttons below since that variant
   only overrides size, not color. */
.ocp-stepper button:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-stepper button:hover { background: var(--ocp-page); }
}
.ocp-stepper input {
  width: 2.6rem;
  text-align: center;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--ocp-ink);
  -moz-appearance: textfield;
}
.ocp-stepper input:focus {
  outline: none;
  border-color: var(--ocp-ink);
  background: var(--ocp-surface);
}
.ocp-stepper input::-webkit-outer-spin-button,
.ocp-stepper input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Cart lines — a phone's native swipe-to-delete: .ocp-swipe-content is the visible,
   draggable layer; .ocp-swipe-bg is a surface with a trash icon sitting behind it,
   only ever revealed as the content slides away (never animated in itself, which
   is what makes it read as "underneath" rather than "sliding in"). Transparent at
   rest — .ocp-swipe-active (added the instant a drag starts, cartpage.js/
   staffpos.js/staffquick.js's shared pointerdown handler) is what turns it red,
   so a cart row never shows red until someone actually starts swiping it. */
.ocp-cart-line {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--ocp-line);
  /* Lets cartpage.js's swipe-to-delete own horizontal drags while the browser keeps
     handling vertical scroll natively — without this, a touch-start on the row would
     be ambiguous between "scroll the page" and "start a swipe" and often cost a stutter
     before either wins. */
  touch-action: pan-y;
}
.ocp-swipe-bg {
  position: absolute;
  /* 1px inset, not 0 — belt-and-suspenders beyond border-radius:inherit below: a
     rounded parent (overflow:hidden) clipping two independently-rendered sibling
     rectangles to the same curve can still leave a 1px antialiasing seam where a
     sliver of red peeks through at the corners on some renderers/zoom levels. A
     1px margin guarantees the opaque content always fully overlaps this layer at
     rest; imperceptible once an actual swipe drags the content aside. */
  inset: 1px;
  z-index: 0;
  border-radius: inherit;
  background: transparent;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 1.3rem;
}
.ocp-swipe-bg svg { transition: transform .12s ease; }
.ocp-cart-line.ocp-swipe-active .ocp-swipe-bg { background: var(--ocp-bad); }
.ocp-cart-line.ocp-swipe-armed .ocp-swipe-bg { background: #A61414; }
.ocp-cart-line.ocp-swipe-armed .ocp-swipe-bg svg { transform: scale(1.15); }
.ocp-swipe-content {
  position: relative;
  z-index: 1;
  /* Matches whatever radius .ocp-cart-line (its parent) is using, and paints an
     actual opaque background at every pixel including the rounded corners — belt
     and suspenders against .ocp-swipe-bg's red showing through at the corners,
     which overflow:hidden's clip-path antialiasing can let bleed through by a
     pixel on some renderers when both layers are sharp-cornered rectangles. */
  border-radius: inherit;
  display: flex;
  gap: 0.85rem;
  align-items: center;
  padding: 1rem 0;
  background: var(--ocp-surface);
}
.ocp-cart-line-name { font-weight: 600; font-size: 0.93rem; color: var(--ocp-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ocp-cart-line-price { color: var(--ocp-muted); font-size: 0.82rem; }
.ocp-cart-line-total { font-weight: 700; margin-left: auto; text-align: right; color: var(--ocp-ink); }

/* Card-based cart row (cart panel / dedicated Cart page / drawer / Staff Browse mode
   — cartpage.js and staffquick.js's lineRowBrowse both use these classes): each line
   is its own elevated, rounded card (var(--ocp-surface), a shade lighter than the
   page behind it) rather than a flush list separated by hairlines. Thumbnail+title
   grouped on the left; total (bold) stacked above the qty stepper on the right,
   unit-price/note small and muted underneath; the × sits in the card's own top-right
   corner, independent of the price stack so it never looks like part of the total. */
.ocp-cart-line {
  border-bottom: none;
  border-radius: var(--ocp-r);
  margin-bottom: 0.6rem;
}
.ocp-cart-line:last-child { margin-bottom: 0; }
.ocp-swipe-content.ocp-cart-row {
  flex-direction: row;
  align-items: flex-start;
  gap: 0.75rem;
  /* Right padding reserves room for the × (.ocp-remove, absolutely positioned
     at right:0.6rem, ~25.6px wide) — measured live: the original 2.25rem
     left only ~0.8px of real clearance between × and Total's own right edge
     (× itself already consumes 35.2 of the 36px reserved), so ANY total one
     digit wider than a short test value ("600.00" vs "20.00") tips straight
     into an overlap — not actually conditional on having a product image,
     that just happened to correlate with the longer totals in the report.
     Bumped for real, digit-count-independent breathing room. */
  padding: 0.9rem 2.6rem 0.9rem 0.9rem;
  background: var(--ocp-surface);
  border-radius: var(--ocp-r);
  box-shadow: var(--ocp-shadow);
}
.ocp-cart-line-img {
  width: 44px;
  height: 44px;
  border-radius: var(--ocp-r-sm);
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--ocp-page);
}
.ocp-cart-row-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.25rem;
  padding-top: 0.15rem;
}
.ocp-cart-row-right {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.35rem;
}
.ocp-cart-row-right .ocp-cart-line-total { margin-left: 0; }
.ocp-cart-row-bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
}
.ocp-cart-row-meta { display: flex; align-items: center; gap: 0.4rem; font-size: 0.76rem; color: var(--ocp-muted); }
.ocp-cart-row-meta .ocp-note-add, .ocp-cart-row-meta .ocp-note-edit { font-size: 0.76rem; }

/* Narrow-card reflow — shared by two contexts that don't have room for the
   default side-by-side [image|name+meta][total+stepper] row: the 320px
   desktop cart panel (always this narrow, at any viewport) and any
   phone-width view of the same card (Cart.cshtml, Menu.cshtml's mobile
   drawer, Browse mode's drawer — below the 900px breakpoint where the side
   panel takes over instead). Both were reported cramped: price+note wrapping
   onto its own line, the stepper crowding against it. Re-flows into two rows
   via display:contents on the two wrapper divs (same technique as the
   desktop Quick Search grid rebuild) — no markup/JS changes, CSS-only:
     row 1: [image, spans both rows] name ............ qty stepper   total
     row 2: [image continues]        price + note (spans under name/qty/total)
   .ocp-cart-line-total's own `margin-left:auto` (its general-case rule,
   above) is harmless here — the grid placement already puts it in the last
   column, and auto-margin in a grid item just aligns it within that column's
   own box, still flush right. */
/* No image column here (unlike the mobile version below) — at a fixed 320px
   this is narrower than even a small phone's viewport, and the math doesn't
   work: image(44px) + stepper(~96px) + total(~58px) + 3 gaps already consume
   the entire available content width on their own, leaving 0px for the name
   (measured directly: grid-template-columns resolved to "44px 0px 96px 58px").
   Dropping the thumbnail here is an acceptable tradeoff — this panel is a
   compact running summary, not the primary place someone reviews their cart
   (the dedicated Cart page / mobile drawer, both plenty wide, keep it). */
/* Panel widened from 320px to 400px specifically to make room for the
   thumbnail again (dropped in an earlier pass when 320px measured too
   narrow to fit it alongside stepper+total) — re-added here, spanning both
   rows on the left, same as the mobile version below. Row 1: name + total.
   Row 2: stepper + price/note. (Two items per row, not three — name+
   stepper+total together still don't fit even at 400px once the image
   claims its own column; see the still-current measurement note on the
   mobile block below for why.) */
.ocp-cart-panel .ocp-swipe-content.ocp-cart-row {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr — real bug (2026-08-30): a plain 1fr track's
     implicit min-width is its own content's max-content size, so a long
     product name/tags never actually yielded space to the total/stepper
     column. For most totals this was invisible (the name column had room to
     spare), but a genuinely large total ("60,000.00" — a 100x-qty line) needs
     more than column 3's own reserved auto-width, and with nothing forcing
     column 2 to shrink, the whole grid overflowed its own padding-box, with
     the excess bleeding straight into the padding-right reserved for the ×
     button (the ×/Total overlap fix elsewhere only covers ordinary-width
     totals, not this overflow case). minmax(0, 1fr) lets column 2 actually
     shrink below its content's natural width, so the name/tags truncate
     (ocp-cart-line-name already has its own ellipsis rule) instead of forcing
     the row wider than its container. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 0.6rem;
  row-gap: 0.4rem;
}
.ocp-cart-panel .ocp-cart-row-body,
.ocp-cart-panel .ocp-cart-row-right { display: contents; }
.ocp-cart-panel .ocp-cart-line-img { grid-column: 1; grid-row: 1 / span 2; }
.ocp-cart-panel .ocp-cart-line-name { grid-column: 2; grid-row: 1; white-space: normal; }
.ocp-cart-panel .ocp-cart-line-total { grid-column: 3; grid-row: 1; }
.ocp-cart-panel .ocp-cart-row-meta { grid-column: 2; grid-row: 2; }
.ocp-cart-panel .ocp-stepper { grid-column: 3; grid-row: 2; justify-self: end; }
@media (max-width: 899px) {
  /* Single row, 4 grid tracks (image / name+meta / qty / total) — the × sits
     on top via its own existing absolute positioning (.ocp-cart-line >
     .ocp-remove below), not a 5th grid track, but reads as the row's 5th
     visual column. .ocp-cart-row-body stays a normal flex-column box (NOT
     display:contents) so the name and price/note stay grouped together as
     ONE track, matching the plainly-requested shape: image, name (left),
     qty (centered), total (right), x. */
  .ocp-swipe-content.ocp-cart-row {
    display: grid;
    grid-template-columns: auto 1fr auto auto;
    align-items: center;
    column-gap: 0.6rem;
  }
  .ocp-cart-row-right { display: contents; }
  .ocp-cart-line-img { grid-column: 1; grid-row: 1; }
  .ocp-cart-row-body { grid-column: 2; grid-row: 1; min-width: 0; }
  .ocp-cart-line-name {
    /* Stays nowrap+ellipsis — the name column is only ~46-80px once qty
       (~96px) and total (~92px) claim their share of a phone-width row;
       letting it wrap produces an ugly tall ransom-note column instead of a
       clean single truncated line. */
    display: block;
  }
  /* grid-row:1 explicit on ALL FOUR tracks, not left to "auto" — sparse
     auto-placement processes items in DOM order (total comes before stepper
     in the markup) and only moves its search cursor forward; since total's
     explicit column (4) is past stepper's (3), the cursor had already
     advanced beyond column 3 by the time stepper was placed, pushing it
     into a second implicit row instead of sharing row 1. Reproduced via
     computed gridTemplateRows showing two rows when only one was wanted. */
  .ocp-stepper { grid-column: 3; grid-row: 1; }
  .ocp-cart-line-total { grid-column: 4; grid-row: 1; }
  /* The name+meta column is only ~46-80px once qty+total claim their share of
     a phone-width row — the unit price and the note button previously sat
     SIDE BY SIDE here (.ocp-cart-row-meta's own base display:flex) and
     overflowed into the stepper's space at that width (measured/
     screenshotted, a real visual bug). Per your request to bring the unit
     price back (so someone can see it without doing total÷qty in their
     head), it's stacked as its own line above the note instead of trying to
     fit both side by side again — same narrow-column constraint, different
     fix. Font-size/color matched to the note button's own (0.76rem,
     var(--ocp-muted)) rather than .ocp-cart-line-price's normal 0.82rem, so
     the two stacked lines read as one small "extra details" block, not two
     different text treatments. */
  .ocp-cart-row-meta { flex-direction: column; align-items: flex-start; gap: 0.1rem; }
  .ocp-cart-row-meta .ocp-cart-line-price { font-size: 0.76rem; }
}
@media (min-width: 900px) {
  /* Desktop /cart page ONLY (.ocp-cart-page marker class — Cart.cshtml's own
     container, not Menu.cshtml's side panel or the mobile drawer, both of
     which already have their own tuned layouts above). Same "evenly spread
     columns" alignment concept as the desktop Quick Search row grid: image +
     name(25%) on the left, Qty/Price/Total as three evenly-proportioned
     columns instead of total+stepper cramped together on the right with
     price buried under the name. Every wrapper flattened via
     display:contents so name/note/price/qty/total can each land in their
     own grid cell — all given BOTH an explicit grid-column AND grid-row
     (not left to "auto"), learned from the sparse-auto-placement bug on the
     mobile row above (an explicit-column/auto-row item can get pushed onto
     a spurious extra row depending on DOM order). */
  .ocp-cart-page .ocp-swipe-content.ocp-cart-row {
    display: grid;
    grid-template-columns: auto 25% repeat(3, 1fr);
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: 1.25rem;
  }
  .ocp-cart-page .ocp-cart-row-body,
  .ocp-cart-page .ocp-cart-row-meta,
  .ocp-cart-page .ocp-cart-row-right { display: contents; }
  .ocp-cart-page .ocp-cart-line-img { grid-column: 1; grid-row: 1 / span 2; }
  .ocp-cart-page .ocp-cart-line-name { grid-column: 2; grid-row: 1; white-space: normal; }
  .ocp-cart-page [data-note-wrap] { grid-column: 2; grid-row: 2; }
  .ocp-cart-page .ocp-stepper { grid-column: 3; grid-row: 1 / span 2; justify-self: center; }
  .ocp-cart-page .ocp-cart-line-price { grid-column: 4; grid-row: 1 / span 2; justify-self: center; }
  .ocp-cart-page .ocp-cart-line-total { grid-column: 5; grid-row: 1 / span 2; justify-self: end; }
}
.ocp-stepper-sm { gap: 0.4rem; }
.ocp-stepper-sm button { width: 25px; height: 25px; font-size: 0.95rem; }
.ocp-stepper-sm input { width: 2.1rem; font-size: 0.88rem; }
/* Staff POS's own Qty field only (dense .ocp-quick-row rows + Browse mode's
   #ocp-quick-panel-lines) — the +/- buttons stay exactly as they are; only the
   number field's own box gets the same bordered/filled look as the Discount/
   Price fields beside it, so all three read as one consistent row of fields.
   The customer-facing /cart page's stepper (same .ocp-stepper-sm class) is
   deliberately untouched — scoped to these two staff-POS containers only. */
.ocp-quick-row .ocp-stepper input,
#ocp-quick-panel-lines .ocp-stepper input,
#ocp-quick-browse-lines .ocp-stepper input {
  border-color: var(--ocp-line);
  background: var(--ocp-page);
  border-radius: var(--ocp-r-sm);
  font-weight: 400;
  /* Qty has no upper bound (a cashier can type any amount) so it gets more
     visual room than Discount (capped 0-100, never more than 3 digits) —
     both share an explicit height/box-sizing with .ocp-discount-input/
     .ocp-price-input below so all three fields in the row line up exactly,
     regardless of their differing font-sizes/padding. */
  width: 4.2rem;
  height: 1.9rem;
  padding: 0 0.3rem;
  box-sizing: border-box;
}
.ocp-quick-row .ocp-stepper input:focus,
#ocp-quick-panel-lines .ocp-stepper input:focus,
#ocp-quick-browse-lines .ocp-stepper input:focus {
  border-color: var(--ocp-ink-soft);
  background: var(--ocp-page);
}
.ocp-quick-row .ocp-discount-input,
.ocp-quick-row .ocp-price-input,
#ocp-quick-panel-lines .ocp-discount-input,
#ocp-quick-panel-lines .ocp-price-input,
#ocp-quick-browse-lines .ocp-discount-input,
#ocp-quick-browse-lines .ocp-price-input {
  height: 1.9rem;
  box-sizing: border-box;
}
/* Discount's own box stays compact (auto width, capped) instead of
   stretching to fill its container like Price does — a 0-100 value never
   needs a wide box, and a wide-but-mostly-empty box is what was reading as
   "off"/"huge" against everything else in the row. Real bug fix (2026-08-30):
   #ocp-quick-browse-lines (the main Browse-mode list — as distinct from the
   side panel/drawer, #ocp-quick-panel-lines) was missing from this scope
   entirely, so THAT container's discount box rendered at its unconstrained
   native width — exactly the "huge empty box breaking the row" regression. */
.ocp-quick-row .ocp-discount-input,
#ocp-quick-panel-lines .ocp-discount-input,
#ocp-quick-browse-lines .ocp-discount-input {
  width: auto;
  max-width: 2.6rem;
  margin: 0 auto;
}
/* Browse mode's note+discount now share one compact inline row (below Price,
   which stays on its own line) instead of Discount being a third full-width
   stacked block under .ocp-cart-row-meta's mobile column layout — restores
   "close to one line" instead of 3 stacked rows per card. Discount's own
   margin:0-auto rule above still applies inside here; in a row context with
   only two children this reads as "pushed to the row's right edge". */
.ocp-cart-row-noterow {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}
/* Real bug (UI/UX review, 2026-08-30): on a narrow Browse-mode card, this flex
   row's default flex-shrink:1 let the Discount input shrink below the space
   its own typed value needs (e.g. "10" measured at 31px in a 25px box),
   silently clipping a digit. Discount's own value is short and load-bearing
   (a cashier-entered percentage) — it must never shrink; the note field, which
   is free text and fine to truncate, absorbs the squeeze instead. */
.ocp-cart-row-noterow .ocp-discount-input {
  flex-shrink: 0;
}
.ocp-cart-row-noterow .ocp-note-add,
.ocp-cart-row-noterow .ocp-note-edit,
.ocp-cart-row-noterow .ocp-note-input {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Remove (×) is a direct child of .ocp-cart-line (a sibling of .ocp-swipe-content,
   not nested inside the price stack) — pinned to the card's own top-right corner
   independent of the price/stepper column, so it never shifts based on total width
   or wraps onto its own line the way it did stacked under the total. */
.ocp-cart-line > .ocp-remove {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  /* .ocp-swipe-content has z-index:1 (the red-swipe-bg-bleed fix) — without this,
     that gave it its own stacking context that painted OVER this sibling button by
     default (z-index:auto loses to any explicit z-index regardless of DOM order),
     burying the × until a swipe dragged the content out of the way. */
  z-index: 2;
}
/* Mobile: vertically centered on the row itself instead of top-pinned against
   the whole card — a short single-row card should read as ONE row, not a
   reserved ×-only line above the content. MUST come after the unconditional
   rule above (same selector, tied specificity — a same-specificity override
   inside an earlier-appearing @media block loses the cascade to a later
   unconditional rule regardless of the media query; this was tried once
   already, placed too early in the file, and silently never took effect). */
@media (max-width: 899px) {
  .ocp-cart-line > .ocp-remove {
    top: 50%;
    transform: translateY(-50%);
  }
}
/* Staff Quick Search's own product rows — deliberately matches StockEntry.cshtml's
   own product card (.se-card et al) layout/spacing exactly (per your reference
   screenshots + a direct read of StockEntry.cshtml's CSS): Name+stock badge on top,
   a centered Qty/Price/Total column group below with a hairline divider, "+ note"
   as its own line at the bottom, a single round × pinned top-right — three stacked
   sections on phone/tablet, collapsing to one row on desktop (≥992px, StockEntry's
   own breakpoint) since there's room. No edit/copy icons or cost-price field —
   those are StockEntry-specific (stock-in) concepts this order tool has no
   equivalent of. */
/* Step 2's "Back to items" — a plain text link read as too small/subtle at desktop
   width (StockEntry-review feedback), where a bare 0.85rem link looks lost against
   a wide page. Stays a plain link on phone (already fine there); grows into a real
   bordered pill button, same visual weight as .ocp-call-btn, at desktop. */
.ocp-quick-back-btn {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: none;
  border: none;
  color: var(--ocp-ink-soft);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0;
  margin-bottom: 1.1rem;
  cursor: pointer;
}
/* Plain text link at every width, no bordered-pill box to swap a background on
   (that only appears at desktop width below) — opacity press feedback, same idiom
   as .ocp-remove/.ocp-note-add's own plain-text-button treatment, works
   identically whether or not the desktop pill styling is active. */
.ocp-quick-back-btn:active { opacity: .65; }
@media (min-width: 992px) {
  .ocp-quick-back-btn {
    font-size: 0.92rem;
    padding: 0.55rem 1.1rem;
    border: 1px solid var(--ocp-line);
    border-radius: var(--ocp-r-pill);
    background: var(--ocp-surface);
  }
}
/* Pre-existing :hover here was only guarded by the (min-width: 992px) box query,
   not (hover: hover) — a real gap this whole pass exists to close, since a
   Galaxy-Tab-class touch device in landscape crosses 992px and would have kept
   the sticky-hover bug this file is otherwise being fixed for everywhere else.
   Re-scoped to combine both conditions rather than leaving this one rule as the
   sole unguarded exception. */
@media (min-width: 992px) and (hover: hover) and (pointer: fine) {
  .ocp-quick-back-btn:hover { background: var(--ocp-page); }
}
.ocp-quick-row-content {
  flex-direction: column;
  align-items: stretch;
  gap: 0.65rem;
  padding: 0.75rem 0.9rem;
  border-radius: 14px;
  position: relative;
}
.ocp-quick-row-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
  padding-right: 1.7rem;
}
.ocp-quick-row-name { min-width: 0; }
.ocp-quick-row-badges { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
/* Smaller than the photo-card's .ocp-remove — this icon sits right next to dense
   text on a compact row, not floating alone in a card corner, so the original
   27px/15px-icon size read as oversized/heavy next to it. */
.ocp-quick-row-icon[hidden] { display: none; }
.ocp-quick-row-icon {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  /* var(--ocp-r-pill), not hardcoded 50% — see .ocp-stepper button's comment
     above for why (sharp-cornered themes otherwise got a stray perfect circle). */
  border-radius: var(--ocp-r-pill);
  border: none;
  background: var(--ocp-page);
  color: var(--ocp-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  position: absolute;
  top: 0.65rem;
  right: 0.7rem;
}
.ocp-quick-row-icon svg { width: 12px; height: 12px; }
.ocp-quick-row-icon:active { opacity: 0.7; }
@media (hover: hover) and (pointer: fine) {
  .ocp-quick-row-icon:hover { opacity: 0.7; }
}
.ocp-quick-row-cols {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.6rem;
  border-top: 1px solid var(--ocp-line);
  padding-top: 0.55rem;
}
/* Discount field inserted between Qty and Price (Qty -> Discount -> Price ->
   Total) — one extra track when Branch.EnableDiscount is on for this branch. */
/* Qty gets more of the row than Discount (see the field-size comment on
   .ocp-stepper input above) — Price/Total stay an even split between them. */
.ocp-quick-row-cols-4 { grid-template-columns: 1.4fr 0.7fr 1fr 1fr; }
.ocp-quick-col { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; text-align: center; }
.ocp-quick-col input.ocp-price-input { max-width: 100%; text-align: center; }
.ocp-quick-col label {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ocp-muted);
}
/* Also > span, not just > div — Price renders as a plain <span class="ocp-money">
   (priceFieldHtml's non-editable branch) rather than a <div> when the tenant
   can't edit price at sale, which was escaping this rule entirely and falling
   back to a much larger inherited size — reading as MORE prominent than Total
   (a real <div>, correctly sized here) instead of less. */
.ocp-quick-col:not(:first-child) > div, .ocp-quick-col:not(:first-child) > span { font-size: 0.85rem; color: var(--ocp-ink-soft); }
.ocp-quick-col-total > div { font-weight: 700; color: var(--ocp-ink); }
.ocp-quick-row-note {
  font-size: 0.8rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--ocp-line);
  /* Review fix (2026-08-31) — without this, the flag-comment button's [data-
     note-wrap] sibling span (now holding a width:100% .ocp-note-input while a
     note is being edited) took the full row width as a plain block child,
     pushing the flag button onto a second line. flex + gap matches
     .ocp-cart-row-noterow's own already-correct treatment of the identical
     note-input-plus-sibling-button layout. */
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.ocp-quick-row-note [data-note-wrap] { min-width: 0; flex: 1; }
/* Staff Quick order's own rows have no product image pinning the layout, so a wider
   total (MZN 200.00 vs MZN 20.00) could nudge the × button a few px per row without
   this — a fixed minimum keeps every row's columns lined up regardless of amount. */
#ocp-quick-lines .ocp-quick-col-total { min-width: 84px; }

/* Desktop — StockEntry's own breakpoint (≥992px). Real CSS Grid with FIXED column
   widths shared by every row (not flex+gap, which let each row's columns start at
   a different X depending on that row's own name length — exactly the "nothing is
   aligned" problem). display:contents on the two mobile wrapper divs (.ocp-quick-
   row-top, .ocp-quick-row-cols) makes THEIR CHILDREN the direct grid items of
   .ocp-quick-row-content instead, without needing separate desktop-only markup —
   the same 3 elements just get explicitly placed into named grid cells. Note moves
   into row 2 of the name column (under the name/badges) instead of its own
   full-width bottom section, since there's no need for a 3rd stacked row anymore. */
@media (min-width: 992px) {
  .ocp-quick-row-content {
    display: grid;
    /* Name = 25% of the row; the remaining space (minus the fixed × column) splits
       EQUALLY across Qty/Price/Total (three 1fr tracks) — per your spec, not
       StockEntry's own fixed-px columns, which left dead space on a wide row. */
    grid-template-columns: 25% repeat(3, 1fr) 28px;
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: 1.25rem;
    row-gap: 0.3rem;
    padding: 0.55rem 0.85rem;
  }
  .ocp-quick-row-top, .ocp-quick-row-cols { display: contents; }
  .ocp-quick-row-name { grid-column: 1; grid-row: 1; padding-right: 0; }
  .ocp-quick-row-icon { grid-column: 5; grid-row: 1; position: static; justify-self: end; }
  .ocp-quick-col:nth-of-type(1) { grid-column: 2; grid-row: 1; }
  .ocp-quick-col:nth-of-type(2) { grid-column: 3; grid-row: 1; }
  .ocp-quick-col-total { grid-column: 4; grid-row: 1; }
  .ocp-quick-row-note {
    grid-column: 1 / span 4;
    grid-row: 2;
    padding-top: 0;
    border-top: none;
  }
  /* Discount column present (.ocp-has-discount, set on the row root when
     Branch.EnableDiscount is on) shifts everything from Price onward one
     track right: Name=1, Qty=2, Discount=3 (already correct via the base
     nth-of-type(2) rule above — Discount IS the 2nd .ocp-quick-col in DOM
     order here), Price=4 (needs its own nth-of-type(3) rule, below — Total
     no longer collides with it since .ocp-quick-col-total is the 4th .ocp-
     quick-col div, past nth-of-type(3)), Total=5, remove-icon=6. */
  .ocp-has-discount .ocp-quick-row-content { grid-template-columns: 25% 1.4fr 0.7fr 1fr 1fr 28px; }
  .ocp-has-discount .ocp-quick-col:nth-of-type(3) { grid-column: 4; grid-row: 1; }
  .ocp-has-discount .ocp-quick-col-total { grid-column: 5; }
  .ocp-has-discount .ocp-quick-row-icon { grid-column: 6; }
  .ocp-has-discount .ocp-quick-row-note { grid-column: 1 / span 5; }
  .ocp-quick-row .ocp-stepper-sm button { width: 22px; height: 22px; font-size: 0.85rem; }
  /* Desktop only, a little bigger — plenty of room here vs. the mobile card,
     where this isn't touched yet (checking how it reads there first). */
  .ocp-quick-row .ocp-quick-col label { font-size: 0.7rem; }
  .ocp-quick-row .ocp-quick-col:not(:first-child) > div,
  .ocp-quick-row .ocp-quick-col:not(:first-child) > span { font-size: 0.95rem; }
  .ocp-quick-row .ocp-stepper input { font-size: 0.98rem; width: 4.6rem; }
  .ocp-quick-row .ocp-discount-input { font-size: 0.9rem; max-width: 2.9rem; }
  /* Real bug (UI/UX review, 2026-08-30): when price IS editable at sale, it
     renders as <input class="ocp-price-input">, a direct child that is neither
     div nor span — the ":not(:first-child) > div/span" rule above never catches
     it, so Price rendered ~19% smaller than its Qty/Discount/Total row-mates on
     desktop. (When price is NOT editable it's a plain <span>, already covered.) */
  .ocp-quick-row .ocp-price-input { font-size: 0.95rem; }

  /* Bottom bar's total, aligned to the SAME X as the row's own "Total" column
     above it. TWO earlier attempts at this (both measured wrong in the browser,
     not just eyeballed) tried to replicate the row's grid as a matching grid on
     the bar's own track div — that can't work: the track is a `flex:1 1 auto`
     item sandwiched between the clear-all icon (~38px) and the action button
     (~115px, "Next"/"Send to POS"), so it only ever gets ~1092px of the ~1205px
     the row's own content grid gets — flex-grow:1 also means `margin:auto`
     never gets any leftover space to center into, and any `max-width` bigger
     than the available flex space never binds. No amount of padding on the
     track can fix a box that's fundamentally narrower and asymmetrically
     offset from the row's own box.
     Fix: don't replicate a box model — replicate the ARITHMETIC. The row's
     Total-column center, as a function of viewport width W (capped at
     .ocp-layout's own 1280px max-width), works out to a plain linear formula:
       contentLeft   = 37.6px                          (.ocp-layout's 1.5rem + the row card's own 0.85rem)
       contentWidth  = W - 75.2px                       (contentLeft on both sides)
       col1 (name)   = 25% of contentWidth
       each of Qty/Price/Total = (75% of contentWidth − 28px icon col − 4×1.25rem gaps) / 3
       Total-column center = 0.875·W − 58.2px           (derived algebraically, verified: at W=1280
                                                           this gives 1061.8px, matching the row's own
                                                           measured Total-column center exactly)
     Positioned absolutely (escaping the flex row entirely, so the sibling
     icon/button widths can't distort it), then centered on that X via
     translateX(-50%). The absolutely-positioned child's containing block is
     .ocp-cartbar's PADDING BOX, but that box's left edge coincides with its
     border-box edge (padding sits inside the border box, doesn't shift its
     origin) — which is the viewport edge here (.ocp-cartbar has left:0) — so
     no extra offset for .ocp-cartbar's own padding is needed. (A first attempt
     subtracted padding-left here on that assumption and measured exactly
     padding-left's worth — 17.6px — off; removed.) */
  #ocp-quick-bar-track {
    position: absolute;
    left: calc(0.875 * min(100vw, 1280px) - 58.2px);
    top: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
  }
}
.ocp-remove {
  background: none;
  border: none;
  color: var(--ocp-muted);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0.3rem;
  text-decoration: underline;
}
/* Plain-text-button idiom (no background to swap) — same opacity press feedback
   as .ocp-quick-row-icon/.ocp-search-go, plus an ink-soft color darken on hover so
   a mouse user gets a legibility cue before committing to the tap. */
.ocp-remove:active { opacity: 0.65; }
@media (hover: hover) and (pointer: fine) {
  .ocp-remove:hover { color: var(--ocp-ink-soft); }
}

.ocp-cart-line-note { margin-top: 0.35rem; }
.ocp-note-add, .ocp-note-edit {
  border: none;
  background: none;
  padding: 0;
  font-size: 0.78rem;
  cursor: pointer;
  text-align: left;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}
.ocp-note-add { color: var(--ocp-muted); text-decoration: underline; }
.ocp-note-edit { color: var(--ocp-ink-soft); font-style: italic; }
.ocp-note-add:active, .ocp-note-edit:active { opacity: 0.65; }
@media (hover: hover) and (pointer: fine) {
  .ocp-note-add:hover { color: var(--ocp-ink-soft); }
  .ocp-note-edit:hover { color: var(--ocp-ink); }
}
/* Comment/stock-notification milestone (2026-08-31) — the flag-a-line-comment
   button (web equivalent of desktop's double-click-a-cart-row-to-comment). A
   distinct control from .ocp-note-add/.ocp-note-edit above, so it never sits
   inside their own [data-note-wrap] (which gets its innerHTML replaced when a
   note is being edited — see staffpos.js's own comment). */
.ocp-flag-comment {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  margin-left: 0.4rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--ocp-muted);
  cursor: pointer;
  border-radius: var(--ocp-r-sm);
  vertical-align: middle;
  flex-shrink: 0;
}
.ocp-flag-comment:active { opacity: 0.65; }
.ocp-flag-comment-done { color: var(--ocp-bad); }
.ocp-flag-comment:disabled { cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .ocp-flag-comment:not(:disabled):hover { color: var(--ocp-ink-soft); background: var(--ocp-line); }
}

/* Invoice-comment icon (Pos.cshtml's #ocp-invoice-comment-btn) — same "has
   content" signal as the flag icon above, but brand-colored not bad/red: a
   saved invoice comment isn't a warning, just informational. */
.ocp-call-btn.ocp-has-note { color: var(--ocp-brand); border-color: var(--ocp-brand); }

.ocp-note-input {
  width: 100%;
  font-size: 0.82rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  background: var(--ocp-page);
  color: var(--ocp-ink);
}
.ocp-note-input:focus { outline: none; border-color: var(--ocp-ink-soft); }

/* Price is now an always-visible FIELD (like Cost Price in Stock Entry), not
   a link that swaps into an input on tap — .ocp-price-input is rendered
   directly by priceFieldHtml (staffpos.js) whenever the tenant can edit price
   at sale, styled the same as .ocp-discount-input just below. A price
   actually overridden (not just editable) gets the same accent treatment as
   other "this line was changed" cues in this project, so a cashier scanning
   the cart can spot it without opening every field. */
.ocp-price-input.ocp-price-overridden { border-color: var(--ocp-brand); color: var(--ocp-brand); }
.ocp-price-input {
  width: 100%;
  max-width: 6.5rem;
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  background: var(--ocp-page);
  color: var(--ocp-ink);
}
.ocp-price-input:focus { outline: none; border-color: var(--ocp-ink-soft); }

/* Same input treatment as .ocp-price-input above, just narrower (a 0-100
   percentage never needs 6.5rem of room) — an always-visible field, same as
   Price, not a link/badge that swaps into an input on tap. */
.ocp-discount-input {
  width: 100%;
  max-width: 4rem;
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  background: var(--ocp-page);
  color: var(--ocp-ink);
  text-align: center;
}
.ocp-discount-input:focus { outline: none; border-color: var(--ocp-ink-soft); }

/* Milestone 1.12 — manual resync spinner. Respects reduced-motion (per this
   project's own build-cleanly rule) by just staying static rather than spinning
   forever-looking with no visual feedback at all — the button's own disabled
   state during the fetch is still the real "in progress" signal either way. */
.ocp-spin svg { animation: ocp-spin-rotate 0.8s linear infinite; }
@keyframes ocp-spin-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ocp-spin svg { animation: none; }
}

/* Fixed bottom cart bar — a flat, quiet toolbar (straight edges, thin top border,
   minimal shadow), not a floating rounded "bubble". Contrast comes from having
   exactly ONE rounded element in the bar (the CTA button) against otherwise straight
   lines — a bar that's rounded AND holds a pill button double-stacks the roundness
   and reads as toy-like. Restraint here is the fix. */
.ocp-cartbar[hidden] { display: none; } /* explicit: our own display:flex below would
   otherwise win the cascade over the UA stylesheet's [hidden]{display:none} rule,
   since both have equal specificity and ours loads later. */
.ocp-cartbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  background: var(--ocp-surface);
  border-top: 1px solid var(--ocp-line);
  box-shadow: 0 -1px 0 rgba(26, 25, 23, .02), 0 -4px 12px rgba(26, 25, 23, .05);
  padding: 0.8rem 1.1rem calc(0.8rem + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
/* min-width:0 lets this flex child actually shrink below its own content's
   natural width (the default min-width:auto blocks that) — needed so it's
   the flex-basis that gives way under a narrow phone, not the text wrapping
   mid-value. white-space:nowrap on the count/total spans themselves is the
   other half: found live (a real bug, not cosmetic) with "N units · M items"
   added before the total on the same line — a narrow bar plus a long Checkout
   button left too little room, and "MZN 20.00" was splitting across two
   lines instead of just staying tight against its neighbors. */
.ocp-cartbar-info { display: flex; flex-direction: column; min-width: 0; }
.ocp-cartbar-count { font-size: 0.75rem; color: var(--ocp-muted); white-space: nowrap; }
/* Same amber OneClickPOSWeb's StockEntry.cshtml uses for its own bottom-bar
   Total (--se-amber: #F5A623, .se-total-amount) — literal, not a theme token,
   matching that page's own choice exactly rather than reinterpreting it as an
   accent-color-following value. Shared by every cart-bar Total across this
   project (customer bar, Checkout, Staff Quick Search, Staff POS all render
   this exact class). Dark mode only — amber against light mode's pale page
   background reads as low-contrast/washed out, so light mode falls back to
   --ocp-ink (near-black) instead, same three-state pattern as every other
   theme-aware color in this file. */
/* max-width/overflow/ellipsis (2nd Opus review, 2026-09-06) — this shared class
   had no overflow guard at all; TableOrder.cshtml's own mobile cart bar can now
   show the "remaining [paid/total]" bracket (roughly 3x longer than a plain
   total) inside this element's `min-width:0` flex parent (.ocp-cartbar-info),
   which overflows visibly rather than shrinking without one. Harmless no-op for
   every other page's plain, short total. */
.ocp-cartbar-total { font-size: 1.15rem; font-weight: 700; color: var(--ocp-ink); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ocp-cartbar-total { color: #F5A623; }
}
:root[data-theme="dark"] .ocp-cartbar-total { color: #F5A623; }
.ocp-cartbar-btn {
  min-height: 44px;
  padding: 0 1.4rem;
  border-radius: var(--ocp-r-pill);
  border: none;
  /* The one brand-orange moment in the bar — this is the single most important CTA
     on the page (drives conversion), so it earns the accent color everything else
     deliberately doesn't use. */
  background: var(--ocp-brand);
  color: var(--ocp-brand-ink);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 700;
  font-size: 0.95rem;
  cursor: pointer;
  transition: opacity .12s ease;
}
.ocp-cartbar-btn:active { opacity: .85; }
.ocp-cartbar-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* --ocp-brand-dark, not opacity/filter — it's already defined per-color specifically
   as a slightly-darker variant of --ocp-brand for exactly this kind of pressed/hover
   state (see the html[data-color=...] blocks near the top of this file), so this is
   the correct token to reuse here rather than inventing a dimming effect. Used the
   same way for every other var(--ocp-brand)-filled button below (.ocp-btn-primary,
   .ocp-poscta-btn, .ocp-mixed-toggle's active state). */
@media (hover: hover) and (pointer: fine) {
  .ocp-cartbar-btn:hover:not(:disabled):not(.ocp-disabled) { background: var(--ocp-brand-dark); }
}
/* .ocp-cartbar-btn is an <a>, not a <button>, for the Checkout link specifically —
   :disabled above only ever matches real form controls, never an anchor, so the
   empty-cart disabled state needs its own class (see cart.js's applyToBar()). */
.ocp-cartbar-btn.ocp-disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }

/* Mobile cart drawer — phones don't have room for a persistent side panel like
   desktop/tablet gets, but navigating fully away to /cart just to check what's in
   it (and back) is the "make me wait, feeling stuck" pattern we're avoiding
   elsewhere. A chevron on the bar expands a sheet of editable line items directly
   above it (DoorDash/Uber Eats' bottom-cart-bar pattern) — the running total stays
   visible on the bar underneath at all times, open or closed. .ocp-cartbar is
   already `position: fixed`, which is itself a containing block, so the drawer only
   needs `position: absolute` to anchor exactly above it — no extra positioning
   context required. */
.ocp-cartbar-toggle {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--ocp-line);
  background: var(--ocp-page);
  color: var(--ocp-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.ocp-cartbar-toggle svg { transition: transform .18s ease; }
.ocp-cartbar-toggle:active { transform: scale(0.9); }
@media (hover: hover) and (pointer: fine) {
  .ocp-cartbar-toggle:hover { background: var(--ocp-line); }
}
/* #ocp-clear-top (Menu.cshtml's search row) moves to the bottom bar on mobile
   instead (#ocp-cartbar-clear, beside the chevron toggle above) — three icons
   (search-go, favorites, clear) plus the search field didn't fit a narrow phone
   width, measured with the row's right edge flush against the viewport edge
   (~0px margin). Desktop keeps it in the search row unchanged — the row has
   room there, and the bottom bar isn't even shown at that width. */
@media (max-width: 899px) {
  #ocp-clear-top { display: none; }
}
/* Same specificity as the browser's own [hidden]{display:none} UA rule, and this
   stylesheet loads after it — without this, [hidden] silently loses the tie. Never
   mattered for _CartBar.cshtml's own toggle (never hidden there, only one mode on
   the customer Menu page) until Staff Quick's #ocp-quick-browse-toggle started
   toggling [hidden] to hide it during Quick Search mode. Same bug class as
   .ocp-cartbar[hidden]/.ocp-appearance-popover[hidden] elsewhere in this file. */
.ocp-cartbar-toggle[hidden] { display: none; }
.ocp-cartbar-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }

.ocp-cartbar-drawer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  max-height: min(55vh, 420px);
  overflow-y: auto;
  background: var(--ocp-surface);
  border-top: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r) var(--ocp-r) 0 0;
  box-shadow: 0 -4px 16px rgba(26, 25, 23, .1);
  padding: 0.4rem 1.1rem;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
}
.ocp-cartbar-drawer[hidden] { display: none; }
.ocp-cartbar-drawer.ocp-open { opacity: 1; transform: translateY(0); pointer-events: auto; }
/* padding-right — two DIFFERENT things need clearing, both real, found via two
   separate live/device reports:
   (1) × itself needs room — .ocp-remove sits at a fixed right:0.6rem off the
   whole card (.ocp-cart-line), same everywhere in the app, and the general
   (non-drawer) row rule reserves 2.6rem of right padding for exactly that
   reason. This drawer-specific override was zeroing horizontal padding out
   entirely, so × was only ever clear of the Total by accident, for short
   values ("MZN 20.00") — a longer one ("MZN 280.00") measurably overlapped
   it (confirmed live: .ocp-remove's left edge landed inside the Total's own
   box). Matching the general rule's 2.6rem fixes this regardless of value
   length, the same way it already does everywhere else.
   (2) Screenshotted separately on a real device: the drawer's own scrollbar
   (an overlay style here — reserves zero layout width, confirmed live, draws
   ON TOP of content instead of pushing it aside) sitting across the Total
   figures once there are enough lines to scroll. 2.6rem already covers this
   too (previously a separate, smaller 0.75rem bolted on for just this), so
   there's only one number to reason about, not two. */
.ocp-cartbar-drawer .ocp-cart-line .ocp-swipe-content { padding: 0.65rem 2.6rem 0.65rem 0; }
.ocp-cartbar-drawer .ocp-cart-line:not(:last-child) { border-bottom: 1px solid var(--ocp-line); }

/* Owner request, 2026-09-04 — TableOrder's narrow-phone 4-tab bottom bar
   (Cart/Kitchen/Bill/Pay), mocked up and approved before building. Reuses
   .ocp-cartbar/-drawer for the fixed positioning + two collapsible drawers
   (already generic, not page-specific); only the tab row itself is new.
   Tablet keeps the existing single-chevron #ocp-order-mobile-bar unchanged —
   this bar and that one are mutually exclusive by width, enforced by JS
   (stafftableorder.js's updateMobileBarVisibility) and backstopped here. */
.ocp-order-phone-bar {
  padding: 0;
  align-items: stretch;
}
.ocp-phone-tab, .ocp-phone-tab-mount .ocp-print-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 0.55rem 0.25rem calc(0.55rem + env(safe-area-inset-bottom));
  background: none;
  border: none;
  border-right: 1px solid var(--ocp-line);
  color: var(--ocp-ink-soft);
  font-size: 0.68rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
.ocp-phone-tab-mount { display: flex; flex: 1; }
/* .ocp-order-print-kitchen-btn/-bill-btn are real .ocp-print-btn elements
   (relocated here as-is by positionCartFooter(), not cloned) — this reskins
   them from a full-width labeled row button into a tab, matching the other
   three tabs' own look, without needing a second copy with different markup. */
.ocp-phone-tab-mount .ocp-print-btn {
  width: 100%;
  height: 100%;
  margin: 0;
}
.ocp-phone-tab-mount .ocp-print-btn svg { width: 20px; height: 20px; }
.ocp-phone-tab:last-child, .ocp-phone-tab-mount:last-child .ocp-print-btn { border-right: none; }
.ocp-phone-tab:active, .ocp-phone-tab-mount .ocp-print-btn:active { background: var(--ocp-page); }
/* Owner request, 2026-09-04 — plain tinted TEXT on the Pay tab read as just
   another neutral icon sitting flush in the row, not as the page's primary
   action — inconsistent with how every other "this is the money button"
   moment in this app is styled (.ocp-poscta-btn/.ocp-cartbar-btn: the WHOLE
   control brand-filled, not just its text). Matches that convention here —
   solid fill, no divider needed since the fill itself is the boundary. */
.ocp-phone-tab-pay {
  background: var(--ocp-brand);
  color: var(--ocp-brand-ink);
  border-right: none;
}
.ocp-phone-tab-pay:active { background: var(--ocp-brand-dark); }
.ocp-phone-tab[aria-expanded="true"] svg { transform: rotate(180deg); }
.ocp-order-phone-bar .ocp-cartbar-drawer { padding: 0.4rem 1.1rem; }

@media (min-width: 480px) {
  #ocp-order-phone-bar[hidden] { display: none !important; }
}
@media (max-width: 479px) {
  #ocp-order-mobile-bar[hidden] { display: none !important; }
}

/* "Call this branch" — a plain tel: link needs no JS and works identically
   everywhere; styled to match the online-status pill / theme-toggle's quiet icon
   treatment rather than standing out as a second CTA next to "View cart". */
.ocp-call-btn {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  color: var(--ocp-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
/* Restaurant Milestone 2, 2.7 — "View recorded payments" is the first
   hidden-toggled .ocp-call-btn in this codebase (every other use is a static,
   always-visible icon button) — needs LESSONS.md #2's own [hidden] override,
   same reasoning already documented just below for .ocp-badge-count. */
.ocp-call-btn[hidden] { display: none; }

/* Pending-count badge on the Goods Received toolbar icon (Milestone 1.16
   follow-up). Absolute over its .ocp-call-btn parent (which is already
   position:relative via inline style, since it's the one .ocp-call-btn that
   needs an anchor) — [hidden] override needed per this file's own lesson #2,
   since the base rule sets display:flex for centering the number. */
.ocp-badge-count {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: var(--ocp-r-pill);
  background: var(--ocp-brand);
  color: var(--ocp-brand-ink);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
.ocp-badge-count[hidden] { display: none; }

/* Side drawer menu (Milestone 1.16 follow-up, user request 2026-08-29) — same
   surface/shadow/z-index family as .ocp-sheet above, sliding in from the side
   instead of up from the bottom. Uses [hidden] + a slide transform rather than
   .ocp-sheet's own "no display set" trick, since this needs display:flex for
   its column layout — hence the explicit [hidden] override (lesson #2). */
.ocp-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(300px, 85vw);
  z-index: 81;
  background: var(--ocp-surface);
  box-shadow: -8px 0 24px rgba(0, 0, 0, .18);
  transform: translateX(100%);
  transition: transform .2s ease;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding-bottom: env(safe-area-inset-bottom);
}
.ocp-drawer[hidden] { display: none; }
.ocp-drawer.ocp-open { transform: translateX(0); }
/* Backdrop sits at the same layer as .ocp-toast (z-index:80) — .ocp-drawer
   itself (81) is what actually wins the top layer while open; a toast firing
   mid-open (e.g. a background badge refresh) stays visually under the drawer
   panel because of that, even though it's later in DOM order than the
   backdrop. */
#ocp-drawer-backdrop { z-index: 80; }
/* The appearance popover's OWN pickers (Look/Color — #ocp-style-sheet/
   #ocp-color-sheet, both built by their own JS and appended to <body>, not
   inside the drawer) default to .ocp-sheet's z-index:61 — below the drawer
   (81) that now hosts their trigger. Without this override, opening either
   picker from inside the new drawer renders it dimmed and pointer-inert
   behind the drawer's own backdrop (lesson #3: a z-index addition can bury
   lower/auto-stacked siblings, here the drawer burying an EXISTING sheet it
   didn't know about). */
#ocp-style-sheet, #ocp-color-sheet { z-index: 91; }
#ocp-style-sheet-backdrop, #ocp-color-sheet-backdrop { z-index: 90; }
.ocp-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.1rem;
  border-bottom: 1px solid var(--ocp-line);
  flex: 0 0 auto;
}
.ocp-drawer-section {
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--ocp-line);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.ocp-drawer-section:last-child { border-bottom: none; margin-top: auto; }
.ocp-drawer-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ocp-muted);
}
/* position:relative so #ocp-theme-slot's injected .ocp-appearance-popover
   (position:absolute, top:calc(100% + 0.5rem)) anchors to this small row
   instead of falling through to .ocp-drawer itself (position:fixed, the
   next positioned ancestor) — anchoring to the full drawer height would
   push the popover below the drawer's own visible/scrollable box. */
.ocp-drawer-appearance-row { display: flex; gap: 0.5rem; position: relative; }
.ocp-drawer-link {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 0.4rem;
  border-radius: var(--ocp-r-sm);
  color: var(--ocp-ink);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 600;
  border: none;
  background: none;
  width: 100%;
  text-align: left;
  cursor: pointer;
}
.ocp-drawer-link:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-drawer-link:hover { background: var(--ocp-page); }
}
.ocp-drawer-link-danger { color: var(--ocp-bad); }
/* Quiet circular icon-button idiom (see .ocp-fav-btn above) — covers every
   .ocp-call-btn usage including the ID-keyed ones that reuse this same class
   (#ocp-fav-filter, #ocp-clear-top on Menu.cshtml's search row, and the inline-
   style-overridden #ocp-clear-cancel in the Clear-All confirm sheet), so no
   separate rule is needed for those. */
.ocp-call-btn:active { transform: scale(0.92); }
@media (hover: hover) and (pointer: fine) {
  .ocp-call-btn:hover { background: var(--ocp-page); }
}

/* Staff Quick order's entry-mode switch (Search / Browse) — a compact icon pair in
   the topbar reading as ONE toggle, not two full-width labeled buttons competing for
   vertical space with the actual content below. */
.ocp-mode-icons {
  display: inline-flex;
  flex: 0 0 auto;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-pill);
  background: var(--ocp-surface);
  padding: 2px;
  gap: 2px;
}
.ocp-mode-icon-btn {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--ocp-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.ocp-mode-icon-btn.ocp-active { background: var(--ocp-ink); color: var(--ocp-page); }
.ocp-mode-icon-btn:not(.ocp-active):active { transform: scale(0.9); }
.ocp-mode-icon-btn.ocp-active:active { opacity: .8; }
@media (hover: hover) and (pointer: fine) {
  .ocp-mode-icon-btn:not(.ocp-active):hover { background: var(--ocp-page); }
  .ocp-mode-icon-btn.ocp-active:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}

/* "Order it again" banner — only ever shown on an empty-cart Menu load when a past
   order exists for this branch, so it never competes with an active shopping
   session. Quiet surface treatment, not the brand accent — this is a convenience,
   not a primary CTA. */
.ocp-reorder-banner[hidden] { display: none; }
.ocp-reorder-banner {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  padding: 0.75rem 0.9rem;
  /* Renders as the true first child of .ocp-page when shown (conditional, ahead of
     .ocp-search) — needs its OWN top margin matching .ocp-search's 1.1rem, since it
     can't rely on that rule (which only ever applies to whichever element ends up
     first) to create the gap from the topbar above it. Without this it sat flush
     against the topbar ("touches the navbar"). */
  margin: 1.1rem 0 1rem;
  font-size: 0.86rem;
}
.ocp-reorder-banner span { flex: 1; min-width: 0; color: var(--ocp-ink-soft); }
.ocp-reorder-banner button { flex: 0 0 auto; }
.ocp-reorder-add {
  border: none;
  background: var(--ocp-ink);
  color: var(--ocp-page);
  font-weight: 700;
  font-size: 0.82rem;
  padding: 0.5rem 0.9rem;
  border-radius: var(--ocp-r-pill);
  cursor: pointer;
}
.ocp-reorder-add:active { opacity: .8; }
@media (hover: hover) and (pointer: fine) {
  .ocp-reorder-add:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}
.ocp-reorder-dismiss {
  border: none;
  background: transparent;
  color: var(--ocp-muted);
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ocp-reorder-dismiss:active { transform: scale(0.9); }
@media (hover: hover) and (pointer: fine) {
  .ocp-reorder-dismiss:hover { background: var(--ocp-page); border-radius: var(--ocp-r-pill); }
}

/* Forms */
.ocp-field { margin-bottom: 1rem; }
.ocp-field label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ocp-ink-soft);
  margin-bottom: 0.35rem;
}
.ocp-field input,
.ocp-field textarea,
.ocp-field select {
  width: 100%;
  min-height: var(--ocp-tap);
  padding: 0 0.9rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  font-size: 0.95rem;
  font-family: inherit;
  color: var(--ocp-ink);
}
.ocp-field input:focus,
.ocp-field textarea:focus,
.ocp-field select:focus {
  outline: none;
  border-color: var(--ocp-ink);
}
.ocp-field textarea { min-height: 5rem; padding: 0.65rem 0.9rem; }
/* The generic .ocp-field input rule above blows a checkbox up to a full-width,
   46px-tall blank box (appearance:auto native rendering doesn't scale its
   check glyph to fill an oversized box, so it just reads as a big empty
   square) — found live on the Mixed payment form's "Cash"/"Card"/"Mobile"
   toggles. Reset back to a normal small checkbox, sized/positioned to sit
   inline with its label text rather than stacking above it. */
.ocp-field label input[type="checkbox"] {
  width: 18px;
  height: 18px;
  min-height: 0;
  padding: 0;
  margin: 0 0.4rem 0 0;
  border-radius: 4px;
  vertical-align: -3px;
  accent-color: var(--ocp-brand);
}

/* A custom trigger button standing in for a <select> (opens a picker sheet
   instead of a native dropdown, e.g. the POS payment screen's Client field) —
   styled to read as the SAME kind of field as a real .ocp-field select
   (matching border/radius/height/background exactly, not a pill), with an
   explicit chevron since there's no native OS affordance to fall back on for
   a plain <button>. Previously misused .ocp-call-btn (a 36px circular
   icon-button class meant for things like the call/theme-toggle icons) with
   inline style overrides — border-radius:50% on a short, full-width element
   produced a stadium/pill shape with no dropdown affordance at all. */
.ocp-select-trigger {
  width: 100%;
  min-height: var(--ocp-tap);
  padding: 0 0.9rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  font-size: 0.95rem;
  font-family: inherit;
  font-weight: 600;
  color: var(--ocp-ink);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  text-align: left;
}
.ocp-select-trigger:focus-visible {
  outline: none;
  border-color: var(--ocp-ink);
}
.ocp-select-trigger:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-select-trigger:hover { background: var(--ocp-page); }
}
.ocp-select-trigger .ocp-select-trigger-chevron {
  flex: 0 0 auto;
  color: var(--ocp-ink-soft);
}

.ocp-segmented { display: flex; gap: 0.5rem; }
.ocp-segmented label {
  flex: 1;
  text-align: center;
  padding: 0.75rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  cursor: pointer;
  font-weight: 600;
  font-size: 0.88rem;
  color: var(--ocp-ink-soft);
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.ocp-segmented input { display: none; }
.ocp-segmented input:checked + label {
  border-color: var(--ocp-ink);
  background: var(--ocp-ink);
  color: var(--ocp-page);
}

/* Staff/Settings.cshtml — one row per branch, a real switch (not a checkbox) since
   this is a live on/off preference, not a form field being filled in. */
.ocp-settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.1rem;
}
.ocp-settings-row:not(:last-child) { border-bottom: 1px solid var(--ocp-line); }

/* Collapsible per-branch card (Staff/Settings.cshtml) — collapsed shows just a name
   + one-line summary of what's on/off, expanded reveals the actual toggles/fields.
   The header is a real <button> (unlike .ocp-list-row elsewhere in this project,
   which deliberately uses div+role=button — here there's no swipe/ripple gesture
   competing for the same element, and a native button's own UA background is
   overridden explicitly below anyway, so the dark-mode-invisible-text bug the style
   picker hit doesn't apply). */
.ocp-settings-card-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.1rem;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ocp-settings-card-header:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-settings-card-header:hover { background: var(--ocp-page); }
}
.ocp-settings-chevron { flex: 0 0 auto; color: var(--ocp-muted); transition: transform .15s ease; }
.ocp-settings-card-header[aria-expanded="true"] .ocp-settings-chevron { transform: rotate(180deg); }
.ocp-settings-card-body { border-top: 1px solid var(--ocp-line); }
.ocp-settings-card-body[hidden] { display: none; }
.ocp-switch { position: relative; display: inline-block; width: 44px; height: 26px; flex: 0 0 auto; }
.ocp-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.ocp-switch-track {
  position: absolute;
  inset: 0;
  background: var(--ocp-line);
  border-radius: var(--ocp-r-pill);
  transition: background .15s ease;
}
.ocp-switch-track::before {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  left: 3px;
  top: 3px;
  background: var(--ocp-surface);
  border-radius: 50%;
  box-shadow: var(--ocp-shadow);
  transition: transform .15s ease;
}
.ocp-switch input:checked + .ocp-switch-track { background: var(--ocp-brand); }
.ocp-switch input:checked + .ocp-switch-track::before { transform: translateX(18px); }
.ocp-switch input:focus-visible + .ocp-switch-track { outline: 2px solid var(--ocp-brand); outline-offset: 2px; }
/* staffsettings.js disables the input while its toggle POST is in flight — same
   opacity-dim idiom as .ocp-btn-primary:disabled/.ocp-poscta-btn:disabled below,
   applied to the visible track since the real input is the invisible (opacity:0)
   hit target. Without this the track already flips to its new position instantly
   (the checkbox's own :checked state, which the click applies before this file's
   change handler even runs) but gives no visible cue that a save is still pending
   underneath that flip. */
.ocp-switch input:disabled + .ocp-switch-track { opacity: 0.5; }
.ocp-switch input:disabled { cursor: not-allowed; }
/* filter:brightness, not a background swap — the track's background is one of TWO
   different tokens depending on checked state (--ocp-line off / --ocp-brand on), so
   a single filter works uniformly across both without needing two separate hover/
   active rules split on :checked. */
.ocp-switch:active .ocp-switch-track { filter: brightness(0.92); }
@media (hover: hover) and (pointer: fine) {
  .ocp-switch:hover .ocp-switch-track { filter: brightness(0.95); }
}

/* Staff/Settings.cshtml's appearance dropdowns — same field look as .ocp-field
   select but sized to sit on the right of a .ocp-settings-row instead of
   stretching full width. */
.ocp-settings-select {
  min-height: 40px;
  padding: 0 2rem 0 0.8rem;
  border-radius: var(--ocp-r-sm);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  font-size: 0.9rem;
  font-family: inherit;
  color: var(--ocp-ink);
  flex: 0 0 auto;
  max-width: 55%;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}

/* Alerts / states */
.ocp-alert {
  border-radius: var(--ocp-r-sm);
  padding: 0.8rem 1rem;
  font-size: 0.88rem;
  margin-bottom: 1rem;
}
/* var(--ocp-bad)/var(--ocp-ok), not hardcoded hex — those stay fixed mid-brightness
   reds/greens while the *-soft backgrounds flip dark in dark mode; a hardcoded dark
   shade (e.g. #7F1D1D) landed dark-on-dark once --ocp-bad-soft went dark. */
.ocp-alert-bad { background: var(--ocp-bad-soft); color: var(--ocp-bad); white-space: pre-line; }
.ocp-alert-ok { background: var(--ocp-ok-soft); color: var(--ocp-ok); }

/* Demo-tenant-only staff-login callout on the public branch picker (Index.cshtml) —
   only ever rendered when TenantInfo.IsDemoTenant is true, never on a real client
   build, so a real client's storefront never carries this. */
.ocp-demo-banner {
  border: 1px solid var(--ocp-line);
  border-left: 3px solid var(--ocp-brand);
  border-radius: var(--ocp-r-sm);
  background: var(--ocp-brand-soft);
  padding: 0.9rem 1.1rem;
  margin-bottom: 1.25rem;
}
.ocp-demo-banner a { color: var(--ocp-brand); font-weight: 700; font-size: 0.9rem; }

/* Sheet result rows (Staff/Quick.cshtml's product picker reuses .ocp-sheet/.ocp-list-row
   rather than a bespoke modal) — keyboard/mouse-hover highlight, same active-state
   language as the category chips/tabs above. */
.ocp-list-row.ocp-active { background: color-mix(in srgb, var(--ocp-ink) 10%, var(--ocp-surface)); }

/* Item 27 (2026-09-02) — the transfer item-select sheet's own rows share
   .ocp-list-row's base layout/tap-target but need a visually stronger, PERSISTENT
   selected state (not the momentary hover/keyboard-nav tint above) — a light
   brand tint, same token .ocp-card-img already uses for its own brand-tinted
   surface, so this reads as "chosen," not just "focused." */
.ocp-transfer-row.ocp-active { background: var(--ocp-brand-soft); }
.ocp-transfer-row.ocp-active [data-transfer-badge] { background: var(--ocp-brand); color: var(--ocp-brand-ink); }

/* Item 28 — the transfer destination sheet's own .ocp-list-row rows are divs, not
   real buttons, so a "disabled while a transfer is in flight" state (see
   setTransferDestRowsLocked in stafftableorder.js) needs its own visual, matching
   the opacity-dim idiom every real disabled control in this file already uses. */
.ocp-list-row.ocp-locked { opacity: 0.5; pointer-events: none; }

/* Staff fast-entry toast — a brief, dismissing-itself confirmation after "Send to
   POS" (the plan's "toast + clears + refocuses rather than navigating away"). */
.ocp-toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--ocp-cartbar-h) + 1.2rem);
  transform: translateX(-50%) translateY(8px);
  z-index: 80;
  background: var(--ocp-ink);
  color: var(--ocp-page);
  padding: 0.7rem 1.2rem;
  border-radius: var(--ocp-r-pill);
  font-size: 0.85rem;
  font-weight: 600;
  box-shadow: var(--ocp-shadow-float);
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
}
.ocp-toast.ocp-show { opacity: 1; transform: translateX(-50%) translateY(0); }

.ocp-empty {
  text-align: center;
  padding: 3.5rem 1rem;
  color: var(--ocp-muted);
}
.ocp-empty svg { color: var(--ocp-line); margin-bottom: 0.75rem; }
.ocp-empty p { margin: 0.15rem 0; }
.ocp-empty .ocp-empty-title { font-weight: 700; color: var(--ocp-ink); font-size: 1.05rem; }

.ocp-btn-primary {
  display: inline-block;
  min-height: var(--ocp-tap);
  line-height: var(--ocp-tap);
  padding: 0 1.5rem;
  border-radius: var(--ocp-r-pill);
  border: none;
  background: var(--ocp-brand);
  color: var(--ocp-brand-ink);
  font-weight: 700;
  font-size: 0.95rem;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.ocp-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
/* --ocp-brand-dark background swap, not opacity — see .ocp-cartbar-btn's own
   comment above for why this is the right token. Also correct under Menu.cshtml's
   #ocp-clear-confirm-btn inline style="background:var(--ocp-bad)" override: an
   opacity-based effect would work there too, but a couple of instances of
   .ocp-btn-primary elsewhere set an inline background (see that same element) that
   a plain :hover background rule can never win against anyway (inline style
   specificity always wins) — noted, not a blocker, since the button still gets a
   real pressed/hover cue everywhere .ocp-btn-primary is used at its own default
   brand color. */
.ocp-btn-primary:active:not(:disabled) { background: var(--ocp-brand-dark); }
@media (hover: hover) and (pointer: fine) {
  .ocp-btn-primary:hover:not(:disabled) { background: var(--ocp-brand-dark); }
}
/* Explicit color:#fff — .ocp-btn-primary's own color:var(--ocp-brand-ink) follows
   the accent picker, and in dark mode 9 of 11 accents resolve that to near-black,
   silently making WhatsApp-green text unreadable. This button's background is a
   fixed brand color (WhatsApp's own), not the site accent, so its text needs to
   be fixed too, not inherited. */
.ocp-btn-whatsapp { background: #25D366; color: #FFFFFF; }

/* Same sizing as .ocp-btn-primary, bordered/surface fill instead of the brand color
   — for a secondary choice sitting next to a primary one (e.g. Branch.cshtml's
   Send Order / Open POS pair), not a full second button system. */
.ocp-btn-secondary {
  display: inline-block;
  min-height: var(--ocp-tap);
  line-height: calc(var(--ocp-tap) - 2px);
  padding: 0 1.5rem;
  border-radius: var(--ocp-r-pill);
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  color: var(--ocp-ink);
  font-weight: 700;
  font-size: 0.95rem;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.ocp-btn-secondary:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-btn-secondary:hover { background: var(--ocp-page); }
}

/* ---- Pos.cshtml — payment flow polish ----
   Cart's bottom bar/panel footer now show the 5 payment-type buttons directly
   (no separate "cart checked, choose a type" screen — tapping one runs the
   precheck and advances straight to the type-fixed payment form). Scoped to
   .ocp-pos-page/.ocp-pos-bar/.ocp-pos-panel-footer, NOT the bare #ocp-quick-bar/
   #ocp-quick-panel-footer ids — Quick.cshtml uses those same ids for its own
   single "Send to POS" button and must stay untouched. */
.ocp-pos-page { --ocp-cartbar-h: 140px; }

.ocp-pos-bar { flex-direction: column; align-items: stretch; padding: 0; }
/* .ocp-layout has its own margin:0 auto for centering — a flex item with auto
   margins in the cross axis does NOT get stretched by align-items:stretch (the
   auto margins absorb the stretch space instead, per spec), so without this it
   collapses toward min-content width instead of the intended 1280px-centered
   box, dragging everything nested inside it (down to the TOTAL/count text) to
   near-zero width too. An explicit width makes the centering robust regardless
   of stretch behavior — the same fix any flex-item-that-also-self-centers needs. */
.ocp-pos-bar .ocp-layout { width: 100%; }
/* .ocp-pos-bar-page overrides ONLY the bottom padding .ocp-page normally carries
   for scroll clearance under the real page content — not useful inside a fixed
   bar. Left/right padding and max-width are left alone (inherited from .ocp-page/
   .ocp-layout as actually reused in the markup) so this box is computed by the
   same rules as the real page, not a hand-derived approximation. */
.ocp-pos-bar-page { position: relative; padding-top: 0.6rem !important; padding-bottom: calc(0.5rem + env(safe-area-inset-bottom)) !important; display: flex; flex-direction: column; gap: 0.5rem; }
.ocp-pos-bar-toprow { display: flex; align-items: center; gap: 0.6rem; min-height: 22px; padding-right: 130px; }
/* Count before total, same line (not stacked). Scoped under .ocp-pos-bar-page —
   NOT just the #ocp-quick-bar-track id, which Quick.cshtml's own (structurally
   different, plain-flex-row) bar also uses; an unscoped id-only version of this
   rule once applied there too and broke it (the total got absolutely positioned
   out of Quick.cshtml's flex row entirely, overlapping the "Next" button —
   screenshotted, real regression). .ocp-pos-bar-page only exists on Pos.cshtml's
   bar, so this can't leak onto Quick.cshtml's again.
   left:auto/transform:none are explicit, not defaults left implicit — Quick.cshtml
   has its OWN #ocp-quick-bar-track rule at ≥992px (this file, "Bottom bar's
   total, aligned to..." above) that sets left/transform on the bare id with no
   class scoping of its own; since ids match across both pages, that rule's
   left/transform were STILL applying here even after right/top were correctly
   overridden (confirmed live — computed left:1061.8px, transform translating by
   the OLD rule's own -50%/-50%, only rendering in the right place at exactly
   1280px width by coincidence, where both pages' formulas happen to converge —
   drifted at every other desktop width). Reset explicitly so this rule alone
   fully determines the box, nothing inherited from the older id-only rule.
   right:0.9rem (not 0) — .ocp-pos-bar-page inherits .ocp-page's own right
   padding at this width, so right:0 sits flush against the PADDING edge, not
   the page's real content edge; each cart-line card has its OWN 0.9rem right
   padding on top of that (.ocp-quick-row-content), so the card's TOTAL column
   sits a further 0.9rem in from where the page padding alone would put it —
   matching that inset is what actually lines the two up, confirmed live.
   top: measured live against .ocp-pos-bar-toprow's own real center (both are
   siblings under .ocp-pos-bar-page; the toprow's chevron/clear icons sit
   vertically centered via flexbox, while this track is absolutely positioned
   and can't share that same centering, so its top has to be hand-matched to
   the toprow's actual rendered center instead of guessed). */
.ocp-pos-bar-page .ocp-cartbar-info#ocp-quick-bar-track { position: absolute; left: auto; transform: none; top: 0.5rem; right: 1.9rem; flex-direction: row; align-items: baseline; gap: 0.4rem; text-align: right; }
/* Desktop-only correction — .ocp-layout only becomes a flex row (with .ocp-page as
   a stretched flex child) at this same breakpoint; that specific nesting measured
   its own containing-block width (AND, it turns out, vertical offset — see top
   below) for absolutely-positioned descendants short of its real rendered value
   by a consistent, confirmed-live amount, so the mobile values under/over-shoot
   there by a different margin. Narrow width never enters that flex context
   (.ocp-layout is a plain block below 900px), so the mobile values are already
   correct there — this override must NOT apply narrower than 900px or it would
   push the total off-position on mobile instead of fixing it. */
@media (min-width: 900px) {
  /* top: re-measured after fixing the left/transform leak above — the previous
     1.3rem value was tuned while transform:translate(-50%,-50%) was still
     leaking in from Quick.cshtml's rule, and that transform's Y-component
     (-50% of this element's own ~25px height, i.e. about -12.5px) was quietly
     doing most of the real centering work. Removing the leak without
     re-deriving top left a consistent ~12.7px gap at every desktop width
     (confirmed identical at both 1000px and 1280px, so this is a real
     constant offset, not another width-dependent drift). */
  /* right: re-derived — the previous -3.65rem was tuned back when this block
     held only the Total figure; adding the "N units · M items" text before it
     (a later round) made the whole flex row wider, and since the block is
     right-anchored as one rigid unit (not the Total specifically), that extra
     text shifted the Total's own on-screen position away from the row's Total
     column above it without moving the block's own right edge at all —
     reported and re-measured live: Total sat ~205px right of the row's Total
     column center. Re-derived the same way as every other magic number in
     this block: measured live, not guessed. */
  .ocp-pos-bar-page .ocp-cartbar-info#ocp-quick-bar-track { right: 9.14rem; top: 0.5rem; }
  /* Discount-enabled case only — the Discount column adds a 4th grid track to
     each cart row (Qty/Discount/Price/Total instead of Qty/Price/Total), which
     shifts the row's own Total column to a DIFFERENT on-screen position than
     the non-discount case at the same viewport width. A fixed rem offset can't
     track both cases at once (confirmed live: the row Total's right edge moves
     at a different px-per-viewport-px rate than this bar's own fixed-right
     positioning does below the 1280px cap), so this needs its own width-
     dependent formula rather than a second constant. Derived from two real,
     live-measured points (not guessed): at W=1280 the row Total's right edge
     sits 145px from the viewport edge; at W=1100 it sits 129px — i.e. slope
     ~0.088889 (16px over 180px), matched here via calc() against the same
     min(100vw,1280px) cap .ocp-layout itself stops growing at. Confirmed
     exact (0px gap) live at both 1280 and 1100. This segment is only valid
     ≥992px — see the 900-991px override below for why. */
  .ocp-pos-bar-page.ocp-discount-enabled .ocp-cartbar-info#ocp-quick-bar-track { right: calc(0.088889 * min(100vw, 1280px) + 7.462px); }
}
@media (min-width: 900px) and (max-width: 991.98px) {
  /* The row grid itself has its own real breakpoint at 992px (the
     display:contents desktop-only realignment, see the .ocp-quick-col-total
     rule around line 1443) — the row Total's on-screen position genuinely
     jumps there, not just slows/speeds its growth rate. The ≥992px formula
     above was calibrated entirely from points ≥992px and left a real,
     confirmed-live ~6-7px gap in this narrower 900-991px band. Re-derived
     from its own two live points in this band (W=900: needed right=80.462px;
     W=970: needed right=89.462px — slope ~0.12857), confirmed 0-1px gap live
     across 900/950/970. */
  .ocp-pos-bar-page.ocp-discount-enabled .ocp-cartbar-info#ocp-quick-bar-track { right: calc(0.12857 * (100vw - 900px) + 80.462px); }
}

.ocp-poscta-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.3rem; }
/* Hidden entirely (not just disabled) once a payment attempt is underway —
   showPaymentStep (staffpos.js) toggles this so the cart bar/side-panel's own
   5 type buttons don't sit visibly under a totally separate, type-fixed payment
   form. Author `display: grid` above always beats the browser's UA default
   [hidden]{display:none} regardless of source order (origin, not specificity,
   decides that fight) — CLAUDE.md's lesson #2, same fix shape as
   .ocp-summary-row[hidden]/.ocp-totchange-row[hidden] elsewhere in this file. */
.ocp-poscta-grid[hidden] { display: none; }
.ocp-poscta-btn {
  /* --ocp-tap (46px) — the project's own standard minimum tap target, used as
     the floor everywhere else (.ocp-field input, .ocp-btn-primary, etc.) rather
     than a bespoke bigger size for just this one component; the bar was eating
     a lot of vertical space at the previous height. */
  min-height: var(--ocp-tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.12rem;
  /* Whole button brand-filled — the SAME color "Open POS"/.ocp-btn-primary/
     .ocp-cartbar-btn already use for the app's primary actions (--ocp-brand),
     not --ocp-ink. These 5 buttons ARE that same class of action (driving the
     actual sale), so they get the app's one real "this is the important button"
     color, not the separate ink-filled treatment small utility icon actions
     (.ocp-search-go, .ocp-scan-fab) use — those two colors mean different
     things elsewhere in this app and were wrongly conflated here at first. */
  background: var(--ocp-brand);
  border: 1px solid var(--ocp-brand);
  border-radius: var(--ocp-r-sm);
  padding: 0.3rem 0.15rem;
  font-family: inherit;
  font-size: 0.66rem;
  font-weight: 700;
  color: var(--ocp-brand-ink);
  cursor: pointer;
}
.ocp-poscta-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; margin-bottom: 0.1rem; color: inherit; }
.ocp-poscta-icon svg { width: 13px; height: 13px; }
.ocp-poscta-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Item 33 (2026-09-02) — owner report: "check all button hovers to ensure this
   does not happen as cash icon disappeared due to same colors". Root cause: the
   icon's cutout details (the coin's inner circle, a card's stripe) are inline
   SVG fill="var(--ocp-brand)" — a literal snapshot of the button's UN-hovered
   background, meant to "punch through" to it (see _PosTypeGrid.cshtml's own
   comment). Swapping the background to a DIFFERENT token on hover/active
   (--ocp-brand-dark) broke that illusion in exactly the color/theme
   combinations where --ocp-brand-dark reads close to --ocp-brand-ink (the
   icon's own outer-shape color) — the cutout stopped matching the real
   background, and in the worst case the whole icon lost contrast against it.
   Fixed the same way .ocp-switch's own active state already handles an
   identical "keep a two-tone icon's colors in the same relative relationship"
   need: filter: brightness() darkens the WHOLE rendered button uniformly —
   background, outer shape, and the fixed-color cutout all scale together, so
   the cutout keeps matching its background in every theme/color combination,
   not just the default one. */
.ocp-poscta-btn:not(.ocp-mixed-toggle):active:not(:disabled) { filter: brightness(0.9); }
@media (hover: hover) and (pointer: fine) {
  .ocp-poscta-btn:not(.ocp-mixed-toggle):hover:not(:disabled) { filter: brightness(0.94); }
}
.ocp-poscta-grid.ocp-checking { opacity: 0.5; pointer-events: none; }
.ocp-pos-panel-footer .ocp-poscta-btn { font-size: 0.85rem; }

/* Mixed's Cash/Card/Mobile toggles — same .ocp-poscta-btn button, but OFF by
   default (outline, not filled) since Mixed starts with nothing selected;
   .ocp-active flips it to the same filled-brand look the cart-screen type
   buttons always have. Real buttons, not checkboxes, so this reads as "tap to
   include this method" rather than a bare checkbox+label. */
.ocp-mixed-toggle-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.3rem; margin: 0.2rem 0 0.9rem; }
.ocp-mixed-toggle { background: var(--ocp-surface); border-color: var(--ocp-line); color: var(--ocp-ink-soft); }
/* Bug fix (2026-09-05) — reported live: "Cash's icon color is the same as the
   hover/active color" (invisible when selected). Root cause: `rect:last-of-type`
   assumes every icon has a SECOND rect that's the cutout (true for Card/Mobile,
   both genuinely two-rect icons) — but Cash's cutout is a CIRCLE, and Cash has
   only ONE rect total (its outer coin/bill shape). `:last-of-type` matches a
   lone element trivially (there's no other rect to not be "last"), so this rule
   ALSO recolored Cash's outer shape, not just its circle cutout. Harmless in the
   OFF state (currentColor already resolves to the same --ocp-ink-soft value
   here, so the override was a no-op) but broke the ACTIVE state completely:
   verified live, Cash's rect AND circle both computed to the exact same
   rgb() as the button's own --ocp-brand background — a fully invisible icon,
   not just a subtle color clash. Fixed with `:nth-of-type(2)` instead of
   `:last-of-type` — explicitly "the second rect, if one exists," which
   correctly matches Card/Mobile's real stripe cutout and matches nothing at
   all for Cash's single rect, leaving it on `currentColor` as intended.
   Bug fix (2026-09-06) — the OFF-state half of that rule (removed here) was
   only a no-op for Cash's OWN RECT (the `rect:nth-of-type(2)` half of the
   selector, which matches nothing on Cash's single-rect icon) — but the
   SAME rule's `circle:last-of-type` half DID match Cash's own circle, so
   removing this rule also changes Cash's off-state circle back to its own
   inline `fill="var(--ocp-brand)"` instead of the forced ink-soft value
   (correction, 4th Opus review pass — the original version of this comment
   overstated the removal as fully inert for Cash). This is a beneficial,
   not harmful, side effect: it gives Cash's off-state icon the same
   genuine two-tone treatment Card/Mobile now get back below, rather than
   Cash alone staying a flat silhouette while the other two regained
   contrast. The rule this same comment is really about — forcing
   Card/Mobile's real 2nd-rect stripe to the SAME ink-soft value as the
   body's own currentColor (line above) — collapsed their genuinely
   two-tone icon into one flat-colored silhouette in the everyday,
   non-active state — reported live as Card's icon reading as barely more
   than a thin line at 13px. Removing it lets Card/Mobile's stripe fall back
   to its own inline `fill="var(--ocp-brand)"`, restoring real two-tone
   contrast exactly where there was never a clash to fix in the first place
   (the ACTIVE-state override below, which exists BECAUSE of a real clash —
   brand background behind a brand-colored stripe — is untouched). */
.ocp-mixed-toggle.ocp-active { background: var(--ocp-brand); border-color: var(--ocp-brand); color: var(--ocp-brand-ink); }
.ocp-mixed-toggle.ocp-active .ocp-poscta-icon svg circle:last-of-type, .ocp-mixed-toggle.ocp-active .ocp-poscta-icon svg rect:nth-of-type(2) { fill: var(--ocp-brand); }
.ocp-mixed-toggle[hidden] { display: none; }
/* Two-state press/hover, matching whichever fill the toggle currently has: off
   (bordered surface, same idiom as .ocp-cat-trigger etc.) vs on. Item 33
   (2026-09-02) — the ON state used to swap background to --ocp-brand-dark on
   press/hover while its own cutout fill (line 2610) stayed pinned to the
   un-hovered --ocp-brand, the same cutout-mismatch bug fixed above for the
   5-button grid — same filter: brightness() fix, same reasoning (darkens
   background/cutout/outer-shape together, uniformly, in every theme). The OFF
   state's own background swap (--ocp-page) is untouched — its cutout uses the
   theme-neutral --ocp-ink-soft, not a brand token, so it isn't exposed to this
   specific mismatch. */
.ocp-mixed-toggle:not(.ocp-active):active:not(:disabled) { background: var(--ocp-page); }
.ocp-mixed-toggle.ocp-active:active:not(:disabled) { filter: brightness(0.9); }
@media (hover: hover) and (pointer: fine) {
  .ocp-mixed-toggle:not(.ocp-active):hover:not(:disabled) { background: var(--ocp-page); }
  .ocp-mixed-toggle.ocp-active:hover:not(:disabled) { filter: brightness(0.94); }
}

/* Active-field highlight — which .ocp-amount-input the shared keypad is
   currently typing into isn't obvious until you tap a key and see a digit
   land somewhere; this marks it up front. */
.ocp-amount-input.ocp-active-field { border-color: var(--ocp-brand); box-shadow: 0 0 0 2px var(--ocp-brand-soft); }

/* Superseded, 2026-08-31 — .ocp-amount-input is type="text" now (an Opus
   review finding: type="number" silently sanitizes .value to "" for any
   comma-decimal entry, e.g. "246,80", BEFORE staffpos.js's own comma-handling
   parsePriceInput ever runs — the earlier readonly-removal fix alone didn't
   actually make comma input work, only direct-typing with a period). A plain
   text input never had spinner arrows to begin with, so the suppression rules
   that briefly lived here are gone, not just hidden. */

/* Numeric keypad — an additional, tap-friendly way to fill any .ocp-amount-input
   alongside direct typing (inputmode="decimal" on each field) — a cashier taps
   through amounts without needing to reach for a keyboard. Hidden entirely for
   Credit (no amount field to target) — see staffpos.js's showPaymentStep. */
.ocp-keypad { margin: 0.9rem 0 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.ocp-keypad[hidden] { display: none; }
.ocp-key {
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: inherit;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--ocp-ink);
  cursor: pointer;
}
.ocp-key:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-key:hover { background: var(--ocp-page); }
}
/* var(--ocp-brand), not --ocp-brand-dark — --ocp-brand-dark in dark mode is the
   LIGHT-mode brand value (dark, saturated), which reads as low-contrast text on
   the dark --ocp-surface these three sit on. --ocp-brand is the correct "accent
   text on a surface" token in both themes, matching .ocp-demo-banner a and
   .ocp-fav-btn.ocp-fav-active's own established pattern. */
.ocp-key-fn { color: var(--ocp-brand); font-size: 0.9rem; }

/* Sale-complete summary card + print placeholders */
.ocp-summary-card { margin-top: 1.1rem; background: var(--ocp-surface); border: 1px solid var(--ocp-line); border-radius: var(--ocp-r-sm); overflow: hidden; }
.ocp-summary-row { display: flex; justify-content: space-between; align-items: baseline; padding: 0.6rem 0.9rem; }
/* Real bug, live-reproduced: populateConfirmedSummary correctly sets hidden=true
   on #ocp-pay-confirmed-change-row for a zero-change sale, but the row rendered
   anyway (display:flex above always beats the browser's default [hidden]{display:
   none} — author CSS wins over UA styles outright, not even a specificity tie),
   showing the STALE change text left over from a previous sale that DID have
   change. Same recurring bug class this project has hit 3+ times before
   ([hidden] losing to a same-specificity/higher-priority custom display rule) —
   see CLAUDE.md's lesson #2. */
.ocp-summary-row[hidden] { display: none; }
.ocp-summary-row + .ocp-summary-row { border-top: 1px solid var(--ocp-line); }
.ocp-summary-label { font-size: 0.78rem; color: var(--ocp-ink-soft); font-weight: 600; }
.ocp-summary-value { font-size: 0.85rem; font-weight: 700; text-align: right; }
.ocp-summary-total .ocp-summary-value { color: var(--ocp-brand); font-size: 1rem; }
.ocp-summary-change .ocp-summary-value { color: var(--ocp-brand); }

.ocp-print-row { margin-top: 0.9rem; display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.ocp-print-btn {
  min-height: var(--ocp-tap);
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--ocp-ink-soft);
  cursor: pointer;
}
.ocp-print-btn:active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-print-btn:hover { background: var(--ocp-page); }
}
/* Restaurant Milestone 2, 2.5 — Cancel Order/Cancel to Edit reuse this class and
   are shown/hidden via the [hidden] attribute (LESSONS.md #2's bug class: an
   author-stylesheet display:flex rule of equal specificity otherwise beats the
   UA stylesheet's [hidden]{display:none}). */
.ocp-print-btn[hidden] { display: none; }

/* Logic/UI-UX review (2026-09-01) — the "hide the staff chrome, print on
   white" rule was copy-pasted byte-for-byte into Fecho.cshtml's and
   EndDay.cshtml's own <style> blocks; Report.cshtml/Stock.cshtml had no Print
   action or print rule at all. Every class referenced here (.ocp-topbar/
   .ocp-no-print/.ocp-drawer/.ocp-sheet-backdrop/.ocp-page) is already a
   global, site-wide name — safe to define once, unconditionally, since a
   page that has none of these elements simply gets a no-op rule. The
   print-styled standalone pages (KitchenTicket/Bill/Invoice/TableQr) keep
   their own distinct @media print blocks — they use a different toolbar
   shape entirely (.ocp-inv-toolbar, not the staff topbar/drawer), not the
   same duplication. */
@media print {
  .ocp-topbar, .ocp-no-print, .ocp-drawer, .ocp-sheet-backdrop { display: none !important; }
  .ocp-page { padding: 0 !important; }
  body { background: #fff !important; color: #000 !important; }
}

/* Read-only Total/Change pair on the payment step — mirrors desktop's own
   PaymentScreen.cs TOTAL|CHANGE row. Neither value is ever submitted; both are
   purely informational (see the comment on #ocp-pay-totchange-row in Pos.cshtml). */
/* Payment/confirmation screens read as a focused single-column form, not a
   full-width page — especially now that the topbar is hidden while on them
   (nothing else is competing for the width). Narrow width (phones) is left
   alone; this only kicks in once there's real spare width to center within. */
@media (min-width: 700px) {
  #ocp-step-payment, #ocp-step-confirmed { max-width: 480px; margin: 0 auto; }
}

/* Credit's balance-note message ("This sale will use part of this client's
   existing balance automatically.", #ocp-pay-credit-balance in Pos.cshtml) —
   was plain .ocp-sub text with no dedicated rule of its own (no bare .ocp-sub
   rule exists at all outside two other page-scoped variants elsewhere in this
   file), so it sat flush against the Client field above and the Total|Account
   boxes below with no breathing room. Boxed to match .ocp-totchange-col just
   below it (same page-tinted card treatment) so the two read as one connected
   informational group instead of a stray sentence between unrelated elements. */
#ocp-pay-credit-balance {
  background: var(--ocp-page);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  padding: 0.65rem 0.85rem;
  margin: 0 0 1rem;
  line-height: 1.45;
  font-size: 0.85rem;
  color: var(--ocp-ink-soft);
}

.ocp-totchange-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 1rem; }
.ocp-totchange-row[hidden] { display: none; }
.ocp-totchange-col { background: var(--ocp-page); border: 1px solid var(--ocp-line); border-radius: var(--ocp-r-sm); padding: 0.65rem 0.85rem; }
.ocp-totchange-label { font-size: 0.72rem; font-weight: 700; color: var(--ocp-ink-soft); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.2rem; }
.ocp-totchange-value { font-size: 1.05rem; font-weight: 700; color: var(--ocp-ink); font-variant-numeric: tabular-nums; }

.ocp-branch-card {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r);
  padding: 1.1rem 1.2rem;
  margin-bottom: 0.75rem;
  text-decoration: none;
  color: var(--ocp-ink);
  background: var(--ocp-surface);
  box-shadow: var(--ocp-shadow);
  transition: transform .12s ease;
}
.ocp-branch-logo {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: var(--ocp-r-sm);
  object-fit: contain;
  background: var(--ocp-page);
  border: 1px solid var(--ocp-line);
  padding: 3px;
}
/* Theme toggle — injected by theme.js into each page's #ocp-theme-slot (a small
   anchor every topbar declares in its own icon row), NOT position:fixed. It used to
   float fixed at the top-right corner, which silently sat on top of whatever else
   lived in that corner (the share/call icons on Menu, the "← Cart" back link on
   Checkout) — a real "hidden button" bug, not just a visual quirk. Sized to match
   .ocp-call-btn so it reads as one consistent row of icon buttons. */
.ocp-theme-toggle {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--ocp-line);
  background: var(--ocp-surface);
  color: var(--ocp-ink-soft);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.ocp-theme-toggle:active { transform: scale(0.92); }
@media (hover: hover) and (pointer: fine) {
  .ocp-theme-toggle:hover { background: var(--ocp-page); }
}

/* One "Appearance" button (colorswitcher.js's consolidateAppearanceButtons) tucking
   theme/style/color's three separate icon buttons behind it, instead of all three
   sitting inline in the topbar — anchored under the trigger, right-aligned so it
   never runs off the right edge of a narrow phone screen. */
.ocp-appearance-popover {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 40;
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r);
  box-shadow: var(--ocp-shadow-float);
}
/* Same specificity as the browser's own [hidden]{display:none} UA rule, and this
   stylesheet loads after it — without this explicit override, [hidden] silently
   loses that tie and the popover shows open on every page load. Same bug class as
   .ocp-cartbar[hidden] elsewhere in this file — worth the explicit rule every time
   a custom "display: flex/grid/etc." class might combine with [hidden]. */
.ocp-appearance-popover[hidden] { display: none; }

/* Checkout's "Order summary" — an invoice-style grid (Item/Qty/Price/Total), not a
   single "2x Name ... total" line, so a shopper can see the per-unit price they're
   being charged. Fixed-width numeric columns (not flex:auto) so every row's numbers
   land in the same place regardless of how long an item name or how many digits a
   total has — an actual grid, not eyeballed alignment. */
.ocp-invoice-table { display: flex; flex-direction: column; }
.ocp-invoice-row {
  display: grid;
  grid-template-columns: 1fr 2.4rem 4.5rem 4.5rem;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--ocp-line);
  font-size: 0.85rem;
}
.ocp-invoice-row:last-child { border-bottom: none; }
.ocp-invoice-head {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ocp-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border-bottom: 1px solid var(--ocp-line);
  padding-bottom: 0.5rem;
}
.ocp-invoice-item { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ocp-ink); }
.ocp-invoice-head .ocp-invoice-item { color: var(--ocp-muted); }
.ocp-invoice-qty { text-align: center; color: var(--ocp-ink-soft); }
.ocp-invoice-price { text-align: right; color: var(--ocp-ink-soft); }
.ocp-invoice-total { text-align: right; font-weight: 700; color: var(--ocp-ink); }
.ocp-invoice-head .ocp-invoice-total { font-weight: 700; }
@media (max-width: 380px) {
  /* A very narrow phone loses the Price column rather than truncating the item
     name down to nothing — Qty and Total are the two numbers that matter most to
     glance-check ("did I add the right amount"); unit price is a nice-to-have that
     can be recovered by tapping into Cart. */
  .ocp-invoice-row { grid-template-columns: 1fr 2.2rem 4.2rem; }
  .ocp-invoice-price { display: none; }
}
@media (min-width: 992px) {
  /* Same 25%-name / 75%-split-3-ways concept as the desktop Quick Search row
     grid — the fixed-rem Qty/Price/Total columns above left most of a wide
     desktop row empty (Item stretching to fill 1fr while three ~4.5rem
     numeric columns huddled at the far right). */
  .ocp-invoice-row { grid-template-columns: 25% repeat(3, 1fr); }
}

/* Material-ish tap feedback: a ripple expanding from the tap point. Pure CSS/JS, no
   library — the selector list here must match RIPPLE_SELECTOR in theme.js. Tapping a
   card/row IS the add-to-cart action (no separate "+" badge/checkmark any more — the
   cart bar's total updating is confirmation enough), so it gets the same ripple as
   every other tappable surface. */
.ocp-list-row, .ocp-card, .ocp-btn-primary, .ocp-cartbar-btn, .ocp-branch-card, .ocp-tab, .ocp-poscta-btn {
  position: relative;
  overflow: hidden;
}
.ocp-ripple {
  position: absolute;
  border-radius: 50%;
  background: var(--ocp-ripple);
  transform: scale(0);
  animation: ocp-ripple-anim 500ms ease-out;
  pointer-events: none;
}
@keyframes ocp-ripple-anim {
  to { transform: scale(2.6); opacity: 0; }
}

.ocp-branch-card:active { transform: translateY(1px); background: color-mix(in srgb, var(--ocp-ink) 10%, var(--ocp-surface)); }
/* Same elevate-on-hover cue as .ocp-card below — already presses down on :active,
   so hover lifts the other direction instead of trying to reuse that same
   transform value for both states. Owner-flagged (2026-09-07) — the shadow
   alone was the exact "barely-there in light mode" defect already fixed once
   for .ocp-list-row/.ocp-table-card (this rule is reused as the row style for
   every branch/table/takeaway/pending-order picker list in the app, e.g.
   Receive.cshtml/OnlineOrders.cshtml), so it gets the same color-mix tint. */
@media (hover: hover) and (pointer: fine) {
  .ocp-branch-card:hover { box-shadow: var(--ocp-shadow-float); background: color-mix(in srgb, var(--ocp-ink) 8%, var(--ocp-surface)); }
}
.ocp-branch-card strong { font-size: 1.02rem; font-weight: 700; }
.ocp-branch-card .ocp-sub { display: block; color: var(--ocp-muted); font-size: 0.85rem; margin-top: 0.25rem; }

/* Camera barcode scan FAB (Barcode-style branches only) — hidden by default via the
   [hidden] attribute until scanner.js confirms BarcodeDetector support. */
.ocp-scan-fab[hidden] { display: none; }
.ocp-scan-fab {
  position: fixed;
  right: 1.1rem;
  bottom: calc(var(--ocp-cartbar-h) + 1.1rem);
  z-index: 25;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: none;
  background: var(--ocp-ink);
  color: var(--ocp-page);
  box-shadow: var(--ocp-shadow-float);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.ocp-scan-fab:active { transform: scale(0.92); }
@media (hover: hover) and (pointer: fine) {
  .ocp-scan-fab:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}

.ocp-scanner-modal[hidden] { display: none; }
.ocp-scanner-modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: #0B0A09;
  display: flex;
  flex-direction: column;
}
.ocp-scanner-video {
  flex: 1;
  width: 100%;
  object-fit: cover;
  background: #000;
}
.ocp-scanner-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(78vw, 320px);
  height: 180px;
  border: 3px solid rgba(255, 255, 255, .85);
  border-radius: 16px;
  pointer-events: none;
}
.ocp-scanner-top {
  position: absolute;
  top: 0; left: 0; right: 0;
  display: flex;
  justify-content: flex-end;
  padding: 1rem;
  z-index: 1;
}
.ocp-scanner-close {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,.15);
  color: #fff;
  font-size: 1.2rem;
  cursor: pointer;
}
/* Fixed rgba(255,255,255,...), not a var(--ocp-...) token — deliberate, matching
   this element's own existing background/color above: .ocp-scanner-modal is a
   fixed #0B0A09 full-screen camera overlay that never themes with light/dark mode
   or the accent picker (same reasoning as .ocp-btn-whatsapp's fixed brand color
   elsewhere in this file), so a token-based hover would be inconsistent with
   its own resting state, not more theme-correct. */
.ocp-scanner-close:active { background: rgba(255,255,255,.32); }
@media (hover: hover) and (pointer: fine) {
  .ocp-scanner-close:hover { background: rgba(255,255,255,.24); }
}
.ocp-scanner-bottom {
  padding: 1rem 1.2rem calc(1.2rem + env(safe-area-inset-bottom));
  background: #0B0A09;
}
.ocp-scanner-status { color: #fff; text-align: center; font-size: 0.9rem; margin: 0 0 0.5rem; }

/* Footer — same on every page, no shared _Layout to hold it centrally (see this
   file's header comment), so each page includes the _Footer partial directly. */
.ocp-footer {
  text-align: center;
  padding: 2rem 1rem 1rem;
  font-size: 0.78rem;
  color: var(--ocp-muted);
}
.ocp-footer a { color: var(--ocp-muted); text-decoration: none; }
.ocp-footer a:hover { text-decoration: underline; }
.ocp-footer-sep { margin: 0 0.5rem; }

/* Full-page loading overlay — shown the instant a branch card is tapped on Index, so
   there's immediate visual feedback instead of an unresponsive-feeling pause while
   the next page's server round trip completes (a real cross-page navigation, not an
   SPA transition, so this is deliberately simple: show, then get replaced wholesale
   when the new page arrives). */
.ocp-page-loading {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: var(--ocp-page);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity .15s ease;
}
.ocp-page-loading.ocp-show { opacity: 1; pointer-events: auto; }
.ocp-spinner {
  width: 34px;
  height: 34px;
  border: 3px solid var(--ocp-line);
  border-top-color: var(--ocp-brand);
  border-radius: 50%;
  animation: ocp-spin .7s linear infinite;
}
@keyframes ocp-spin { to { transform: rotate(360deg); } }
/* Centered inline-page loading spinner (Receive.cshtml's own loading state) —
   a plain class instead of an inline style="display:flex" on the element,
   specifically so the [hidden] attribute can actually hide it. An inline
   style always beats a stylesheet [hidden] rule regardless of specificity —
   same lesson already documented above for .ocp-spinner-inline. */
.ocp-inline-loading { display: flex; justify-content: center; padding: 3rem 0; }
.ocp-inline-loading[hidden] { display: none; }
/* Small inline variant — same ring/border-color/animation as the base 34px
   spinner above (used together, e.g. class="ocp-spinner ocp-spinner-inline"),
   just sized to sit next to a line of button text instead of centered alone in
   a full loading screen (Confirm's "Checking…"/"Still working…" state, Quick
   Search's "Sending…" state). Needs its own display:inline-block since the base
   class sets no display at all (fine as a block-level loading div elsewhere,
   not fine as a <span> next to inline text, which stays display:inline and
   ignores width/height without this). Setting display here means this needs
   its own [hidden] override too, same reasoning as .ocp-poscta-grid[hidden]
   above — an author display rule always beats the UA default. */
.ocp-spinner-inline {
  width: 15px;
  height: 15px;
  border-width: 2px;
  display: inline-block;
  vertical-align: -3px;
  margin-right: 0.4rem;
}
.ocp-spinner-inline[hidden] { display: none; }
/* Found by review, live-confirmed only via box-model reasoning (couldn't screenshot
   the auth-gated page): margin-right above was tuned against Pos.cshtml's Confirm
   button, a plain display:inline-block button with no flex gap of its own. Quick
   Search's two action buttons that reuse this same spinner (#ocp-quick-action-btn
   via .ocp-cartbar-btn's own display:inline-flex;gap:0.4rem, and
   #ocp-quick-panel-action-btn's own inline flex;gap:0.4rem) already space every
   child that far apart — the spinner's own margin then stacks on top of that gap,
   making spinner-to-label spacing roughly double the label-to-icon spacing right
   next to it. Zeroed here so flex gap alone governs the spacing in that context. */
.ocp-cartbar-btn .ocp-spinner-inline,
#ocp-quick-panel-action-btn .ocp-spinner-inline { margin-right: 0; }

/* Skeleton grid — shown in #ocp-grid-container WHILE a search/category fetch is in
   flight (menu.js's swapGrid), replacing the plain opacity-dim so a catalog swap
   reads as "loading new content" rather than "the page just went dim/stuck". */
.ocp-skel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
@container ocp-page (min-width: 480px) { .ocp-skel-grid { grid-template-columns: repeat(3, 1fr); gap: 1.15rem; } }
@container ocp-page (min-width: 760px) { .ocp-skel-grid { grid-template-columns: repeat(4, 1fr); } }
.ocp-skel-card { border-radius: var(--ocp-r); overflow: hidden; background: var(--ocp-surface); border: 1px solid var(--ocp-line); }
.ocp-skel-img { aspect-ratio: 1.1; background: var(--ocp-line); }
.ocp-skel-line { height: 12px; margin: 0.9rem 0.9rem 0; border-radius: 6px; background: var(--ocp-line); }
.ocp-skel-line.ocp-skel-price { width: 45%; margin-top: 0.6rem; margin-bottom: 0.9rem; }
.ocp-skel-img, .ocp-skel-line {
  animation: ocp-skel-pulse 1.1s ease-in-out infinite;
}
@keyframes ocp-skel-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

/* Share-link last resort — only shown when the Web Share API, async Clipboard API,
   AND execCommand('copy') all failed (e.g. testing over a plain-HTTP LAN address,
   where the first two don't even exist — both require a secure context). A visible,
   pre-selected URL beats the button silently doing nothing. */
.ocp-share-fallback {
  position: fixed;
  left: 1rem;
  right: 1rem;
  bottom: calc(var(--ocp-cartbar-h) + 1.1rem);
  z-index: 50;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  box-shadow: var(--ocp-shadow-float);
  padding: 0.75rem 0.9rem;
}
.ocp-share-fallback span { display: block; font-size: 0.78rem; color: var(--ocp-muted); margin-bottom: 0.4rem; }
.ocp-share-fallback input {
  width: 100%;
  font-size: 0.85rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  background: var(--ocp-page);
  color: var(--ocp-ink);
}

/* Restaurant Milestone 2, 2.1 — the floor board (Staff/Tables.cshtml). Cards reuse
   .ocp-page's own container-query context (same breakpoints as the product grid,
   .ocp-grid above) rather than a page-width media query, so the board never
   overlaps a side panel the way a viewport-keyed grid could on a wider screen. */
/* [hidden] loses to a same-specificity/later display: rule (LESSONS.md #2's bug
   class) — both view panels set their own `display`, so the [hidden] override has
   to be explicit here, not assumed from the UA stylesheet alone. */
.ocp-tables-grid[hidden], .ocp-tables-rows[hidden] { display: none; }
.ocp-tables-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.85rem;
}
@container ocp-page (min-width: 480px) { .ocp-tables-grid { grid-template-columns: repeat(3, 1fr); gap: 1rem; } }
@container ocp-page (min-width: 760px) { .ocp-tables-grid { grid-template-columns: repeat(4, 1fr); } }

/* Item 2 (2026-09-02) — "choose a size of the card which can fit the items and
   then fix the size": min-height + centered content so every card in the grid
   reads as the same size regardless of whether its own reservedFor text wraps
   to a second line, instead of each row's height following its tallest card. */
.ocp-table-card {
  position: relative;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r);
  padding: 0.9rem 0.8rem;
  min-height: 116px;
  /* Mobile-sizing bug fix (2026-09-04) — a grid item's default min-width:auto
     lets its content's min-content size (here, the nowrap status badge below)
     floor the whole COLUMN's track width even though the track is declared
     1fr, distorting every row that shares that column (verified live: one
     long status like "Bill printed · Awaiting payment" stretched its column
     to 238px while the sibling column was squeezed to 92px in a 375px-wide
     2-column grid). min-width:0 makes the card size purely from the grid
     track, matching every other 1fr-grid card in this file. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.35rem;
  cursor: pointer;
}
/* Owner follow-up (2026-09-02) — "just the split payment part can be beside
   the icon (do not move the icon just beside it)": the icon stays exactly
   where it always was (centered, own row); this wrapper only exists so the
   split-fraction badge (below) can sit directly next to it as a pair, without
   restructuring the rest of the card. With no split in progress this row is
   just the icon alone — visually identical to before. */
/* UI/UX review fix (2026-09-02) — flex-wrap:wrap added as a defensive fallback:
   a large-total split fraction ("1,240.00/3,600.00", realistic on a multi-item
   bill) plus the icon could exceed a narrow 2-column mobile card's width; with
   wrap allowed the badge drops to its own centered line instead of overflowing
   the card's border. No visible change for the common short-value case. */
.ocp-table-card-icon-row { display: flex; align-items: center; justify-content: center; gap: 0.35rem; flex-wrap: wrap; }
.ocp-table-card-icon { color: var(--ocp-ink-soft); }
.ocp-table-card-name { font-weight: 600; color: var(--ocp-ink); font-size: 0.88rem; }
/* Item 24 (2026-09-02) — "standardize table/takeaway status indicators on outline
   style": was a solid filled pill on table cards only (takeaway cards had no
   badge shape at all, just plain .ocp-sub text) — now one shared outline badge
   (border + text in the status color, transparent fill) for BOTH card types,
   every state, so a table and a takeaway read as the same kind of indicator. */
.ocp-table-card-status {
  display: inline-flex;
  align-items: center;
  font-size: 0.76rem;
  font-weight: 600;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  border: 1.5px solid;
  white-space: nowrap;
  /* Mobile-sizing bug fix (2026-09-04) — same last-resort ellipsis truncation
     .ocp-table-card-split already has just below, now also applied here: even
     with the grid-column distortion fixed above (min-width:0), a long status
     string in a genuinely narrow card (e.g. a 4-column desktop-width grid, or
     a very small phone) should truncate gracefully rather than spill past the
     card's own border. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Owner follow-up (2026-09-02) — the split-payment fraction ("[60.00/100.00]")
   used to be appended straight onto the main status badge's text
   (BuildStatusText), which is what actually made that badge overflow/wrap on
   a long split — moved server-side into its own small SplitFraction field
   (TableBoardRow/TakeawayBoardRow), rendered here as its own compact pill
   beside the icon instead. Smaller/quieter than the main status badge — it's
   supplementary detail, not the primary status. */
.ocp-table-card-split {
  font-size: 0.66rem;
  font-weight: 700;
  /* Owner follow-up (2026-09-02) — "make the color yellow as it was": matches
     the main status badge's own color, so the two badges read as belonging
     to the same status rather than one looking unrelated/washed out next to
     the other. Light-mode contrast fix (2026-09-05) then gave THIS RULE'S TWO
     Tables.cshtml call sites (the table + takeaway cards) an inline
     `style="background:...; color:...;"` carrying the same solid-accent-fill
     + paired contrast-text treatment as the main badge — inline always wins,
     so `background` below is dead for those two specifically. It is NOT dead
     everywhere: `stafftableorder.js`'s table-switcher sheet reuses this exact
     class with no inline style at all (correction, 2nd Opus review,
     2026-09-06 — a prior version of this comment wrongly called it dead
     unconditionally), so that context still renders the original quiet
     `var(--ocp-page)` fill with no accent color, matching the "quieter,
     supplementary" intent below as-is. Deliberate, flagged trade-off on the
     two Tables.cshtml sites only: they now read as visually LOUD as the main
     badge, not quieter — kept this way rather than risk a second contrast bug
     reintroducing the light-mode-unreadable issue with a lower-effort fix,
     and left inconsistent with the switch-sheet's own still-quiet rendering
     of the "same" pill; revisit if the
     owner wants the quieter look back specifically. */
  background: var(--ocp-page);
  border-radius: 999px;
  padding: 0.1rem 0.4rem;
  white-space: nowrap;
  /* UI/UX review fix (2026-09-02) — last-resort truncation if even a wrapped
     own-line badge (see .ocp-table-card-icon-row) is still too wide for a very
     narrow card; ellipsis reads as "there's more" rather than spilling text
     past the card border. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ocp-table-card-status.ocp-status-free { border-color: var(--ocp-ok); color: var(--ocp-ok); }
/* Kitchen monitor (pending-items #11) — a small, quiet, SEPARATE pill from
   .ocp-table-card-status above (never blended into it — that badge's own
   KitchenPrinted/PrePrinted color/icon rule is unrelated and unchanged). Sits
   directly below the main status badge on cards, inline after it on rows. */
.ocp-kitchen-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--ocp-ink-soft);
  background: var(--ocp-page);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  margin-top: 0.3rem;
  white-space: nowrap;
  width: fit-content;
}
/* Item 2 — "you don't need 1 line for it... rather change the card background
   color and then add the alarm on the table description (save space of the
   card)": the reminder no longer adds its own pill/row (which made a
   reminder-due card taller than its neighbors); it now tints the whole
   card/row background and prepends a small bell inline in the existing name
   text (stafftableorder.js's own updateReminders toggles both together). */
.ocp-table-card.ocp-reminder-due, .ocp-table-row.ocp-reminder-due {
  background: color-mix(in srgb, #001f5c 14%, var(--ocp-surface));
  border-color: #001f5c;
}
/* Adversarial review fix (2026-09-02) — this rule shares its plain-class
   specificity with .ocp-table-card:active/:hover (added earlier in this file),
   so being LATER in source order silently cancelled all hover/press feedback
   on a reminder-due card/row — it read as completely inert when tapped, the
   exact "does tapping something give visible feedback" check the Baseline
   checklist exists for. Explicit combined selectors here always win regardless
   of source order, deepening the tint on press/hover rather than replacing it,
   so the urgent color stays legible throughout. */
.ocp-table-card.ocp-reminder-due:active, .ocp-table-row.ocp-reminder-due:active {
  background: color-mix(in srgb, #001f5c 24%, var(--ocp-surface));
}
@media (hover: hover) and (pointer: fine) {
  .ocp-table-card.ocp-reminder-due:hover, .ocp-table-row.ocp-reminder-due:hover {
    background: color-mix(in srgb, #001f5c 24%, var(--ocp-surface));
  }
}
.ocp-reminder-icon { margin-right: 0.3rem; }
.ocp-reminder-icon[hidden] { display: none; }

.ocp-tables-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.ocp-table-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-left: 4px solid var(--ocp-line);
  border-radius: var(--ocp-r-sm);
  padding: 0.65rem 0.85rem;
  cursor: pointer;
}
.ocp-table-row-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.ocp-table-row-name { font-weight: 600; color: var(--ocp-ink); font-size: 0.88rem; }

/* Items 3/6 (2026-09-02) — quick-action rail, porting desktop's own RestaurantTables.cs
   sidebar (buttonPrintToKitchen_Click/buttonPrintBill_Click, a "loaded tool, click
   target" interaction). Collapsed by default on mobile (a floating toggle expands it
   as an overlay panel); a persistent thin rail on tablet+ (700px, an existing
   breakpoint already used elsewhere in this file) per the owner's own framing. */
.ocp-quick-rail-toggle {
  position: fixed;
  left: 1.1rem;
  bottom: 1.1rem;
  z-index: 25;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: none;
  background: var(--ocp-ink);
  color: var(--ocp-page);
  box-shadow: var(--ocp-shadow-float);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.ocp-quick-rail-toggle:active { transform: scale(0.92); }
.ocp-quick-rail-toggle.ocp-active { background: var(--ocp-brand); color: var(--ocp-brand-ink); }
@media (hover: hover) and (pointer: fine) {
  .ocp-quick-rail-toggle:hover { background: color-mix(in srgb, var(--ocp-ink) 85%, var(--ocp-page) 15%); }
}
.ocp-quick-rail {
  position: fixed;
  left: 1.1rem;
  bottom: 4.3rem;
  z-index: 24;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  background: var(--ocp-surface);
  border: 1px solid var(--ocp-line);
  border-radius: var(--ocp-r);
  padding: 0.6rem;
  box-shadow: var(--ocp-shadow-float);
}
.ocp-quick-rail[hidden] { display: none; }
.ocp-quick-rail-title { font-size: 0.72rem; font-weight: 700; color: var(--ocp-muted); text-transform: uppercase; letter-spacing: 0.04em; padding: 0 0.3rem; }
.ocp-quick-rail-btn {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: var(--ocp-tap);
  padding: 0.5rem 0.7rem;
  background: transparent;
  border: none;
  border-radius: var(--ocp-r-sm);
  color: var(--ocp-ink);
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  white-space: nowrap;
}
.ocp-quick-rail-btn.ocp-active { background: var(--ocp-brand); color: var(--ocp-brand-ink); }
.ocp-quick-rail-btn:not(.ocp-active):active { background: var(--ocp-page); }
@media (hover: hover) and (pointer: fine) {
  .ocp-quick-rail-btn:not(.ocp-active):hover { background: var(--ocp-page); }
}
/* Tablet+ — always-visible thin rail (no toggle needed), icons only, no title/labels
   (the "thin line" the owner asked for), positioned so it never overlaps the board's
   own content (app.css's .ocp-page/.ocp-layout already reserve their own side gutters
   at this width, same reasoning as .ocp-pos-bar-page's own comment on reusing real
   layout math instead of a guessed offset). */
@media (min-width: 700px) {
  .ocp-quick-rail-toggle { display: none; }
  .ocp-quick-rail {
    bottom: auto;
    top: 50%;
    transform: translateY(-50%);
    left: 0.5rem;
  }
  .ocp-quick-rail[hidden] { display: flex; }
  .ocp-quick-rail-title { display: none; }
  .ocp-quick-rail-btn span { display: none; }
  .ocp-quick-rail-btn { padding: 0.7rem; }
}
.ocp-quick-tool-banner {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--ocp-brand);
  color: var(--ocp-brand-ink);
  border-radius: var(--ocp-r-pill);
  padding: 0.55rem 0.6rem 0.55rem 1rem;
  font-size: 0.85rem;
  font-weight: 600;
  box-shadow: var(--ocp-shadow-float);
  max-width: calc(100vw - 1.5rem);
}
.ocp-quick-tool-banner[hidden] { display: none; }
.ocp-quick-tool-banner button {
  background: color-mix(in srgb, var(--ocp-brand-ink) 15%, transparent);
  color: inherit;
  border: none;
  border-radius: var(--ocp-r-pill);
  padding: 0.35rem 0.75rem;
  font-weight: 700;
  font-size: 0.8rem;
  cursor: pointer;
  flex: 0 0 auto;
}
/* The touch-first stand-in for desktop's cursor-icon swap (RestaurantTables.cs sets
   this.Cursor to the armed tool's own icon) — a gentle pulse on every real target so
   it's visually obvious the board is now in "tap a table to fire" mode, not its
   normal navigate-on-tap mode. */
body.ocp-tool-armed .ocp-table-card,
body.ocp-tool-armed .ocp-table-row {
  animation: ocp-quick-tool-pulse 1.6s ease-in-out infinite;
}
@keyframes ocp-quick-tool-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ocp-brand) 45%, transparent); }
  50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--ocp-brand) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  body.ocp-tool-armed .ocp-table-card, body.ocp-tool-armed .ocp-table-row { animation: none; }
}

/* Mobile-sizing bug fix (2026-09-04) — reported live: focus a field on
   TableOrder.cshtml (a qty/price/discount/note input), then navigate back to
   Tables.cshtml — the page comes back zoomed in and needs a manual pinch-out.
   Root cause confirmed: iOS Safari (and several other mobile browsers)
   automatically zooms the whole viewport in on focus whenever the focused
   input/textarea/select's computed font-size is under 16px, to keep the text
   legible while editing — and a browser-back navigation restores the exact
   painted state from bfcache, zoom level included, unlike a fresh page load
   which always starts unzoomed. Every text-entry control in this stylesheet
   was sized well under that threshold (13.1-15.2px — .ocp-note-input/
   .ocp-price-input/.ocp-discount-input at 0.82rem, .ocp-field input/.ocp-
   stepper input at 0.95rem, .ocp-share-fallback input at 0.85rem, etc. —
   confirmed via a live sweep, not a guess). Rather than hunt down and
   individually raise each of the dozen-plus separate input-related rules
   scattered across this file (real risk of missing one, and every one added
   later would need the same fix repeated), one rule at the true point of
   truth: any real text-entry control renders at least 16px on any viewport
   narrow enough to plausibly be a touchscreen phone, full stop. !important is
   deliberate here — this is a single, cross-cutting accessibility/UX
   invariant that must win over every existing more-specific component rule
   (and every future one), not a normal component style fighting for the
   cascade. Verified live: .ocp-discount-input (its narrowest real usage, 4rem
   max-width) and .ocp-price-input still fit realistic content ("100" /
   "1234.56") with no self-overflow at 16px. */
@media (max-width: 768px) {
  input, select, textarea { font-size: 16px !important; }
}

