/* CVS Tracker capture PWA — mobile-first, large touch targets, light/dark.
   No build step; plain CSS with custom properties. */
:root {
  /* --- legacy aliases (kept so existing screens render unchanged) --- */
  --accent: #cc0000;
  --accent-ink: #ffffff;
  /* 0H badge fill: white count text needs ≥4.5:1 (WCAG 1.4.3), so the badge keeps the
     DARK red in both themes (the dark-mode --accent #ef4444 only clears ~3.8:1 with white).
     Deliberately NOT overridden in the dark block — white on #cc0000 = 5.9:1 either way. */
  --badge-bg: #cc0000;
  --bg: #f6f7f8;
  --card: #ffffff;
  --ink: #16191c;
  --muted: #5b6470;
  --line: #d8dde2;
  /* 0F (WCAG 1.4.11): --line is a hairline for dividers; it is BELOW the 3:1 non-text
     contrast a form control's boundary needs (#d8dde2 on #f6f7f8 ≈ 1.27:1). This token is
     the affordance border for interactive controls (inputs, select, .step, chips, tabs):
     ≥3:1 against the page/card/input fills in BOTH themes (light #7e8893 → 3.1–3.6:1). */
  --control-border: #7e8893;
  --err: #b3261e;
  --ok: #1a7f37;
  --warn: #9a6700;
  --radius: 0.8rem;
  font-size: 16px;

  /* === Design system (U-0) — the single source of truth the redesign builds on.
     Color: page / card / inset surfaces, ink, hairline, CVS red, status ramp.
     New tokens alias the legacy ones above for now so nothing shifts visually;
     screens migrate onto them in U-1+. */
  --surface: var(--card);
  --surface-2: #eceff2;            /* inset (e.g. inputs, wells) */
  --ink-muted: var(--muted);
  --accent-weak: color-mix(in srgb, var(--accent) 12%, var(--surface)); /* selected/hover red tint */
  /* ONE semantic status ramp (replaces the drifted .s-* / .sev-* pill systems) */
  --info: #1f6feb;
  --danger: var(--err);
  --neutral: var(--ink-muted);

  /* Typography — one size per semantic rank */
  --fs-xs: 0.75rem; --fs-sm: 0.875rem; --fs-md: 1rem;
  --fs-lg: 1.125rem; --fs-xl: 1.375rem; --fs-2xl: 1.75rem;
  --lh-tight: 1.2; --lh: 1.5;

  /* Spacing & shape */
  --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
  --space-4: 1rem; --space-5: 1.5rem; --space-6: 2rem;
  --card-pad: var(--space-4);      /* every card uses this (kills "every card is slightly off") */
  --radius-sm: 0.5rem;             /* --radius (0.8rem) stays the default card radius */
  --radius-pill: 999px;
  --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); /* one subtle elevation */
  --shadow-2: 0 -2px 10px rgba(0, 0, 0, 0.10), 0 1px 3px rgba(0, 0, 0, 0.06); /* a lift for pinned/overlay surfaces (visitDash, modals) */
  /* Map pin/dot/legend colors. Tuned for the OSM map tiles, which render LIGHT in both themes —
     so these are deliberately theme-INDEPENDENT (NOT overridden in the dark block below): a pin
     always sits on a light map. Centralized so the pins, the list dots, and the map legend share
     one source of truth (was duplicated hex across the .mapPin-- and .mapDot-- modifiers). */
  --pin-fresh: #cc0000;   /* expired in the last 7 days */
  --pin-recent: #e08600;  /* expired 8–30 days ago */
  --pin-new: #1f6feb;     /* never scouted */
  --pin-clear: #9aa4af;   /* recently visited, nothing expired */
  /* Named breakpoints (documentary — CSS can't read vars in @media; mirror these
     in the media queries below): --bp-sm 22rem, --bp-md 26rem. */
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171a; --card: #1d2125; --ink: #e7ebee; --muted: #9aa4af;
    --line: #333a41; --accent: #ef4444;
    --control-border: #717c86; /* 0F: ≥3:1 control boundary on dark surfaces (3.35–4.22:1) */
    /* design-system tokens that don't auto-track via var() in dark */
    --surface-2: #262b30;
    --info: #4493f8;
    /* Brighten the status hues for dark so green/amber TEXT (visit coupon counts, the
       "visited Nd ago" note, the H1 dashboard tiles) clears 4.5:1 on dark surfaces —
       the light #1a7f37/#9a6700 dropped to ~2.8:1 against the dark cards. */
    --ok: #3fb950;
    --warn: #d29922;
    --shadow-1: 0 2px 6px rgba(0, 0, 0, 0.4);
    --shadow-2: 0 -2px 12px rgba(0, 0, 0, 0.5), 0 1px 4px rgba(0, 0, 0, 0.4);
  }
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.5;
  /* reserve room at the bottom for the fixed bottom nav (harmless extra space
     on the signed-out screen, which has no nav) */
  padding: 0 1rem calc(env(safe-area-inset-bottom) + 4.5rem) 1rem;
  overflow-x: clip; /* a stray-wide element must never zoom the whole page out */
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
[hidden] { display: none !important; }

.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.4rem 0.1rem; position: sticky; top: 0; background: var(--bg); z-index: 5;
  padding-top: calc(env(safe-area-inset-top) + 0.35rem);
}
.brand { font-weight: 700; font-size: 1.05rem; color: var(--accent); flex: none; }
/* nav + identity; wraps to a second line instead of forcing the page wider than the screen */
.user-chip { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 0.3rem 0.4rem; font-size: 0.8rem; min-width: 0; }
.user-chip #who { display: none; max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (min-width: 26rem) { .user-chip #who { display: inline; } }
.avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; }
/* 0Q: right-side topbar group (sync chip + identity), so the chip rides with the user
   chip on the right and the brand stays pinned left (topbar is justify-content:space-between). */
.topbar__right { display: inline-flex; align-items: center; justify-content: flex-end; gap: 0.5rem; min-width: 0; flex-wrap: wrap; }
/* 0Q: the global sync status pill — compact, persists on every route. data-state tints it
   info while syncing, warn when re-auth is needed, ok for the brief "All sent ✓". */
.sync-chip {
  display: inline-flex; align-items: center; flex: none; font-size: 0.72rem; font-weight: 700;
  padding: 0.2rem 0.55rem; border-radius: var(--radius-pill); border: 1px solid var(--control-border);
  background: var(--surface); color: var(--ink-muted);
  white-space: nowrap; max-width: 52vw; overflow: hidden; text-overflow: ellipsis;
}
/* State is carried by the border + background tint (the .pill--* convention); the TEXT stays
   --ink/--ink-muted so it clears 4.5:1 on the light tints (colored small text did not). */
.sync-chip[data-state="sync"] { border-color: color-mix(in srgb, var(--info) 55%, var(--surface)); background: color-mix(in srgb, var(--info) 14%, var(--surface)); }
.sync-chip[data-state="auth"] { border-color: color-mix(in srgb, var(--warn) 60%, var(--surface)); background: color-mix(in srgb, var(--warn) 18%, var(--surface)); color: var(--ink); font-weight: 800; }
.sync-chip[data-state="done"] { border-color: color-mix(in srgb, var(--ok) 55%, var(--surface)); background: color-mix(in srgb, var(--ok) 16%, var(--surface)); }

main { max-width: 30rem; margin: 0 auto; }

.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem; }
.center { text-align: center; }
.lead { font-size: 1rem; margin: 0 0 0.75rem; }
/* in-card intro/sub note (replaces ad-hoc inline margins on store/dashboard card paragraphs) */
.card__note { margin: 0 0 var(--space-2); }
.card__note--tight { margin: 0; }
.msg { font-weight: 600; min-height: 1.5em; }
.msg.err, .formMsg.err { color: var(--err); }
.formMsg.ok { color: var(--ok); }   /* lifecycle feedback: "Logged — picked up ✓" */
.muted { color: var(--muted); font-weight: 400; font-size: 0.9em; }
.gbtn { display: flex; justify-content: center; margin-top: 1rem; min-height: 44px; }

/* sign-in hero (K2): logo + tagline + the three things the app does + the Google
   button. Centered, comfortable, branded — replaces the bare card. */
.signin { max-width: 22rem; margin: 0 auto; padding: var(--space-6) 0 var(--space-4); display: grid; gap: var(--space-5); text-align: center; }
.signin__hero { display: grid; gap: var(--space-3); justify-items: center; }
.signin__logo { width: 76px; height: 76px; border-radius: 18px; box-shadow: var(--shadow-1); }
.signin__title { margin: 0; font-size: var(--fs-2xl); font-weight: 800; color: var(--accent); line-height: var(--lh-tight); }
.signin__tag { margin: 0; font-size: var(--fs-md); color: var(--ink); line-height: var(--lh); }
.signin__points { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); text-align: left; }
.signin__points li {
  display: flex; align-items: center; gap: var(--space-3); font-size: var(--fs-sm); color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3);
}
.signin__ico { flex: none; font-size: 1.3rem; line-height: 1; }
.signin__cta { display: grid; gap: var(--space-2); justify-items: center; }
.signin__cta .gbtn { margin-top: 0; }
.signin__cta .msg { min-height: 1.2em; font-size: var(--fs-sm); }
/* small build-version line at the foot of the sign-in hero */
.signin__ver { margin: var(--space-2) 0 0; font-size: var(--fs-xs, .75rem); color: var(--ink-muted); letter-spacing: .02em; }

.banner {
  background: color-mix(in srgb, var(--warn) 16%, var(--card));
  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));
  color: var(--ink); border-radius: var(--radius); padding: 0.7rem 0.9rem;
  margin: 0.75rem 0; font-weight: 600; font-size: 0.95rem;
}

/* tightened gap so the whole capture form fits one screen (no scroll) */
form { display: grid; gap: 0.5rem; }
/* a fieldset/group box — the bordered section the capture form is built from
   (was .field in v1; renamed in U-2 so .field is the canonical labeled field). */
.fieldgroup {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card);
  margin: 0; padding: 0.55rem 0.7rem;
}
.fieldgroup > legend { font-size: var(--fs-sm); }
/* canonical labeled field (U-2): one label-over-control unit, reused everywhere a
   single input is labeled — replaces labeled()/.fixField + Review's bare inputs. */
.field { display: grid; gap: var(--space-1); margin: 0; font-weight: 600; font-size: var(--fs-sm); }
.field > input, .field > textarea, .field > select { font-weight: 400; font-size: var(--fs-md); }
legend { font-weight: 700; padding: 0 0.4rem; }
.req { color: var(--err); }

label { display: block; font-weight: 600; margin: 0.5rem 0 0.3rem; }
input[type="text"], input[type="date"], input[type="email"], input[type="number"], textarea {
  width: 100%; font: inherit; padding: 0.7rem; border: 1px solid var(--control-border);
  border-radius: 0.5rem; background: var(--bg); color: var(--ink);
}
input:focus-visible, textarea:focus-visible, button:focus-visible, summary:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--accent) 85%, transparent); outline-offset: 2px;
}
textarea { resize: vertical; }

.big-btn {
  display: block; width: 100%; min-height: 48px; font: inherit; font-weight: 700;
  font-size: 1rem; padding: 0.7rem 1rem; border-radius: var(--radius); cursor: pointer;
  background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent);
}
.big-btn.ghost { background: transparent; color: var(--accent); }
.big-btn.danger { background: transparent; color: var(--err); border-color: color-mix(in srgb, var(--err) 55%, var(--line)); }
.big-btn:disabled { opacity: 0.6; cursor: progress; }

.link-btn {
  font: inherit; background: none; border: none; color: var(--accent);
  text-decoration: underline; cursor: pointer; padding: 0.5rem; min-height: 44px;
}

/* canonical image component (U-2): a captured/preview photo, centered, capped. */
.thumb { margin: 0.75rem 0 0; text-align: center; }
.thumb img { max-width: 100%; max-height: 220px; border-radius: 0.5rem; border: 1px solid var(--line); }

.stepper { display: flex; align-items: center; justify-content: center; gap: 1rem; }
/* "How many?" + "More details" on one row: the stepper hugs the left (reclaiming the
   space the centered stepper wasted), the More toggle sits at the right, and its fields
   drop in below when opened. Wraps gracefully on a very narrow phone. */
.qtyRow { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-3); flex-wrap: wrap; }
.qtyRow .stepper { justify-content: flex-start; gap: var(--space-3); }
.moreToggle { flex: none; }
.moreWrap { margin-top: var(--space-2); }
.moreWrap label:first-child { margin-top: 0; }
/* the +/- are secondary controls, not the primary action — neutral, so red stays
   reserved for Scan / Log / links (kills the "everything is red" overload). */
.step {
  width: 48px; height: 48px; font-size: 1.5rem; line-height: 1; border-radius: 50%;
  border: 1px solid var(--control-border); background: var(--surface); color: var(--ink); cursor: pointer;
  /* manipulation = pan + pinch-zoom stay, but the DOUBLE-TAP-to-zoom gesture is off,
     so rapidly tapping +/- to pile on quantity never zooms the page (iOS Safari). */
  touch-action: manipulation;
}
.step:active { background: var(--surface-2); }
/* the count is a real input so you can tap it and type (e.g. 30) instead of tapping + */
#qty {
  width: 3.6rem; font-size: 1.4rem; font-weight: 700; text-align: center;
  border: 1px solid var(--line); border-radius: 0.5rem; background: var(--bg); color: var(--ink);
  padding: 0.3rem 0; -moz-appearance: textfield; touch-action: manipulation;
}
#qty::-webkit-outer-spin-button, #qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* margin:0 — the form's grid gap already spaces these; the default <p> margins
   were stacking on top and pushing Submit below the fold on first open. */
.locline { margin: 0; font-size: 0.9rem; color: var(--muted); display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
.locline .link-btn { min-height: 0; padding: 0.2rem 0.4rem; }
.formMsg { margin: 0; min-height: 1.25em; font-weight: 600; }
/* when empty it's not a flex item, so the form's gap doesn't reserve a band between the
   last field and Submit — keeps spacing even + waste minimal. min-height still steadies
   it once a message (validation / "Logged ✓") is showing. */
.formMsg:empty { display: none; }

/* in-store close-out ("Done here", OD-1) — set apart from Submit by a divider so
   it never competes with the primary action. Compact: a quiet neutral button with
   its explainer INLINE to the right (P1.1/L4) so the close-out is ~one button tall
   instead of claiming a centered vertical band. */
.doneHere { margin: 0; padding-top: var(--space-2); border-top: 1px dashed var(--line); display: grid; gap: var(--space-1); }   /* #app gap spaces it; the dashed top still sets the close-out apart from Submit */
.doneHere__row { display: flex; align-items: center; gap: var(--space-3); }
.doneHere__btn { flex: none; }   /* sized to its label so the hint can sit beside it */
.doneHereHint { margin: 0; flex: 1; min-width: 0; font-size: 0.78rem; color: var(--muted); text-align: left; line-height: var(--lh-tight); }
.doneHereMsg { margin: 0; min-height: 1.2em; font-weight: 600; font-size: 0.85rem; text-align: left; }
.doneHereMsg.ok { color: var(--ok); }
.doneHereMsg.err { color: var(--err); }

/* Log/capture screen fills the viewport between the sticky topbar and the fixed bottom
   nav, so the PAGE itself never scrolls — only the Recent list does (Mike: "the log page
   shouldn't scroll, only the recent section"). The form/dashboard/close-out are fixed;
   Recent takes the leftover space and scrolls internally. The chrome subtracted ≈ topbar
   (~2.4rem) + nav reserve (4.5rem) + the safe-area insets. */
#app {
  display: flex; flex-direction: column;
  /* ONE uniform gap between every visible container (Recent, the form, close-out, the
     visit banner) — the children carry no ad-hoc top/bottom margins, so spacing reads
     even and, because hidden/sr-only children aren't flex items, it stays even whether
     the visit banner is showing or not (Mike 2026-06-19: "make everything even… the
     banner should go at the bottom"). */
  gap: var(--space-2);
  height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 7rem);
  /* normally everything fits and only #lastScan scrolls; this is the safety valve for a
     tall state (e.g. the post-scan match card) — #app scrolls instead of clipping. */
  overflow-y: auto; overflow-x: hidden;
}
/* the capture form is itself a column with the SAME gap, so its fieldsets line up on the
   one rhythm as the top-level containers — no merged/flush boxes, no double spacing. */
#app > #captureForm { display: flex; flex-direction: column; gap: var(--space-2); }
#app > #captureForm, #app > .visitDash, #app > .doneHere, #app > .banner { flex: none; }
/* Recent absorbs the leftover and SHRINKS into it (flex-shrink + min-height:0), so the
   form stays put and only this list scrolls; a ~1-row floor keeps it from vanishing. */
#lastScan { flex: 1 1 auto; min-height: 3.5rem; display: flex; flex-direction: column; }
#lastScan .lastScan__list { flex: 1 1 auto; min-height: 0; max-height: none; }

/* "Recent" panel at the TOP of Log (moved 2026-06-19): a SCROLL LIST of the newest captures
   that fills the space above the form (flex:1) and scrolls internally, with an "All recent ›"
   tap-through to the full feed. Each row: tiny photo + name + status pill. The full Recent feed
   rides the design-system .rows/.row list. */
.lastScan {
  margin: 0; padding: var(--space-2) var(--space-3);   /* #app's gap spaces it; no ad-hoc margin */
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.lastScan__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-1); }
.lastScan__title { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--ink-muted); }
.lastScan__all { flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0.2rem 0.4rem; white-space: nowrap; font-weight: 600; }
/* P1.1 (L3): Recent fills the leftover space at the bottom of Log with LEGIBLE
   thumbnails instead of a cramped fixed 5rem strip. A viewport-relative cap lets it
   use the available screen height yet still scroll past it; a peek of the next row
   still signals there's more below. */
.lastScan__list { display: grid; gap: var(--space-2); max-height: min(20rem, 42vh); overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.lastScan__row {
  display: flex; align-items: center; gap: var(--space-2); min-height: 2.9rem;
  /* min-width:0 is essential: the row is a GRID item (the list is display:grid) and a
     grid item defaults to min-width:auto (min-content), which would let a long name
     expand the row past the card; with 0 the name's own min-width:0 + ellipsis engage. */
  min-width: 0;
  padding-left: var(--space-1); border-left: 4px solid transparent; /* room for the lifecycle accent */
}
.lastScan__row.row--cpn { border-left-color: var(--ok); }
.lastScan__row.row--soon { border-left-color: var(--warn); }
.lastScan__row img, .lastScan__row .reviewPhoto {
  width: 44px; height: 44px; min-height: 0; max-height: none; flex: none;
  object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--line);
}
/* the no-photo fallback must stay a clean square tile, never an overflowing text box */
.lastScan__row .reviewPhoto.noPhoto { font-size: 0.6rem; font-weight: 600; line-height: 1.05; text-align: center; padding: 2px; overflow: hidden; }
.lastScan__name { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lastScan__name .lbName, .lastScan__name .linkName {
  max-width: 100%; min-height: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
}
/* Recent items are the canonical .row now (U-2); the v1 .recentItem/.rname/.s-*
   pill skins were removed with that migration. */
/* High-contrast text (--ink) on a hue-tinted chip — the hue + the word carry
   the status, so contrast stays well above 4.5:1 in both themes. The .pill--*
   severity modifiers (design-system block below) tint this base. */
.pill {
  font-size: 0.78rem; font-weight: 700; padding: 0.2rem 0.55rem; border-radius: 1rem;
  white-space: nowrap; color: var(--ink); border: 1px solid transparent; flex: none;
}

/* barcode: three peer ways in — Scan · Photo · Type — side by side. Only Scan is the
   red primary; Photo/Type are neutral so red stays meaningful (= the default action). */
.barcodeBtns { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); }
.barcodeBtns > .btn { min-width: 0; padding-left: 0.3rem; padding-right: 0.3rem; white-space: nowrap; }
@media (max-width: 22rem) { .barcodeBtns > .btn { font-size: var(--fs-sm); } }
/* the typed-barcode input the "Type" button toggles open */
.manualWrap { margin-top: var(--space-2); }

/* expiration date: the date input (left) + a "snap a photo" button (right), one row */
.dateRow { display: flex; gap: var(--space-2); align-items: center; }
/* P1.3: the −/+ steppers MUST stay a 48px circle — never let the (greedy, hard-to-shrink native)
   date field squish them. Belt-and-suspenders so no flex quirk can deform them: a fixed 48px basis
   that can't grow OR shrink, an explicit min/width/height, and aspect-ratio to force square. */
.dateRow .step {
  flex: 0 0 48px;
  width: 48px; min-width: 48px; height: 48px; aspect-ratio: 1 / 1;
  border-radius: 50%; padding: 0;
}
/* The date field takes the rest and is allowed to shrink to nothing before the buttons ever do. */
.dateRow > input[type="date"] { flex: 1 1 auto; min-width: 0; }
.dateRow__photo { flex: none; white-space: nowrap; }

.scanned { margin: 0.7rem 0 0; font-weight: 600; color: var(--ok); }
.scanned strong { font-variant-numeric: tabular-nums; }

/* inline live scanner — a small camera box inside the barcode field, not a screen takeover */
.scanner {
  position: relative; margin-top: 0.6rem; width: 100%; aspect-ratio: 4 / 3; max-height: 240px;
  background: #000; border-radius: var(--radius); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
#scanVideo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.scanFrame {
  position: relative; z-index: 1; width: 78%; max-width: 260px; aspect-ratio: 5 / 3;
  border: 3px solid #fff; border-radius: 12px; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.5);
}
.scanHint {
  position: absolute; z-index: 1; left: 0; right: 0; bottom: 0.4rem; margin: 0; padding: 0 0.6rem;
  color: #fff; text-align: center; font-size: 0.8rem; font-weight: 600; text-shadow: 0 1px 4px #000;
}
/* Overlay scanner chips (Cancel + Torch): both keep the ≥44px tap target (review 0a),
   centered so the compact text doesn't make the hit area smaller than it looks. */
.scanCancel {
  position: absolute; z-index: 2; top: 0.4rem; right: 0.4rem; min-height: 44px;
  display: inline-flex; align-items: center;
  font: inherit; font-size: 0.85rem; font-weight: 700; cursor: pointer;
  padding: 0.3rem 0.6rem; text-decoration: none; color: #fff;
  background: rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 0.5rem;
}
/* 0A2: flashlight toggle — mirrors .scanCancel at top-LEFT so it can't overlap Cancel. */
.scanTorch {
  position: absolute; z-index: 2; top: 0.4rem; left: 0.4rem; min-height: 44px;
  display: inline-flex; align-items: center;
  font: inherit; font-size: 0.85rem; font-weight: 700; cursor: pointer;
  padding: 0.3rem 0.6rem; color: #fff;
  background: rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 0.5rem;
}
.scanTorch[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); }

/* 0G: live "expires in N days" echo under the date field; greens when it's already a coupon. */
.dateEcho { margin: var(--space-1) 0 0; font-size: var(--fs-sm); font-weight: 600; color: var(--ink-muted); }
.dateEcho--cpn { color: var(--ok); }

/* 0B: "Updated 3:45 PM" snapshot caption on the Insights decision surfaces. */
.freshStamp { margin: 0 0 var(--space-2); font-size: var(--fs-xs); color: var(--ink-muted); text-align: right; }
/* Insights freshness now sits in the header next to Refresh (was a full body row above the content). */
.insightsFresh { margin-left: auto; font-size: var(--fs-xs); color: var(--ink-muted); white-space: nowrap; }
/* An EMPTY Insights status line must not reserve a 1.5em + margin gap above the content (the wasted space
   under the tabs). It collapses when empty, expands only when a real message is shown. */
#insightsMsg { margin: 0; }
#insightsMsg:empty { min-height: 0; }

/* 0A: feedback-preference rows in More → Preferences (label-wrapped native checkbox; ≥44px). */
.moreSettings { margin-top: var(--space-4); display: grid; gap: var(--space-2); }
.settingRow {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  min-height: 44px; padding: var(--space-2) var(--card-pad);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;
}
.settingRow > span { display: flex; flex-direction: column; gap: 2px; font-weight: 600; min-width: 0; }
.settingRow small { font-weight: 400; }
.settingChk { width: 22px; height: 22px; flex: none; accent-color: var(--accent); cursor: pointer; }

/* admin: photo review (name products from their shelf photos) */
.reviewView { margin: 1rem 0; }
/* admin headers now use the canonical .view-head pattern (P2.1a) */
.reviewList { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }
.reviewPhoto {
  width: 100%; min-height: 120px; max-height: 320px; object-fit: contain; background: var(--bg);
  border: 1px solid var(--line); border-radius: 0.5rem; display: block;
}
.reviewPhoto.noPhoto {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-weight: 600; max-height: none;
}
.reviewBody { display: grid; gap: var(--space-3); }
.reviewMeta { margin: 0; font-size: 0.85rem; color: var(--muted); word-break: break-word; font-variant-numeric: tabular-nums; }
.reviewActions { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.reviewActions > .big-btn { min-width: 0; }
.reviewCardMsg { min-height: 1.2em; font-weight: 600; font-size: 0.9rem; color: var(--ok); }
@media (max-width: 22rem) { .reviewActions { grid-template-columns: 1fr; } }

/* E3: capture-time product match card (admin) — a compact catalog card under the
   barcode field, photo beside name/brand, so a scan can be confirmed before logging. */
.matchCard {
  margin-top: var(--space-3);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--space-3); display: grid; gap: var(--space-2);
}
.matchHead { display: flex; align-items: center; gap: var(--space-2); }
.matchBadge {
  font-size: var(--fs-sm); font-weight: 700; color: var(--ink-muted);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-pill); padding: 0.15rem 0.6rem;
}
.matchBadge.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, var(--line)); }
.matchNew { margin: 0; color: var(--ink-muted); font-size: var(--fs-sm); }
.matchFig { margin: 0; }
.matchFig[hidden] { display: none; }
.matchPhoto {
  width: 120px; height: 120px; object-fit: contain; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius-sm); display: block;
}
.matchBody { display: grid; gap: var(--space-2); }
.matchReplace { flex: none; }
.matchName, .matchBrand {
  width: 100%; box-sizing: border-box; min-width: 0; min-height: 44px;
  padding: 0.5rem 0.6rem; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--card); color: var(--ink); font: inherit;
}
.matchActions { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
.matchSave { flex: none; }
.matchMsg { min-height: 1.2em; font-weight: 600; font-size: var(--fs-sm); color: var(--ok); min-width: 0; }
/* P3.10: read-only "this is what you scanned" line for a member who can't verify it yet */
.matchNameRO { margin: 0; font-weight: 600; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
/* P3.14: the per-member role picker on the Members screen — small, fits the row trail */
.roleSel { min-height: 44px; padding: 0.4rem 0.5rem; border: 1px solid var(--control-border); border-radius: var(--radius-sm); background: var(--card); color: var(--ink); font: inherit; } /* role-select-fix: 44px tap target */
.matchMsg:empty { min-height: 0; }   /* no reserved blank line until there's a message */

/* P1.1 (L1): post-scan confirmation — the captured shelf photo (left) beside the
   catalog match card (right), so it reads as one compact block instead of three
   stacked full-width sections and "Log this item" stays above the fold. The two
   columns engage only when there's BOTH a captured photo AND a match card to show:
   a typed barcode (card only) or a non-admin scan (photo only) collapses to one
   column. Falls back to a clean vertical stack where :has() is unsupported. */
.scanResult { display: grid; gap: var(--space-2); align-items: start; margin-top: var(--space-2); }
.scanResult__shot { margin: 0; min-width: 0; }
/* a long UPC/GTIN is one unbreakable token — let it wrap inside the narrow shot column
   (matching the .reviewMeta/.lcLine treatment) instead of spilling past it. */
.scanResult__shot .scanned { margin: 0; font-size: var(--fs-sm); overflow-wrap: anywhere; word-break: break-word; }
.scanResult__shot .thumb { margin: var(--space-1) 0 0; }
/* keep the shelf-shot column close in height to the (input-driven) match card so the
   2-up post-scan block has little dead space under "Clear", and the whole barcode
   section stays short enough that "Log this item" is in view after a scan. */
.scanResult__shot .thumb img { max-height: 88px; }
.scanResult .matchCard { margin-top: 0; }
/* drop the (empty) photo sub-column when nothing was captured, so a 1-column state has
   no phantom row-gap. The CHILD combinator is required: a descendant :has() also sees
   the figure's <img>/Clear button (which never carry [hidden]) and would always match. */
.scanResult__shot:not(:has(> :not([hidden]))) { display: none; }
/* collapse the whole wrapper in the pristine (nothing-scanned/typed) state so its own
   margin-top isn't dead space under the Scan/Photo/Type row. */
.scanResult:not(:has(.scanResult__shot > :not([hidden]))):not(:has(.matchCard:not([hidden]))) { display: none; }
@media (min-width: 22rem) {
  .scanResult:has(.thumb:not([hidden])):has(.matchCard:not([hidden])) {
    grid-template-columns: minmax(5.5rem, 0.72fr) minmax(0, 1.28fr);
  }
  /* compact the catalog card in the side-by-side: a small photo inline at the left,
     name/brand to its right, tight spacing — so the confirmation stays in the top
     third and "Log this item" stays reachable. Interactive controls KEEP their 44px
     tap target (the density comes from padding/font, never a smaller hit area). Below
     22rem and where :has() is unsupported it falls back to the vertical stack. */
  .scanResult .matchCard {
    padding: var(--space-2); gap: var(--space-1) var(--space-2);
    grid-template-columns: auto minmax(0, 1fr); align-items: start;
  }
  .scanResult .matchHead { grid-column: 1 / -1; }
  .scanResult .matchFig { grid-column: 1; grid-row: 2; margin: 0; }
  .scanResult .matchBody { grid-column: 2; grid-row: 2; gap: var(--space-1); }
  .scanResult .matchPhoto { width: 40px; height: 40px; object-fit: cover; }
  /* Save + Replace stay on ONE line in the narrow card column (a wrapped 2nd line was
     the main thing making the post-scan card tall). */
  .scanResult .matchActions { flex-wrap: nowrap; align-items: center; }
  .scanResult .matchSave { flex: 0 1 auto; }
  .scanResult .matchName, .scanResult .matchBrand { padding: 0.4rem 0.5rem; }
  .scanResult .matchReplace { padding: var(--space-1) var(--space-2); }
  /* a not-yet-catalogued scan renders only the "🆕 New…" line — span it full width */
  .scanResult .matchNew { grid-column: 1 / -1; }
  /* no catalog photo yet -> the body takes the full card width */
  .scanResult .matchCard:not(:has(.matchFig:not([hidden]))) .matchBody { grid-column: 1 / -1; }
}

/* admin: fix flagged observations */
.fixRow { background: var(--card); border: 1px solid var(--line); border-radius: 0.6rem; overflow: hidden; transition: opacity 0.4s ease; }
.fixRow.done { opacity: 0; }
.fixRowHead {
  width: 100%; text-align: left; font: inherit; font-weight: 600; font-variant-numeric: tabular-nums;
  background: transparent; color: var(--ink); border: 0; padding: 0.8rem 0.9rem; cursor: pointer; min-height: 48px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fixRowBody { display: grid; gap: var(--space-3); padding: 0 var(--space-3) var(--space-3); }
.fixWhy {
  margin: 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); font-size: 0.9rem; font-weight: 600;
  background: color-mix(in srgb, var(--err) 12%, var(--card));
  border: 1px solid color-mix(in srgb, var(--err) 35%, var(--line)); color: var(--ink);
}
.fixField { display: grid; gap: var(--space-1); font-weight: 600; font-size: 0.9rem; }
/* guarded barcode field: the input reads as locked (muted, dashed) until "Edit" unlocks it */
.fixBarcode .fixUnlock { justify-self: start; min-height: 36px; padding: 0.2rem 0.4rem; font-size: 0.85rem; }
input.fixLocked {
  background: var(--surface-2); color: var(--ink-muted); border-style: dashed; cursor: not-allowed;
}
.fixPhotos { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.fixThumb { margin: 0; flex: 1; min-width: 8rem; text-align: center; }
.fixThumb img { width: 100%; max-height: 200px; object-fit: contain; border: 1px solid var(--line); border-radius: 0.5rem; background: var(--bg); }
.fixThumb figcaption { font-size: 0.8rem; color: var(--muted); margin-top: 0.2rem; }
.fixDanger { margin-top: 0.6rem; padding-top: 0.8rem; border-top: 1px dashed var(--line); }
.fixLife { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.lifeBtn { width: auto; min-height: 44px; padding: 0.5rem 0.9rem; font-size: 0.95rem; }
.lifeBtn.active { background: var(--accent); color: var(--accent-ink); }

/* store-leaderboard name + recommendation chips (the canonical .row leaderboard, P2.1b) */
.lbName { flex: 1; min-width: 0; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lbRec { font-size: 0.72rem; color: var(--muted); font-weight: 700; flex: none; }
a.big-btn { text-decoration: none; text-align: center; }

.lcLine { margin: 0 0 0.6rem; font-variant-numeric: tabular-nums; word-break: break-word; }

/* product views (D-2) + product detail (U-4).
   The Items hub + detail now ride the design system (.view/.segmented/.row/.stat);
   the old .segs/.seg toggles and .headSpacer/.productTitle filler are retired. */
.lbName.linkName {
  flex: 1; background: none; border: 0; padding: 0; font: inherit; font-weight: 700;
  color: var(--accent); cursor: pointer; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 0; }
/* product-detail header card: full photo stacked over name / brand / barcode */
.prodHead { display: grid; gap: var(--space-3); }
.prodName { font-size: var(--fs-lg); font-weight: 700; line-height: var(--lh-tight); }

/* ============================================================================
   Design system component library (U-0)
   One of each shared component, on the tokens above. These are introduced here
   (behavior-neutral) and screens migrate onto them in U-1+. Class names are the
   canonical redesign vocabulary (off the v1 review/fix names). U-2 took .field
   to its canonical labeled-field form (the v1 box is now .fieldgroup) and gave
   .thumb a --placeholder; the legacy .card base rule stays until its screens are
   rewritten. The .card-- and .pill-- modifiers here are additive.
   ============================================================================ */

/* screen wrapper + [title · action] header */
.view { margin: var(--space-4) 0; }
.view-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.view-head h2 { flex: 1; min-width: 0; margin: 0; font-size: var(--fs-xl); font-weight: 700; line-height: var(--lh-tight); }

/* section heading (neutral; the one section-heading style for every screen) */
.section-h {
  margin: var(--space-4) 0 var(--space-2); font-size: var(--fs-sm); font-weight: 600;
  letter-spacing: 0.03em; text-transform: uppercase; color: var(--ink-muted);
}

/* card modifiers (the base .card above is the one card; these add behavior) */
/* a card whose children stack with the standard gap (e.g. the Photo-review card:
   photo over the name/brand/actions). Replaces the old review-card grid (P2.1b).
   The transition + .done state carry the resolve fade-out the review card had
   (cardResolved() adds .done, then removes the node 500ms later). */
.card--stack { display: grid; gap: var(--space-3); transition: opacity 0.4s ease; }
.card--stack.done { opacity: 0; }

/* a whole list row that acts as one big tap target (the Stores list). The base .row
   already paints the card; this adds the affordance + press feedback. */
.row--tappable { cursor: pointer; }
.row--tappable:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.row--tappable:active { background: var(--accent-weak); }
/* The Log last-scan rows are tappable too (→ the scan-detail page), but they carry only
   a left lifecycle STRIPE (no full border). The generic hover above would tint that
   stripe accent on a plain row; give these a background-only hover and let the cpn/soon
   rules re-assert the green/amber lifecycle color. */
.lastScan__row.row--tappable:hover { border-color: transparent; background: var(--accent-weak); }
.lastScan__row.row--tappable.row--cpn:hover { border-left-color: var(--ok); }
.lastScan__row.row--tappable.row--soon:hover { border-left-color: var(--warn); }
/* J1: a store someone in the app visited recently — a faint "already swept" wash
   (kept light so the red expired count stays readable) + a green recency note. */
.row--visited { background: color-mix(in srgb, var(--ink-muted) 7%, var(--surface)); }
.row__visited { color: var(--ok); font-weight: 600; }

/* list row: [media] [title + meta] [trailing] — one DOM shape for every list */
.row {
  display: flex; align-items: center; gap: var(--space-3); min-width: 0;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--space-3) var(--card-pad);
}
.rows { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.row__media { flex: none; }
.row__main { flex: 1; min-width: 0; }
.row__title { font-size: var(--fs-md); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the title link is a button (inline-block), so the parent's text-overflow can't
   clip it — it must truncate itself, or a long product name widens the whole page
   and forces a horizontal scroll. Mirrors the working .lastScan__name rule. */
.row__title .linkName, .row__title .lbName {
  display: inline-block; max-width: 100%; min-width: 0; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.row__meta { font-size: var(--fs-sm); color: var(--ink-muted); }
/* role-select-fix: space the trailing controls so the role <select> and the irreversible
   Revoke button can't be mis-tapped for each other (they were touching at 0 gap). */
.row__trail { flex: none; display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; justify-content: flex-end; }
/* D4: store drill-in links rendered inline in a meta line (the All-Expired feed).
   A wrapping, middot-separated row of name-links: unlike the single .row__title link
   these may wrap to several lines and must never clip the row or push the page wide. */
.storeLinks { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.storeLinks .lbName, .storeLinks .linkName {
  flex: 0 1 auto; max-width: 100%; min-height: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600;
}
.storeSep { color: var(--ink-muted); }
/* compact leading thumbnail when a product photo (D-4) sits in a row's media slot */
.row__media .reviewPhoto {
  width: 52px; height: 52px; min-height: 0; max-height: none; object-fit: cover; border-radius: var(--radius-sm);
}
.row__media .reviewPhoto.noPhoto { font-size: var(--fs-xs); font-weight: 600; text-align: center; padding: 2px; }

/* item tile (store-page rows): a multi-line, scannable variant of .row. Top-aligned
   so the photo + urgency pill line up with the product name; the name wraps to two
   lines (long product names stay legible instead of truncating to "M&M's Share M…");
   the expiry line is full-contrast (--ink) so the key fact doesn't fade into grey. */
.row--item { align-items: flex-start; }
.row--item .row__title { white-space: normal; margin-bottom: 2px; }
.row--item .row__title .linkName, .row--item .row__title .lbName {
  white-space: normal; overflow: hidden; text-overflow: ellipsis; line-height: var(--lh-tight);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  min-width: 0; max-width: 100%; overflow-wrap: anywhere; /* wrap to 2 lines; break a long unbroken token rather than widen the page */
}
.row--item .row__meta { line-height: var(--lh-tight); }
.row--item .row__meta + .row__meta { margin-top: 2px; }
.row__meta--key { color: var(--ink); font-weight: 600; }

/* H2: Recent-feed lifecycle accent — a left edge that color-codes the log at a glance.
   Green = a coupon picked up today; amber = logged for an upcoming return. Pairs with
   the recentLifecycleSeverity pill so the meaning is never color-only. */
.row--cpn { border-left: 4px solid var(--ok); }
.row--soon { border-left: 4px solid var(--warn); }

/* button: base + modifiers + sizes. Filled-accent defined ONCE. ≥44px always. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  font: inherit; font-weight: 700; font-size: var(--fs-md); line-height: var(--lh-tight);
  min-height: 44px; padding: 0.6rem 1rem; border-radius: var(--radius); cursor: pointer;
  text-decoration: none; background: var(--surface); color: var(--ink); border: 1px solid var(--line);
  touch-action: manipulation; /* no double-tap-zoom on rapid button taps (iOS Safari) */
}
.btn--primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.btn--ghost { background: transparent; color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.btn--danger { background: transparent; color: var(--danger); border-color: color-mix(in srgb, var(--danger) 55%, var(--line)); }
.btn--selected { background: var(--accent-weak); color: var(--accent); border-color: var(--accent); }
.btn--block { display: flex; width: 100%; }
.btn--lg { min-height: 48px; font-size: var(--fs-lg); }
.btn--sm { min-height: 44px; padding: 0.3rem 0.7rem; font-size: var(--fs-sm); font-weight: 600; } /* smaller text, tap target stays ≥44px */
.btn--icon { padding: 0.3rem; min-width: 44px; line-height: 1; } /* square-ish header affordance (e.g. the Planning "ⓘ") */
.btn:disabled { opacity: 0.6; cursor: progress; }

/* pill: one ramp, enum-keyed (see severityFor() in app.js). Additive on the
   existing .pill base; mirrors the legacy .s- / .sev- skins so migration is 1:1.
   NOTE: an earlier version of this comment used a glob with a star-then-slash,
   which closes a CSS comment early and silently killed the very next rule
   (.pill--ok) — the green "Picked up" chip then rendered as plain text. Never put
   a star-slash sequence inside a comment. */
.pill--ok { background: color-mix(in srgb, var(--ok) 28%, var(--surface)); }
.pill--info { background: color-mix(in srgb, var(--info) 24%, var(--surface)); }
.pill--warn { background: color-mix(in srgb, var(--warn) 40%, var(--surface)); border-color: color-mix(in srgb, var(--warn) 60%, var(--surface)); font-weight: 800; }
.pill--danger { background: color-mix(in srgb, var(--danger) 28%, var(--surface)); border-color: color-mix(in srgb, var(--danger) 55%, var(--surface)); font-weight: 800; }
.pill--neutral { background: color-mix(in srgb, var(--neutral) 24%, var(--surface)); }

/* KPI tile + grid */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin: var(--space-1) 0 var(--space-3); }
.stat { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3) var(--card-pad); min-width: 0; overflow: hidden; }
.stat__num { font-size: var(--fs-xl); font-weight: 800; line-height: var(--lh-tight); font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stat__label { margin-top: var(--space-1); font-size: var(--fs-sm); font-weight: 600; }
.stat__sub { margin-top: 0.1rem; font-size: var(--fs-xs); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
@media (max-width: 22rem) { .stat-grid { grid-template-columns: 1fr; } }

/* ONE metric tile, three shapes (P2.1a — collapsed the former .visitBar__metric and
   .visitStat into .stat). The base .stat above is the grid block; these two variants
   re-skin it into the slim sticky Log bar tile and the wrapping visit-card tile.
   Both are centered flex columns on the inset surface with the small radius; the
   status modifiers (below) tint border/bg/number and must follow the variants so the
   status background wins the cascade. Sized to be byte-for-byte the old looks. */
.stat--inline {
  display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1;
  padding: 0.1rem 0.35rem; border-radius: var(--radius-sm); background: var(--surface-2); overflow: visible;
}
.stat--inline .stat__num { font-size: var(--fs-sm); line-height: 1; }
.stat--inline .stat__label { margin-top: 1px; font-size: 0.55rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); }
.stat--chip {
  display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1.1;
  flex: 1 1 auto; min-width: 3.6rem; padding: var(--space-2);
  border-radius: var(--radius-sm); background: var(--surface-2); overflow: visible;
}
.stat--chip .stat__num { font-size: var(--fs-lg); line-height: 1.1; }
/* the bar/card tiles are centered & content-sized; unlike the grid tile their number
   must NOT inherit the base .stat__num ellipsis/clip (the old .visitBar__num/.visitStat__num
   had none), so a wide value ($ earned / $/hr) grows instead of clipping — true 1:1. */
.stat--inline .stat__num, .stat--chip .stat__num { overflow: visible; text-overflow: clip; white-space: normal; }
.stat--chip .stat__label { margin-top: 1px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); }
/* status tints (shared by the bar + card tiles): coupons green, for-future amber,
   revenue accent. cpn is identical across both old systems; soon was bar-only, rev
   card-only. The number takes the hue; high-contrast --ink label stays for the word. */
.stat--cpn { border-color: color-mix(in srgb, var(--ok) 45%, var(--line)); background: color-mix(in srgb, var(--ok) 12%, var(--surface)); }
.stat--cpn .stat__num { color: var(--ok); }
.stat--soon { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); background: color-mix(in srgb, var(--warn) 14%, var(--surface)); }
.stat--soon .stat__num { color: var(--warn); }
/* revenue / dollars-gotten read GREEN like coupons — money is green app-wide (Mike
   2026-06-19: "dollars we receive green … consistent throughout"). */
.stat--rev { border-color: color-mix(in srgb, var(--ok) 45%, var(--line)); background: color-mix(in srgb, var(--ok) 12%, var(--surface)); }
.stat--rev .stat__num { color: var(--ok); }
/* expired / drop / time-waster — RED (the Insights dashboard's "out there now / stop going"
   signal; mirrors --soon/--rev). */
.stat--exp { border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); background: color-mix(in srgb, var(--danger) 10%, var(--surface)); }
.stat--exp .stat__num { color: var(--danger); }
/* neutral chip — "Checked (missed)" and other non-urgent outcomes: muted, never red/green. */
.stat--neutral { border-color: color-mix(in srgb, var(--neutral) 40%, var(--line)); background: color-mix(in srgb, var(--neutral) 8%, var(--surface)); }
.stat--neutral .stat__num { color: var(--ink-muted); }
/* deltaChip — a tiny inline comparison-vs-prior badge used by the dashboard heroes/KPIs.
   Up is good (green) for our metrics; down red; flat neutral. Color is never the only
   signal — the ↑/↓/— arrow carries it too. */
.deltaChip { display: inline-flex; align-items: center; gap: 0.15rem; font-size: 0.7rem; font-weight: 700;
  padding: 0.05rem 0.35rem; border-radius: var(--radius-pill); white-space: nowrap; font-variant-numeric: tabular-nums; }
.deltaChip--up { color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, var(--surface)); }
.deltaChip--down { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, var(--surface)); }
.deltaChip--flat { color: var(--ink-muted); background: color-mix(in srgb, var(--ink-muted) 12%, var(--surface)); }

/* Insights dashboard (P4): the view tab-strip + the persistent global control bar. */
.insightsTabs { margin: 0 0 var(--space-2); }
.insightsControls { margin: 0; }
/* SMALL FLOATING filter button at the bottom (overlays content) — tap opens the bottom sheet.
   Frees the whole top of the dashboard for data; the active range shows on the button, the full
   context on its aria-label + in the sheet. Sits above the bottom nav. */
.insightsFab {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(64px + env(safe-area-inset-bottom)); z-index: 40;
  display: inline-flex; align-items: center; gap: var(--space-2); max-width: 90vw;
  padding: 0.5rem 1rem; font: inherit; font-weight: 700; font-size: var(--fs-sm);
  color: #fff; background: var(--accent); border: none; border-radius: var(--radius-pill);
  box-shadow: var(--shadow-2, 0 4px 16px #0005); cursor: pointer;
}
.insightsFab:active { transform: translateX(-50%) scale(0.97); }
.insightsFab__icon { width: 16px; height: 16px; flex: none; }
.insightsFab__label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#insightsBody.has-fab { padding-bottom: 4rem; } /* clear the floating filter button */

/* 3A: GLOBAL quick-scan FAB — a circular capture button floating above the bottom nav on the
   main tabs (RIGHT side, opposite the centered insights filter FAB, so the two never collide).
   Mirrors the insightsFab elevation + safe-area inset; same z-index < the sheet scrim (50), so an
   open bottom sheet covers it instead of floating over the modal. JS toggles [hidden] per route. */
.quickScanFab {
  position: fixed; right: max(var(--space-4), env(safe-area-inset-right));
  bottom: calc(64px + env(safe-area-inset-bottom)); z-index: 40;
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; padding: 0;
  color: #fff; background: var(--accent); border: none; border-radius: 50%;
  box-shadow: var(--shadow-2, 0 4px 16px #0005); cursor: pointer;
}
.quickScanFab:active { transform: scale(0.95); }
.quickScanFab__icon { width: 26px; height: 26px; flex: none; }

/* Filter bottom sheet */
.sheet__scrim { position: fixed; inset: 0; background: #0006; opacity: 0; transition: opacity .2s; z-index: 50; }
.sheet__scrim.is-open { opacity: 1; }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 51;
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-2) var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom));
  background: var(--surface); border-top-left-radius: 18px; border-top-right-radius: 18px;
  box-shadow: var(--shadow-2, 0 -8px 30px #0004); transform: translateY(100%); transition: transform .2s;
  max-height: 85vh; overflow-y: auto;
}
.sheet.is-open { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) { .sheet, .sheet__scrim { transition: none; } }
.sheet__grab { width: 36px; height: 4px; border-radius: 2px; background: var(--line); margin: 4px auto 0; flex: none; }
.sheet__head { display: flex; align-items: center; justify-content: space-between; }
.sheet__title { font-weight: 700; font-size: var(--fs-md); }
.sheetCtl { display: grid; gap: 6px; }
.sheetCtl__label { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); }
/* horizontal scroll-snap chip rail — any number of range presets on ONE line */
.chipRail { display: flex; gap: var(--space-2); overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; padding-bottom: 2px; scrollbar-width: none; }
.chipRail::-webkit-scrollbar { display: none; }
.chipRail .chip { flex: none; scroll-snap-align: start; white-space: nowrap; cursor: pointer; }
.chipRail .chip.is-active { border-color: var(--accent); color: var(--accent); background: var(--accent-weak); font-weight: 700; }
.insightsCtl { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.insightsCtl__label { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); flex: 0 0 2.6rem; }
.insightsCtl .insightsSeg { flex: 1 1 auto; margin: 0; }
/* compact the control-bar segmented pills so 5 range presets wrap tidily */
.insightsSeg .segmented__item { padding: 0.25rem 0.55rem; font-size: var(--fs-sm); }
.insightsCompare { align-self: flex-start; }
.insightsCompare.is-active { border-color: color-mix(in srgb, var(--ok) 50%, var(--line)); color: var(--ok); background: color-mix(in srgb, var(--ok) 8%, var(--surface)); }
/* lifecycle status chips read as a compact auto-fit row */
.lifecycleChips { grid-template-columns: repeat(auto-fit, minmax(4.2rem, 1fr)); }
/* dashboard headline tile + lone-tile span (P4.4). The hero is the one number that
   leads each dashboard (Stats / admin Dashboard / Revenue Trends); a section grid with
   a single tile spans the full width so it never sits lopsided in the 2-col grid. */
.stat--hero { margin: var(--space-1) 0 var(--space-4); padding: var(--space-4) var(--card-pad); }
.stat--hero .stat__num { font-size: var(--fs-2xl); }
.stat--hero .stat__label { margin-top: var(--space-1); font-size: var(--fs-md); }
.stat--hero .stat__sub { margin-top: var(--space-1); font-size: var(--fs-sm); }
.stat-grid > .stat:only-child { grid-column: 1 / -1; }

/* P4.3 nearby-stores map (Leaflet). The container needs an explicit height; a local
   stacking context (z-index:0) keeps Leaflet's internal panes from ever riding over
   the fixed bottom nav. Pins/popups are themed; OSM tiles render light in both modes. */
/* Map+list picker: a compact map (~42vh) over a synced, scrollable closest-first store list. */
#map { height: 42vh; min-height: 220px; border-radius: var(--radius); border: 1px solid var(--line); overflow: hidden; position: relative; z-index: 0; }
.leaflet-container { background: var(--surface-2); font: inherit; }
.mapFilters { margin: 0 0 var(--space-2); }
/* segmented tabs living in the horizontal map-filter rail: compact + non-shrinking so they
   scroll rather than squash (the rail provides overflow-x). */
.mapFilters .segmented__item { flex: none; scroll-snap-align: start; white-space: nowrap; min-height: 38px; padding: 0.3rem 0.7rem; font-size: var(--fs-xs); }
.mapList { margin: var(--space-3) 0 0; display: grid; gap: var(--space-2); }
.mapRow { display: flex; align-items: center; gap: var(--space-3); scroll-margin: 6rem; }
.mapRow.is-active { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-weak); }
.mapRow__dot { flex: none; display: flex; align-items: center; }
.mapDot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; flex: none; }
.mapDot--fresh { background: var(--pin-fresh); }
.mapDot--recent { background: var(--pin-recent); }
.mapDot--new { background: var(--pin-new); }
.mapDot--clear { background: var(--pin-clear); }
/* P2.8: pin-color legend (the dots are otherwise unexplained). Shown only when the map has pins. */
.mapLegend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); margin: var(--space-2) 0 0; font-size: var(--fs-xs); color: var(--ink-muted); }
.mapLegend__item { display: inline-flex; align-items: center; gap: 0.3rem; }
.mapLegend__dot { width: 10px; height: 10px; border-radius: 50%; flex: none; border: 1px solid rgba(0, 0, 0, 0.15); }
.mapLegend__dot--fresh { background: var(--pin-fresh); }
.mapLegend__dot--recent { background: var(--pin-recent); }
.mapLegend__dot--new { background: var(--pin-new); }
.mapLegend__dot--clear { background: var(--pin-clear); }

/* P4.3 v2 store badges: same size for all — the COUNT is the number inside, the COLOR is the
   status. Expired stores pop; "clear" recedes via opacity (not size). The badge is an INNER div
   (.mapPin) inside Leaflet's marker-icon root (.mapPinWrap), so Leaflet's hard `display:block`
   on the root can't fight the flex centering — the number sits dead-center every time. */
.mapPinWrap { background: none; border: 0; }
.mapPin { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
  box-sizing: border-box; border-radius: 50%; border: 1.5px solid #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  font-weight: 800; font-size: 0.68rem; color: #fff; font-variant-numeric: tabular-nums; }
.mapPin--fresh { background: var(--pin-fresh); }
.mapPin--recent { background: var(--pin-recent); }
.mapPin--new { background: var(--pin-new); }
.mapPin--clear { background: var(--pin-clear); opacity: 0.5; }
.mapPin__n { line-height: 1; text-align: center; }
.mapPin__n:empty { display: none; }
/* cluster badge for overlapping pins — neutral so it reads as "group", not a status */
.mapCluster { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
  box-sizing: border-box; border-radius: 50%; border: 1.5px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  font-weight: 800; color: #fff; background: #45506b; font-variant-numeric: tabular-nums; }
.mapCluster--sm { font-size: 0.66rem; }
.mapCluster--md { font-size: 0.8rem; }
.mapCluster--lg { font-size: 0.95rem; }
/* map controls: "recenter on me" + "fit all stores" */
.mapRecenter, .mapFitAll { width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); font-size: 1.05rem; line-height: 1; cursor: pointer; box-shadow: var(--shadow-1); }

/* the store-detail bottom sheet (replaces the Leaflet popup, which was hard-locked white and
   broke dark mode) — reuses .sheet/.sheet__scrim, themed via the design tokens. */
.mapSheet__title { font-weight: 700; font-size: var(--fs-md); }
.mapSheet__meta { color: var(--ink-muted); font-size: var(--fs-sm); }
.mapSheet__meta > div { line-height: 1.5; }
.mapSheet__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* P3.13: inline product-profile editor on the product page (admin) */
.prodEditBtn { margin-top: var(--space-2); }
.prodEdit__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* H1: current-visit dashboard — the in-store bar (Mike 2026-06-19). LEFT = store name +
   "time here"; RIGHT = a 2×2 grid of KPI cards (coupons + $ green, products neutral, for-
   future amber). It's the LAST child of #app and PINNED to the bottom of the Log scrollport
   with position:sticky bottom:0, so when the form area scrolls (e.g. the post-scan match
   card overflows #app) the bar stays put and the content scrolls under it. (A BOTTOM sticky
   is safe — the old hazard was a TOP sticky with a stale whole-page-scroll offset.) */
.visitDash {
  margin: 0; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--space-2) var(--space-3);
  box-shadow: var(--shadow-2);
  display: flex; align-items: center; gap: var(--space-2) var(--space-3);
  position: sticky; bottom: 0; z-index: 5;
}
.visitBar__left { flex: 1 1 9rem; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* the store address WRAPS instead of truncating to "…" (Mike 2026-06-19): give it room and
   let it run onto a second line when the bar is tight, rather than hiding the whole address. */
.visitBar__where {
  margin: 0; min-width: 0; font-size: var(--fs-sm); color: var(--ink-muted);
  overflow-wrap: anywhere;
}
.visitBar__where .lbName, .visitBar__where .linkName {
  font-weight: 700; color: var(--ink); min-height: 0; max-width: 100%; text-align: left;
  /* override the base .lbName truncation (line ~479) so the address WRAPS, not "…" */
  white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
}
.visitBar__time { margin: 0; font-size: var(--fs-sm); color: var(--ink-muted); white-space: nowrap; }
.visitBar__time .visitBar__timeVal { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
/* the KPI cards: a 2×2 grid (coupons,$ / products,for-future) pinned to the right */
.visitBar__metrics { display: grid; grid-template-columns: repeat(2, minmax(3.6rem, 1fr)); gap: var(--space-1); flex: 0 1 auto; min-width: 0; }
.visitBar__metrics .stat { padding: 0.3rem 0.5rem; }
.visitBar__metrics .stat__num { font-size: var(--fs-lg); }
.visitBar__metrics .stat__label { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); }

/* Visits history (Phase I) — one card per (store, day) trip: a date+duration head,
   the store (tappable), and a wrapping strip of metric tiles. Coupons read green
   (the grab/win); admin economics ($ earned, $/hr) read accent. */
.visitCard {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--card-pad);
}
/* the whole card is one tap target -> the visit's detail. A real <button> styled to
   fill the card, so it's keyboard/SR-operable (the visual chrome stays on the .visitCard li). */
.visitCard__tap {
  display: grid; gap: var(--space-2); width: 100%; margin: 0; padding: 0;
  font: inherit; color: inherit; text-align: left; background: none; border: 0;
  cursor: pointer; border-radius: var(--radius); touch-action: manipulation;
}
.visitCard__tap:active { opacity: 0.85; }
.visitCard__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
/* the day with the trip's clock range stacked beneath it (M1) */
.visitCard__when { min-width: 0; display: grid; gap: 1px; }
.visitCard__date { font-weight: 700; font-size: var(--fs-md); }
.visitCard__time { font-size: var(--fs-xs); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.visitCard__dur { flex: none; font-size: var(--fs-sm); color: var(--ink-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.visitCard__store { min-width: 0; display: flex; align-items: center; gap: var(--space-2); }
.visitCard__store .lbName, .visitCard__store .linkName {
  flex: 1; font-weight: 600; max-width: 100%; min-width: 0; min-height: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.visitCard__chev { flex: none; color: var(--ink-muted); font-size: 1.3rem; line-height: 1; }
/* the card tiles are .stat.stat--chip (P2.1a); the metric tile rules live with .stat */
.visitMetrics { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* visit detail (drill from Visits / Stats): a header (store link + clock range) then
   the shared .stat-grid tiles + the items list. */
.detailHead { margin: 0 0 var(--space-3); display: grid; gap: 2px; }
.detailHead__store { margin: 0; font-size: var(--fs-lg); font-weight: 700; }
.detailHead__store .lbName, .detailHead__store .linkName { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.detailHead__sub { margin: 0; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

/* Operator: correct a visit's real start/end (collapsible). */
.timeEditor { margin: 0 0 var(--space-3); }
.timeEditor__panel { margin-top: var(--space-2); padding: var(--space-3); display: grid; gap: var(--space-3); background: var(--surface-2); border-radius: var(--radius-sm); }
.timeEditor__row { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.timeEditor__field { display: grid; gap: 4px; flex: 1 1 8rem; }
.timeEditor__label { font-size: var(--fs-sm); color: var(--ink-muted); }
.timeEditor__field input[type="time"] { font: inherit; padding: var(--space-2); border: 1px solid var(--neutral); border-radius: var(--radius-sm); background: var(--surface); color: inherit; }
.timeEditor__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.timeEditor__msg { margin: 0; min-height: 1.2em; }

/* Stats day picker: ‹ prev · day (Today) · next › , with a Today jump button. */
.dayNav { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3); }
.dayNav__arrow { flex: none; min-width: 44px; padding: 0.4rem 0.6rem; font-size: 1.3rem; line-height: 1; }
.dayNav__label { flex: 1; min-width: 0; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1px; }
.dayNav__date { font-weight: 700; font-size: var(--fs-md); }
.dayNav__today { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--accent); }
.dayNav__jump { flex: none; }
.statsEveryone { margin-top: var(--space-4); }

/* scan detail (item #2): view + edit one capture, then reprocess. Stacked cards
   (header status · photos · editable fields), on the design-system .card/.field. */
#obsBody { display: grid; gap: var(--space-3); }
.obsHead { display: grid; gap: var(--space-2); padding: var(--space-3) var(--card-pad); }
.obsHead__top { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
.obsHead .fixWhy { margin: 0; }
.obsForm { display: grid; gap: var(--space-3); }
/* Two UNIFORM, fixed-size photo thumbnails side by side (item + date), each with its
   Replace/Add control beneath. Fixed height keeps the slots aligned whether or not a
   slot has a photo (a missing one shows a same-size "No photo" tile, not a collapsed
   box) — fixes the "buttons out of whack" layout. */
.obsPhotos { display: flex; gap: var(--space-3); }
.obsPhotoSlot { flex: 1 1 0; min-width: 0; display: grid; gap: var(--space-1); }
.obsPhotoSlot__cap { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-muted); }
.obsThumb {
  width: 100%; height: 7.5rem; display: flex; align-items: center; justify-content: center;
  overflow: hidden; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.obsThumb__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.obsThumb__empty { color: var(--ink-muted); font-size: var(--fs-xs); font-weight: 600; }
.obsDanger { padding: var(--space-3) var(--card-pad); }

/* close-out summary popup (F5) + any future modal — a centered card over a scrim.
   No framework: showModal() builds it; backdrop tap / ✕ / Escape dismiss. */
.modalBack {
  position: fixed; inset: 0; z-index: 20; display: flex; align-items: center; justify-content: center;
  padding: var(--space-4); background: color-mix(in srgb, #000 55%, transparent);
}
.modalCard {
  width: 100%; max-width: 26rem; max-height: 88vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-2, 0 12px 32px rgba(0,0,0,0.35)); padding: var(--card-pad);
  padding-bottom: calc(env(safe-area-inset-bottom) + var(--card-pad));
}
.modalCard__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-2); }
.modalCard__title { margin: 0; font-size: var(--fs-xl); font-weight: 700; line-height: var(--lh-tight); }
.modalCard__x {
  flex: none; min-width: 44px; min-height: 44px; font-size: 1.1rem; line-height: 1; cursor: pointer;
  background: none; border: 0; color: var(--ink-muted); border-radius: var(--radius-sm); touch-action: manipulation;
}
.modalCard__body { display: grid; gap: var(--space-1); }
.modalCard__foot { margin-top: var(--space-3); }
.modalLede { margin: 0 0 var(--space-2); font-weight: 600; }
.modalNote { margin: var(--space-1) 0 0; font-size: var(--fs-sm); }

/* segmented control (full ARIA tabs/aria-pressed; reused by Shop & Items & More) */
.segmented { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-3); }
.segmented__item {
  font: inherit; font-size: var(--fs-sm); font-weight: 600; min-height: 44px; padding: 0.4rem 0.85rem;
  border: 1px solid var(--control-border); border-radius: var(--radius-pill); background: transparent; color: var(--ink); cursor: pointer;
}
.segmented__item[aria-selected="true"], .segmented__item[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
/* Planning sub-tabs: keep all four (Trip Planner / What's Hot / All Expired / Places) on ONE line —
   "Places" was wrapping to a second row and eating screen height. Compact, no-wrap pills that shrink
   to fit; horizontal scroll is the safety valve on the very narrowest phones (Mike, 2026-06-26). */
#productsNav { flex-wrap: nowrap; gap: var(--space-1); overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
#productsNav::-webkit-scrollbar { display: none; }
#productsNav .segmented__item { flex: 0 1 auto; min-width: 0; white-space: nowrap; padding: 0.4rem 0.6rem; font-size: var(--fs-xs); scroll-snap-align: start; }

/* window filter (Items → What's Hot, D2): a CHIP strip, deliberately distinct from
   the .segmented section tabs above it — soft-cornered, smaller, and tinted-when-active
   (accent-weak fill, NOT the solid-accent fill the tabs use) so it reads as a filter,
   not a second row of tabs. Tap target stays ≥44px. */
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3); }
/* the inline "Window"/"Range" label that marks a chip row as a filter (not section tabs) */
.chipsLabel { align-self: center; margin-right: var(--space-1); font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.03em; color: var(--ink-muted); }
.chip {
  font: inherit; font-size: var(--fs-xs); font-weight: 600; min-height: 44px; padding: 0.3rem 0.7rem;
  border: 1px solid var(--control-border); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink-muted); cursor: pointer;
}
.chip[aria-pressed="true"] { background: var(--accent-weak); color: var(--accent); border-color: var(--accent); }

/* honest states: loading / empty / error are distinct, shared components */
.state { padding: var(--space-5) var(--space-4); text-align: center; color: var(--ink-muted); font-weight: 600; }
.state--error { color: var(--danger); }
.state--loading { display: flex; align-items: center; justify-content: center; gap: var(--space-2); }
/* paragraphs inside a state block (e.g. the location-denied / no-store messages) — one
   shared bottom margin instead of per-paragraph inline styles. errorState's own <p> keeps
   its inline margin, so it is unaffected. */
.state p { margin: 0 0 var(--space-2); }
.spinner {
  width: 1.1em; height: 1.1em; border: 2px solid color-mix(in srgb, var(--ink-muted) 40%, transparent);
  border-top-color: var(--accent); border-radius: 50%; display: inline-block; animation: spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
@keyframes spin { to { transform: rotate(360deg); } }

/* bottom navigation — fixed to the viewport bottom; body reserves space for it */
.navbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 6; display: flex; gap: var(--space-1);
  background: var(--surface); border-top: 1px solid var(--line); box-shadow: var(--shadow-1);
  padding: var(--space-1) var(--space-2) calc(env(safe-area-inset-bottom) + var(--space-1));
}
.moreLinks { display: grid; gap: var(--space-2); }
/* More tab: the sign-out card and the first action-link group butted at 0px (their 1px
   borders sat on each other, reading as an overlap) — separate them. */
#moreView > .card { margin-bottom: var(--space-3); }
/* 1B store-page pin button: give it the same breathing room as the rest of the store page
   (its 1px border was butting the header card above + the segmented tabs below at 0px), and
   the accent "pressed" skin every other aria-pressed toggle uses (chip/segmented/torch). */
.setCurrentBtn { margin: var(--space-3) 0; }
.setCurrentBtn[aria-pressed="true"] { background: var(--accent-weak); color: var(--accent); border-color: var(--accent); }
.navitem {
  flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px;
  min-height: 48px; padding: var(--space-1) 0; font: inherit; font-size: var(--fs-xs); font-weight: 600;
  /* center + tight leading so a two-word label ("Current Store") wraps compactly on
     narrow phones instead of looking uneven against the single-word tabs */
  text-align: center; line-height: 1.1;
  background: none; border: 0; color: var(--ink-muted); cursor: pointer; text-decoration: none;
}
.navitem .navicon { width: 1.5rem; height: 1.5rem; display: block; }
/* inline SVG icon (P2.3, OD-3): sized in em so it tracks the button text, tinted via currentColor */
.ic { width: 1.1em; height: 1.1em; vertical-align: -0.18em; flex: none; }
/* 0F a11y: the active tab is not color-ALONE — a bolder label + a 2px top indicator bar (an
   inset shadow, so no layout shift) carry the state for low-vision / color-blind users too. */
.navitem[aria-current="page"] { color: var(--accent); font-weight: 800; box-shadow: inset 0 2px 0 0 var(--accent); }
/* 0H: "work waiting" count bubble on the More tab (operator-only; hidden when zero). */
.navitem { position: relative; }
.navBadge {
  position: absolute; top: 1px; left: calc(50% + 0.55rem);
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--radius-pill);
  background: var(--badge-bg); color: #fff;
  font-size: 0.62rem; font-weight: 800; line-height: 16px; text-align: center;
}
/* 0H: the per-link count on the More-page Review / Fix buttons — pushed to the row's end. */
.workCount {
  margin-left: auto; min-width: 1.5rem; padding: 0 0.45rem; border-radius: var(--radius-pill);
  background: var(--badge-bg); color: #fff; font-size: var(--fs-xs); font-weight: 800;
  line-height: 1.55; text-align: center; flex: none;
}
/* 0H: the "Use '<previous name>'" fill chip inside a Review card (reuses .chip). */
.useChip { display: inline-block; margin: 0.35rem 0 0.2rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 0C: transient "Logged ✓ — Undo" toast, centered above the bottom nav; auto-dismisses ~6s. */
.undoToast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom) + 72px); z-index: 20;
  display: flex; align-items: center; gap: var(--space-3); max-width: calc(100vw - 2rem);
  background: var(--ink); color: var(--surface);
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);
  border-radius: var(--radius-pill); box-shadow: var(--shadow-2); font-weight: 600;
}
.undoToast__btn {
  font: inherit; font-weight: 800; color: var(--surface); background: none; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--surface) 55%, transparent); border-radius: var(--radius-pill);
  padding: 0.3rem 0.9rem; min-height: 44px; touch-action: manipulation;
}

/* one tokenized focus ring on every interactive element (incl. anchor-buttons) */
.btn:focus-visible, .navitem:focus-visible, .segmented__item:focus-visible, .chip:focus-visible,
.row--tappable:focus-visible, a.btn:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--accent) 85%, transparent); outline-offset: 2px;
}
/* A focused-AND-selected tab is filled solid accent, so an accent-tinted ring would
   vanish into it — switch the ring to accent-ink (the on-accent text color) for contrast. */
.segmented__item[aria-selected="true"]:focus-visible,
.segmented__item[aria-pressed="true"]:focus-visible { outline-color: var(--accent-ink); }

/* Multi-person trip close-out picker (P3.9) */
.tripChips { display: flex; flex-wrap: wrap; gap: .5rem; margin: .25rem 0 .5rem; }
.tripChip.is-on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.tripSplit { margin-top: .25rem; }
.tripSplit__h { margin: .25rem 0; font-size: .85rem; }
.tripSplit__row { display: flex; align-items: center; gap: .5rem; margin: .35rem 0; }
.tripSplit__name { flex: 1; min-width: 0; }
.tripSplit__pct { width: 4.5rem; }
.tripFoot { display: flex; gap: .5rem; }
.tripFoot .btn { flex: 1; }

/* Households (P3.12) — admin screen + the By-household stats rollup */
.hhCreate { display: flex; gap: .5rem; margin: .25rem 0 .75rem; }
.hhCreate__name { flex: 1; min-width: 0; }
.hhMove { max-width: 11rem; }
.hhDetails { width: 100%; }
.hhSummary { display: flex; justify-content: space-between; align-items: center; gap: .75rem; cursor: pointer; list-style: none; padding: .15rem 0; }
.hhSummary::-webkit-details-marker { display: none; }
.hhSummary__name { font-weight: 600; }
.hhSummary__name::before { content: '▸ '; color: var(--muted); }
.hhDetails[open] .hhSummary__name::before { content: '▾ '; }
.hhSummary__meta { color: var(--muted); font-size: .9rem; white-space: nowrap; }
.hhMembers { list-style: none; margin: .35rem 0 .25rem; padding: 0 0 0 1rem; }
.hhMember { display: flex; justify-content: space-between; gap: .75rem; padding: .2rem 0; font-size: .9rem; }
.hhMember__name { min-width: 0; }
.hhMember__meta { color: var(--muted); white-space: nowrap; }

/* Revenue Trends (P4.1) — dependency-free SVG bar charts */
.trendsSeg { margin: .25rem 0 .75rem; }
.barChartWrap { width: 100%; overflow: hidden; margin: .25rem 0 .75rem; }
.barChart { width: 100%; height: auto; display: block; }
.barChart__bar { fill: var(--accent); }
.barChart__axis { stroke: var(--muted); opacity: .35; }
.barChart__peak { fill: var(--muted); font-size: 10px; }
.barChart__xlabel { fill: var(--muted); font-size: 9px; }
.barChart__bar--peak { fill: var(--ok); }
.barChart__avg { stroke: var(--ink-muted); stroke-dasharray: 3 3; opacity: .55; }
.barChart__avglabel { fill: var(--ink-muted); font-size: 9px; }
.barChart__overlay { stroke: var(--accent); stroke-width: 2; opacity: .85; }
.barChart__hit { cursor: pointer; }
.barChart__bar.is-sel { stroke: var(--ink); stroke-width: 1.5; }
.chartCap { min-height: 1.15em; margin: 2px 0 .4rem; text-align: center; font-size: var(--fs-sm); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
/* daily activity heatmap */
.heatWrap { width: 100%; margin: .25rem 0 .5rem; }
.heat { width: 100%; height: auto; display: block; }
.heat__lab { fill: var(--ink-muted); font-size: 8px; }
.heat__cell { cursor: pointer; }
.heat__cell.is-sel { stroke: var(--ink); stroke-width: 1.5; }
.chartLegend { font-size: 0.7rem; margin: -.2rem 0 .6rem; }

/* inline hero sparkline */
.heroSpark { margin-top: 6px; }
.heroSpark .sparkline { width: 100%; height: 30px; display: block; }

/* gauge / bullet bar (Hit rate, Scout payoff) */
.gauge { margin: 0 0 var(--space-3); }
.gauge__top { display: flex; align-items: baseline; justify-content: space-between; }
.gauge__label { font-weight: 600; font-size: var(--fs-sm); }
.gauge__val { font-weight: 800; font-variant-numeric: tabular-nums; }
.gauge__track { height: .55rem; margin-top: 4px; background: var(--surface-2); border-radius: var(--radius-pill); overflow: hidden; }
.gauge__fill { height: 100%; background: var(--warn); border-radius: var(--radius-pill); }
.gauge__track.is-good .gauge__fill { background: var(--ok); }
.gauge__cap { font-size: var(--fs-sm); color: var(--ink-muted); margin-top: 3px; }

/* proportional funnel bars (lifecycle) */
.funnel { display: grid; gap: 6px; margin: 0 0 var(--space-3); }
.funnel__row { display: grid; grid-template-columns: 4.6rem 1fr 2.2rem; align-items: center; gap: var(--space-2); }
.funnel__label { font-size: var(--fs-sm); color: var(--ink-muted); }
.funnel__track { height: .8rem; background: var(--surface-2); border-radius: var(--radius-sm); overflow: hidden; }
.funnel__fill { height: 100%; background: var(--accent); border-radius: var(--radius-sm); min-width: 2px; }
.funnel__fill--exp { background: var(--danger); }
.funnel__fill--cpn { background: var(--ok); }
.funnel__fill--soon { background: var(--warn); }
/* "Checked (missed)" is a neutral, non-urgent outcome — NOT the brand-red base fill (which reads
   like "expired"). The base .funnel__fill keeps --accent for any untinted/unknown row. */
.funnel__fill--neutral { background: var(--neutral); }
.funnel__count { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }

/* secondary-detail disclosure on the Insights tabs */
.insightsMore { margin: 0 0 var(--space-3); }
.insightsMore > summary { cursor: pointer; font-weight: 700; font-size: var(--fs-sm); color: var(--accent); padding: var(--space-2) 0; list-style: none; }
.insightsMore > summary::-webkit-details-marker { display: none; }
.insightsMore > summary::after { content: ' ▾'; color: var(--ink-muted); }
.insightsMore[open] > summary::after { content: ' ▴'; }

/* store-grade distribution bar (Stores tab) */
.gradeBar { display: flex; gap: 2px; height: 1.9rem; margin: 0 0 var(--space-3); border-radius: var(--radius-sm); overflow: hidden; }
.gradeBar__seg { display: flex; align-items: center; justify-content: center; min-width: 1.7rem; font-size: var(--fs-sm); font-weight: 800; color: #fff; }
.gradeBar__seg--ok { background: var(--ok); }
.gradeBar__seg--neutral { background: var(--neutral); }
.gradeBar__seg--warn { background: var(--warn); }
.gradeBar__seg--danger { background: var(--danger); }

/* rev/hr ranking mini-bar inside a store row */
.miniBar { height: .35rem; margin-top: 5px; background: var(--surface-2); border-radius: var(--radius-pill); overflow: hidden; }
.miniBar__fill { height: 100%; border-radius: var(--radius-pill); min-width: 3px; }
.miniBar__fill--ok { background: var(--ok); }
.miniBar__fill--neutral { background: var(--neutral); }
.miniBar__fill--warn { background: var(--warn); }
.miniBar__fill--danger { background: var(--danger); }

/* due-state chip in a store row's trail */
.dueChip { font-size: 0.68rem; font-weight: 700; white-space: nowrap; padding: 0.1rem 0.5rem; }
.dueChip--ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, var(--line)); background: color-mix(in srgb, var(--ok) 8%, var(--surface)); }
.dueChip--warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); background: color-mix(in srgb, var(--warn) 10%, var(--surface)); }
.dueChip--danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); background: color-mix(in srgb, var(--danger) 8%, var(--surface)); }
.fleetToggle { margin: 0 0 var(--space-2); }
.row__usually { font-size: var(--fs-sm); color: var(--ink-muted); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* People leaderboard — ranked rows + a 100%-stacked share bar */
.lbRow__top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.lbRow__name { font-weight: 700; }
.lbRow__rev { font-weight: 800; color: var(--ok); font-variant-numeric: tabular-nums; }
.lbBar { height: .4rem; margin: 5px 0; background: var(--surface-2); border-radius: var(--radius-pill); overflow: hidden; }
.lbBar__fill { height: 100%; background: var(--ok); border-radius: var(--radius-pill); min-width: 2px; }
.lbRow__summary { cursor: pointer; list-style: none; }
.lbRow__summary::-webkit-details-marker { display: none; }
.shareWrap { margin: 0 0 var(--space-3); }
.shareBar { display: flex; height: 1.4rem; border-radius: var(--radius-sm); overflow: hidden; gap: 1px; }
.shareBar__seg { min-width: 3px; }
.shareLegend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); margin-top: 6px; font-size: var(--fs-sm); color: var(--ink-muted); }
.shareLegend__item { display: inline-flex; align-items: center; gap: 5px; }
.shareSwatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; flex: none; }

/* Households admin — card-based create/edit (P3.12 redesign) */
.hhCard { gap: .5rem; }
.hhCard__head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.hhCard__name { font-weight: 700; }
.hhCard__actions { display: flex; gap: .35rem; flex: none; }
.hhCheckList { display: grid; gap: .4rem; margin: .25rem 0; max-height: 42vh; overflow: auto; }
.hhCheck { display: flex; align-items: center; gap: .5rem; font-weight: 400; }
.hhCheck input { width: 1.1rem; height: 1.1rem; flex: none; }
.hhAdd { margin-top: .25rem; }
.hhAdd__sum { cursor: pointer; color: var(--accent); font-weight: 600; padding: .25rem 0; }

/* P-OPT.4 — Today's Run trip optimizer: the timer-style time picker + the planned route */
.tripControl { display: grid; gap: var(--space-3); }
.timeWheels { display: flex; align-items: center; justify-content: center; gap: var(--space-1); }
.timeWheels__sep { font-size: var(--fs-lg); color: var(--ink-muted); font-weight: 600; }
.wheel { position: relative; width: 4rem; height: 8.25rem; }
.wheel::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 2.75rem; transform: translateY(-50%); border-top: 1px solid var(--control-border); border-bottom: 1px solid var(--control-border); pointer-events: none; }
.wheel__list { height: 100%; overflow-y: auto; scroll-snap-type: y mandatory; scrollbar-width: none; -ms-overflow-style: none; }
.wheel__list::-webkit-scrollbar { width: 0; height: 0; display: none; }
.wheel__list:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius); }
.wheel__pad { height: 2.75rem; } /* (8.25 − 2.75)/2 so the first & last item can center */
.wheel__item { height: 2.75rem; display: flex; align-items: center; justify-content: center; scroll-snap-align: center; font-size: var(--fs-lg); color: var(--ink-muted); font-variant-numeric: tabular-nums; cursor: pointer; }
.wheel__item[aria-selected="true"] { color: var(--ink); font-weight: 700; }
.tripToggle { display: flex; align-items: center; gap: var(--space-2); font-size: var(--fs-sm); }
.tripToggle input { width: 1.1rem; height: 1.1rem; flex: none; }
.tripGo { width: 100%; }
/* (A) timer wheels on the left, "End where I started" toggle to their right (its text wraps). */
.tripControl__top { display: flex; align-items: center; justify-content: center; gap: var(--space-3); flex-wrap: wrap; }
.tripControl__top .timeWheels { flex: none; }
.tripControl__top .tripToggle { flex: 1 1 7rem; min-width: 7rem; }
/* (B) collapsed chip shown once a run is planned — tap to re-open the wheels and re-plan. */
.tripControl--collapsed { padding: var(--space-2); }
.tripSummary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  width: 100%; min-height: 44px; padding: 0.45rem 0.85rem; font: inherit; color: var(--ink);
  background: transparent; border: 1px solid var(--control-border); border-radius: var(--radius-pill); cursor: pointer; touch-action: manipulation;
}
.tripSummary__val { font-weight: 700; }
.tripSummary__edit { font-size: var(--fs-sm); font-weight: 600; color: var(--accent); }
.tripSummary__edit::after { content: ' ›'; }
.tripResults:not(:empty) { margin-top: var(--space-3); }
.tripHead { text-align: center; }
.tripHead__rate { font-size: 1.7rem; font-weight: 800; color: var(--accent); line-height: 1.1; }
.tripHead__sub { font-size: var(--fs-sm); }
.tripProb { margin: var(--space-2) 0; padding: var(--space-2) var(--space-3); font-size: var(--fs-sm); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.tripProb__head { font-weight: 600; margin-bottom: var(--space-2); }
.tripProb__rows { display: grid; gap: 4px; }
.tripProb__row { display: grid; grid-template-columns: 2.6rem 1fr auto; align-items: center; gap: var(--space-2); }
.tripProb__pct { font-variant-numeric: tabular-nums; font-weight: 700; text-align: right; }
.tripProb__bar { height: .55rem; background: var(--line); border-radius: 999px; overflow: hidden; }
.tripProb__fill { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.tripProb__amt { color: var(--ink-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* P-OPT.5 — "how the planner's been doing" accuracy panel (predicted vs actual) */
.tripAcc { margin: var(--space-2) 0; padding: var(--space-2) var(--space-3); font-size: var(--fs-sm); background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius); }
.tripAcc__head { font-weight: 600; margin-bottom: 2px; }
.tripAcc__lead { font-weight: 700; }
.tripAcc__note { margin-top: 2px; }
.tripAcc__sub { font-size: var(--fs-xs); margin-top: var(--space-2); }
.tripAcc__runs { display: grid; gap: 4px; margin-top: 4px; }
.tripAcc__run { display: grid; grid-template-columns: 2.6rem 1fr auto; align-items: center; gap: var(--space-2); font-variant-numeric: tabular-nums; }
.tripAcc__day { color: var(--ink-muted); }
.tripAcc__pa { min-width: 0; }            /* let the coupons column shrink/wrap instead of overflowing at 320px */
.tripAcc__amt { white-space: nowrap; }
.tripStep { display: inline-flex; align-items: center; justify-content: center; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--accent); color: #fff; font-size: var(--fs-xs); font-weight: 700; flex: none; }
