:root {
  /* Label: cream paper, warm ink, one accent (oxblood "seal") that only ever
     means "the price you pay" or "the primary action". No surface steps --
     every panel is the same paper the page sits on, separated by rules and
     space instead of a fill or a shadow. See docs/design-brief.md and
     LABEL_THEME_PLAN.md. */
  /* Hunter (docs/design/brand-brief-2026-09-07.md, chosen 2026-09-07):
     taupe paper, hunter green as the ink, gold kept small. The top bar and
     tab bar are painted --ink with --paper text ("dark bars"); everything
     else keeps the label theme's rules-and-space construction. */
  --paper: #e9e2d3;   /* page and every surface; there are no surface steps */
  --ink: #173a30;      /* text, names, prices, borders of interactive controls, filled dots and meter, the two bars */
  --ink-soft: #2a4a40; /* long-form text: reputation rationale, notes */
  --ink-dim: #5a6a60;  /* out-of-stock bottle names */
  /* 5.96:1 on paper -- the muted role has to clear 4.5:1 at 12px kickers;
     the mockup's #5f6b60 measured 4.33 and was darkened. */
  --muted: #495650;    /* kickers, meta, status lines, placeholder, struck prices */
  --muted-on-ink: #b9c2bb; /* inactive tabs and secondary text on the green bars (6.83:1) */
  /* Decorative only -- row separators. Never the border of a control (every
     control border is --ink). See UX_AUDIT.md UI-06. */
  --rule: #cfc6b3;
  /* Gold, the one accent. Two values because gold that reads on taupe is
     too dark to read on green and vice versa: --seal is the old gold used as
     TEXT on paper (4.66:1: the sale price, "Save n%", warn banners, focus
     rings); --gold is the bright gold used ON --ink (6.16:1: badge lettering,
     primary-button lettering, the active tab, the wordmark kicker). The
     --seal name is kept because every rule in this file that means "the
     price you pay or the primary action" already says it. */
  --seal: #77602a;
  --gold: #d4b26a;
  --error: #8b2f21;    /* error text and the error toast: the rust red, 6.45:1 on paper */
  --dim: rgba(23, 58, 48, .55); /* modal backdrop */
  /* The button system, Hybrid 1 (docs/plans/handoff-browse-and-buttons-2026-09-20.md):
     a control is a soft fill with a 6px corner, never an outline. --soft is
     ink at 8% over paper, written out as a literal so the admin pages' older
     CSS resolves it too; --soft-hover is ink at 14%. Fields keep their
     outline: a field is not a button. */
  --soft: #dcd4c3;
  --soft-hover: #d0c7b5;
  --radius: 6px;

  /* "Playfair Fallback" is Georgia narrowed to Playfair's width, so the
     header lockup does not wrap before the web font lands (design review
     finding 3: the wrap the reviewer saw was the unadjusted fallback). */
  --serif: "Playfair", "Playfair Fallback", "Iowan Old Style", Georgia, serif;
  --sans: "Inter", -apple-system, "Helvetica Neue", Arial, sans-serif;
  /* Type scale for the small tracked-caps labels, which were re-declared at
     every site (design review 2026-09-19, findings 5/13/19). Two sizes:
     kicker (section labels, tab bar, pills) and label (buttons, card
     controls). Caption is the 12px meta line. */
  --font-kicker: 600 11px/1 var(--sans);
  --track-kicker: .12em;
  --font-label: 600 12px/1 var(--sans);
  --track-label: .12em;
  --font-caption: 400 12px/1.3 var(--sans);
  /* Two rules, not three weights: a row rule between items and a section
     rule between blocks. Field borders keep spelling --ink out. */
  --rule-row: 1px solid var(--rule);
  --rule-section: 1px solid var(--ink);
  /* The two bars' heights. Every sticky offset used to repeat the number. */
  --bar-tab: 56px;
  --bar-top: 68px;   /* 8 + 52 mark + 8 */

  color-scheme: light;
}

@font-face {
  font-family: "Playfair Fallback";
  src: local("Georgia"), local("Times New Roman");
  size-adjust: 88%;
  ascent-override: 100%;
}
@font-face {
  font-family: "Playfair";
  src: url("fonts/PlayfairDisplay-400.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Playfair";
  src: url("fonts/PlayfairDisplay-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("fonts/Inter-400.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

/* Branded focus ring -- the UA default was already visible and already
   passes SC 2.4.7/2.4.11 (see KB-03), so this is polish, not a fix.
   :focus-visible, not :focus, so it shows for keyboard/programmatic focus
   but not a mouse click. --seal clears 8.07:1 against paper, and modern
   browsers follow the focused element's own shape automatically.
   See KB-03/E3. */
:focus-visible {
  outline: 2px solid var(--seal);
  outline-offset: 2px;
}

html, body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 14px/1.45 var(--sans);
}

/* The document scrolls, normally. An earlier fix for a drifting tab bar made
   <main> the only scroll container (body: 100dvh, overflow hidden), which did
   weld the bar down but cost the native feel: no rubber-band overscroll, and in
   Safari the URL bar never auto-hides, permanently eating a strip of screen.
   Jason uses both Safari and the installed PWA and wants normal page scrolling,
   so the document scrolls again and .tabbar is position:fixed once more.

   Of the two things that made the bar drift, the flapping safe-area inset is
   killed properly below (see .tabbar). The other -- Safari resizing the layout
   viewport as its toolbar collapses -- is inherent to position:fixed in a
   browser tab, and does not exist in the standalone PWA, which has no toolbar. */
body {
  padding-bottom: calc(var(--bar-tab) + env(safe-area-inset-bottom));
  padding-top: env(safe-area-inset-top);
}

/* Bottle names: a display serif, one size per context (section 2 of
   LABEL_THEME_PLAN.md) rather than one shared size across every list/row --
   Detail's headline is meant to read as larger than a card's. An age
   statement (wrapped in .age by displayName()) sets in italic in every
   context. See TY-01/B7. */
.p-name, .bottle-name, .wl-name { font-family: var(--serif); font-weight: 400; margin: 0; }
.p-name { font-size: 22px; line-height: 1.15; margin: 0 0 10px; }
.bottle-name { font-size: 30px; line-height: 1.08; margin: 0 0 14px; }
.wl-name { font-size: 20px; line-height: 1.15; margin: 0 0 6px; padding-right: 20px; }
.age { font-style: italic; }
.bottle-name:focus { outline: none; }

/* Prices and distances should align in a list — tabular figures, not
   proportional ones. `.small` is included broadly (tabular-nums is a no-op
   on non-digit text, so this is safe everywhere it's used, not just the
   numeric instances) rather than hunting down every specific numeric-
   bearing element individually. See TY-05/C15. */
.p-price, .p-was, .p-meta, .p-price-line, .p-caption, .store-row .dist,
.p-stock, .small, .status {
  font-variant-numeric: tabular-nums;
}

/* Section labels inside Bottle Detail's cards (Availability, Price,
   Reputation, Nearby stores) -- a small-caps-ink "section label" treatment,
   standalone (no dependency on any other rule). See SR-11/C2. */
.card-heading {
  margin: 0 0 8px;
  color: var(--muted);
  font: var(--font-label);
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 20px;
  position: sticky;
  top: 0;
  /* Dark bar: painted --ink with --paper text, the slate roof on the
     whitewashed wall. No rule needed; the fill is the edge. */
  background: var(--ink);
  color: var(--paper);
  z-index: 5;
}
/* body's padding-top reserves the safe area only at scroll 0 -- once
   .topbar is actually stuck, `top: 0` sticks it to the literal viewport
   top, under the notch/status-bar overlay, the same class of problem as
   .tabbar's bottom inset above (and the same fix: only in standalone, where
   there's no browser toolbar already occupying that space). INFERRED --
   not run in an installed PWA on a notched device; verify on device. See
   VC-11/D7. */
@media (display-mode: standalone) {
  .topbar { top: env(safe-area-inset-top); }
}
.topbar h1 { font: 400 26px/1 var(--serif); margin: 0; }
.topbar h1 a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 14px; }
/* The lockup: the BH mark, the name, and the gold kicker beneath it (brand
   brief wordmark option 4, "Rare whisky finder"). */
/* The mark at 52px, scaled to the wordmark rather than a size under it
   (owner's call on the review board, 2026-09-21). The bar is 68px:
   --bar-top carries that to every sticky offset. */
.topbar .mark { width: 52px; height: 52px; display: block; flex: none; }
.topbar .wordmark { display: flex; flex-direction: column; gap: 4px; }
/* The lockup never wraps: with "Set location" in the pill a 390px bar has
   no room for the 52px mark and a 26px name, so under 420px the mark drops
   to 44px and the name to 22px (the bar stays 68px: 12 + 44 + 12). Under
   360px the gold kicker goes too. */
.topbar .wordmark > span { white-space: nowrap; }
.loc-pill { white-space: nowrap; }
@media (max-width: 419px) {
  .topbar { padding: 12px 20px; }
  .topbar h1 { font-size: 22px; }
  .topbar h1 a { gap: 12px; }
  .topbar .mark { width: 44px; height: 44px; }
}
@media (max-width: 374px) {
  .loc-pill { font-size: 10px; letter-spacing: .08em; padding: 0 8px; }
}
@media (max-width: 364px) {
  .topbar { padding: 14px 20px; }
  .topbar h1 { font-size: 20px; }
  .topbar h1 a { gap: 10px; }
  .topbar .mark { width: 40px; height: 40px; }
  /* The kicker is the widest line of the lockup at this size; it goes. */
  .topbar .kicker { display: none; }
  .topbar .wordmark { gap: 0; }
}
@media (max-width: 339px) {
  .topbar { padding: 16px 20px; }
  .topbar h1 a { gap: 8px; }
  .topbar .mark { width: 36px; height: 36px; }
  .loc-pill { padding: 0 6px; }
}
.topbar .kicker {
  font: 700 9px/1 var(--sans);
  letter-spacing: .32em;
  text-transform: uppercase;
  color: var(--gold);
}

main { padding: 20px; max-width: 640px; margin: 0 auto; }
.screen[hidden] { display: none; }
/* Every screen names itself -- the header always says "Bottle Hunter", and
   the tab bar's active-state colour was the only other cue. Focus lands here
   on navigation (app.js), so it also needs to be programmatically focusable. */
.screen-title { font: 400 26px/1.1 var(--serif); margin: 22px 0 12px; }
.screen-title:focus { outline: none; }

/* Form controls take the app's own type stack, not the browser's control
   font -- see TY-03. 16px everywhere (never less) so focusing a control in
   iOS Safari never triggers its under-16px zoom-to-fit behaviour. */
input, select, textarea {
  background: var(--paper);
  border: 1px solid var(--ink);
  color: var(--ink);
  border-radius: 0;
  padding: 12px;
  font: 400 16px/1.45 var(--sans);
  width: 100%;
  min-height: 44px;
}
/* A native select's own arrow is replaced with an ink chevron drawn as a
   data: URI -- CSP-allowed for images (the live Caddy block sends
   default-src 'self' with no img-src exception needed for data:), and it
   keeps the control's fill/border matching every other form field instead
   of falling back to the OS's own control chrome. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23173a30' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 10px 7px;
  padding-right: 32px;
}
select:disabled { opacity: .55; }
/* --muted clears 6:1 on paper. */
.modal input::placeholder {
  color: var(--muted);
  opacity: 1;
}
button {
  cursor: pointer;
  font: var(--font-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  background: var(--soft);
  color: var(--ink);
  border: none;
  border-radius: var(--radius);
  padding: 15px 16px;
  min-height: 44px;
}
/* The Watchlist push opt-in stands alone under the list, full width. */
#enable-push { width: 100%; }
button.mini {
  background: var(--soft);
  color: var(--ink);
  padding: 8px 10px;
  font-size: 12px;
  /* 44px: the Apple HIG / WCAG 2.2 AAA hit-region floor. The button is
     visually shorter (about 40px) than that; the extra hit area comes from
     this min-height rather than more padding, so the label doesn't look
     oversized next to a card's other text. See UX_AUDIT.md UI-01. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
button.back {
  padding: 4px 0;
  margin-bottom: 8px;
  font: var(--font-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
/* The button system (design review 2026-09-19, finding 5; soft fills from
   the Hybrid 1 pass, 2026-09-20). The base `button` rule is the secondary
   (soft fill, caps). .btn-primary is the ink fill with gold text. .btn-quiet
   is a text button (Back, Later). .btn-on-ink is a secondary drawn on an
   ink bar (the Detail foot): paper at 14% over the ink.
   Every former one-off (push opt-in, Filter's Show, Review's Same, the
   modal's confirming button) composes these instead of restating them.
   Declared after button.mini and button.back, with the element in the
   selector, so a variant always wins over the size modifiers. Apple's and
   Google's sign-in buttons follow their own brand rules and stay apart. */
button.btn-primary, .btn-primary { background: var(--ink); color: var(--gold); }
button.btn-quiet, .btn-quiet, button.back { background: none; border: none; color: var(--muted); padding-left: 0; padding-right: 0; }
button.btn-on-ink, .btn-on-ink { background: rgba(233, 226, 211, .14); color: var(--paper); }
/* The destructive secondary: the soft fill with the error red as text,
   never a red fill. One per sheet, below a rule, after the safe actions. */
button.btn-danger, .btn-danger { background: var(--soft); color: var(--error); }
/* Pressed, on touch as well as with a pointer (the hover rules below only
   apply with a pointer): the secondary darkens to ink at 14% over paper, the
   primary to --ink-soft. The text controls that draw no fill opt out, with
   the element in the selector so they outrank the generic rule. */
button:active:not(:disabled) { background: var(--soft-hover); }
button.btn-primary:active, .btn-primary:active { background: var(--ink-soft); color: var(--paper); }
button.btn-on-ink:active, .btn-on-ink:active, .detail-foot-watch button.mini:active, button.rv-bar-clear:active, button.loc-pill:active { background: rgba(233, 226, 211, .22); }
button.rv-bar-btn:active { background: var(--paper); }
button.btn-quiet:active, button.back:active, button.p-watch:active, .tabbar button:active, button.find-clear:active, button.modal-x:active, button.rv-name:active, button.rv-undo:active { background: none; }

/* Bottle Detail's cards (Availability, Price, Reputation, About this bottle,
   Also sold as, Nearby stores). */
.card { padding: 18px 0; border-bottom: var(--rule-row); margin: 0; }
/* The Watchlist push opt-in below the list -- a stronger --ink rule, not
   --rule, since it marks a transition from list to action rather than just
   organizing within one screen; the 8px margin keeps it from sitting flush
   against the last .wl-item's own bottom rule. */
.push { border-top: var(--rule-section); padding-top: 18px; margin-top: 8px; }
.push #push-note { margin: 10px 0 0; }

.muted { color: var(--muted); }
.small { font-size: 13px; line-height: 1.45; }

/* Visually hidden but available to assistive tech -- for labels whose text
   the sighted layout already conveys another way (a placeholder, an icon). */
.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;
}

/* Status/count lines read as kickers (small, tracked, uppercase) -- .err
   overrides back to a plain sentence in --seal, since an error message
   mangled into all-caps is harder to read, not easier. */
.status {
  margin: 14px 0 4px;
  min-height: 18px;
  color: var(--muted);
  font: 600 12px/1.3 var(--sans);
  letter-spacing: .12em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  gap: 8px;
}
.status.err {
  color: var(--error);
  text-transform: none;
  letter-spacing: normal;
  font: 400 14px/1.4 var(--sans);
}

.banner {
  border-top: var(--rule-section);
  border-bottom: var(--rule-section);
  padding: 14px 0;
  margin: 14px 0;
  font: 400 15px/1.5 var(--sans);
}
.banner.warn, .banner.miss { border-color: var(--seal); }
.acts { display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
/* Bottle Detail's action row (Watch) leads its card, so the card's own
   padding is all the space it needs above it. */
.card > .acts:first-child { margin-top: 0; }
/* "Also sold as" rows on Bottle Detail: the other retailer's listing on the
   left, its Same/Different (or Unlink) actions on the right, wrapping on
   narrow screens. */
.link-row { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; padding: 8px 0; border-bottom: var(--rule-row); }
.link-row:last-child { border-bottom: 0; }
.link-row .acts { margin-top: 0; }
/* The matcher's provenance on an "Also sold as" row: an inline "Auto-linked ·
   93%" kicker, then the model's one-line reason on its own line. */
.link-auto { white-space: nowrap; }
.link-reason { display: block; }
.banner .acts { margin-top: 12px; }

.product {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 0 12px;
  border-bottom: var(--rule-row);
  margin: 0;
  /* The whole card opens detail; only the button inside it stops that
     propagation to do its own thing. See UX_AUDIT.md UI-04. */
  cursor: pointer;
  position: relative;
}
/* Out of stock nearby: recede the name and stock line only, still fully
   tappable -- nothing dims by opacity (opacity previously alpha-blended
   text down below the AA floor, see VC-03). */
.product.is-out .p-name { color: var(--ink-dim); }
.p-name { margin: 0 0 10px; padding-right: 20px; }
/* Name row: the name and the star (design review findings 7, 8). */
.p-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.p-head .p-name { margin: 0 0 4px; padding-right: 0; min-width: 0; }
/* Pressed: the row fills with the paper's darker tone, the same cue the
   Similar rows use, since the chevron is gone. */
.product:active { background: color-mix(in srgb, var(--ink) 10%, var(--paper)); margin: 0 -8px; padding-left: 8px; padding-right: 8px; }
/* The chevron: the card's (and the watchlist row's) only affordance that a
   tap opens Detail or runs the query. `content` carries an empty alt string
   so VoiceOver, which otherwise reads generated text, treats it as
   decorative. See CARD_ACTIONS_PLAN.md, WATCHLIST_ROWS_PLAN.md. */
/* (The chevron is gone from cards and watch rows alike: the row is the
   control, the star its one separate button. Design review findings 8, 11.) */
.p-meta { color: var(--muted); font: 400 13px/1.45 var(--sans); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
/* The card's price line (design review finding 4): price, then "was" and
   the badge, then the Watch control at the right end. The caption under it
   is the 12px meta: size and strength, later the nearest store. */
.p-price-line { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; color: var(--muted); font: 400 13px/1.45 var(--sans); }
.p-caption { color: var(--muted); font: var(--font-caption); }
.p-caption[hidden] { display: none; }
/* Price leads the meta line; it would otherwise inherit the muted style. */
.p-price { color: var(--ink); font: 600 18px/1 var(--sans); }
.p-price-line .p-price { font-size: 20px; }
.p-price.on-sale { color: var(--seal); }
.p-was { color: var(--muted); text-decoration: line-through; font: 400 13px/1 var(--sans); }
.p-stock { font: 400 14px/1.3 var(--sans); }
.p-stock.out { color: var(--muted); }
/* The card's one action: a text control at the right end of the price line,
   not a boxed button. min-height gives the 44px hit area while the visible
   text stays kicker-sized -- same trick button.mini uses (comment above). */
.p-watch {
  margin-left: auto;
  background: transparent;
  border: none;
  border-radius: 0;   /* a text control, not a button: no fill, no corner */
  color: var(--muted);
  font: var(--font-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  padding: 10px 0 10px 12px;
  min-height: 44px;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}
/* On a card the star stands alone: a 44px square hit area hugging the
   card's top right corner, the glyph at 20px, no tracking. */
.p-head .p-watch { flex: none; width: 44px; height: 44px; margin: -10px -12px -10px 0; padding: 0; justify-content: center; font-size: 20px; letter-spacing: 0; }
.p-watch.is-on { color: var(--ink); }
/* The hidden attribute loses to the display:flex above; a non-owner card
   (api/auth.py tester or anonymous) must not show an inert Watch. */
.p-watch[hidden] { display: none; }
/* The watchlist star's first-tap state -- "Remove?" in seal, the same
   destructive-confirm colour every other tap-to-confirm control uses. */
.p-watch.confirm { color: var(--error); }
/* 51x17px on its own otherwise, and the spacing exception can't apply --
   see TT-01. min-height gives it the 44px hit area on Detail, where it
   stands alone (no enclosing tap target to protect). */
.p-view {
  color: var(--ink);
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Retailer badge — marks a card or store row as Goody Goody's or as carried
   by more than one retailer. Outlined square (the shape the card-level Reputation badge used before it was retired);
   Spec's-only cards/rows get no badge at all, since Spec's is the default
   and an "in stock" flag for the common case is noise, not information. */
.retailer-badge {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  background: transparent;
  border: 1px solid var(--ink);
  color: var(--ink);
  border-radius: 3px;   /* a badge, not a button: the outline stays */
  padding: 2px 8px;
  font: 600 11px/1.3 var(--sans);
  letter-spacing: .12em;
  text-transform: uppercase;
}


details.more { margin: 14px 0; }
details.more > summary {
  cursor: pointer;
  list-style: none;
  color: var(--ink);
  font: 600 12px/1.3 var(--sans);
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 14px 0 4px;
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 8px;
}
details.more > summary::-webkit-details-marker { display: none; }
details.more > summary::before { content: "▾"; color: var(--muted); transition: transform .15s; }
details.more[open] > summary::before { transform: rotate(180deg); }

.tabbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  display: flex;
  background: var(--ink);   /* the second dark bar; see .topbar */
  z-index: 10;
}
/* The safe-area inset is applied ONLY in the installed PWA, and that is the
   whole trick. In a Safari tab the inset flips between 0 and ~34px as the
   toolbar collapses, and spending it as padding here made the BAR'S OWN HEIGHT
   grow and shrink -- its top edge visibly moving, which is what "the nav bar
   moves up and down" actually was. Safari's own toolbar already occupies that
   space, so omitting it there is correct as well as stable. In standalone there
   is no toolbar, the inset is a constant, and it is needed to clear the home
   indicator. */
@media (display-mode: standalone) {
  .tabbar { padding-bottom: env(safe-area-inset-bottom); }
}
.tabbar button {
  flex: 1;
  background: none;
  color: var(--muted-on-ink);
  border: none;
  border-radius: 0;
  padding: 16px 0;
  font: var(--font-kicker);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  min-height: 44px;
}
/* Colour alone was the only cue for the active tab -- fails 1.4.1 for a
   sighted user who can't rely on hue, and VoiceOver read every tab button
   identically regardless of state (aria-current is set in app.js). An inset
   bottom rule gives a second, colour-independent signal. */
.tabbar button.active {
  color: var(--gold);
  box-shadow: inset 0 -3px 0 0 var(--gold);
}

/* Score hero — Bottle Detail's centerpiece for a 1-10 number (Availability,
   Reputation). Large italic serif numeral, a 1-10 segmented meter, a line of
   long-form text below. Formerly the Rarity hero; renamed when Rarity was
   retired since Availability already shared this shape and Reputation now
   does too -- not a restyle, the same visual language for every big-number
   card. */
.score-hero { display: flex; flex-direction: column; gap: 10px; }
/* The flex column's default align-items:stretch is right for the meter bar
   and the rationale paragraph, but it also stretched "Re-check" to the full
   card width -- every other mini button in the app is content-width. See
   TY-06/D8. */
.score-hero > button { align-self: flex-start; }
.score-hero-top { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.score-number { font: italic 400 64px/1 var(--serif); color: var(--ink); font-variant-numeric: tabular-nums; }
.score-hero-max { color: var(--muted); font: 400 14px/1 var(--sans); }
/* Reputation's band label ("widely admired") on the same baseline as the
   number -- the one-point rule means the label matters as much as the digit. */
.score-hero-band { color: var(--ink-soft); font: 400 14px/1 var(--sans); }
.score-meter { display: flex; gap: 4px; }
.score-meter i {
  flex: 1;
  height: 6px;
  border: 1px solid var(--ink);
  font-style: normal;
}
.score-meter i.fill { background: var(--ink); }
.score-hero-rationale { font: 400 15px/1.5 var(--sans); color: var(--ink-soft); margin: 0; }
.score-hero-meta { color: var(--muted); }

/* Per-store rows on Bottle Detail. */
.store-row {
  padding: 12px 0;
  border-bottom: var(--rule-row);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.store-row:last-child { border-bottom: none; }
.store-row-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.store-row-name { font: 600 15px/1.2 var(--sans); color: var(--ink); }
.store-row.is-out .store-row-name, .store-row.is-out .store-row-status { color: var(--muted); }
.store-row-dist { color: var(--muted); font-size: 14px; }
.store-row-hours { font-size: 13px; color: var(--muted); }
.store-row-hours.today { color: var(--ink); font-weight: 600; }
.store-row-links { display: flex; gap: 16px; margin-top: 2px; }
/* "Store page" / "Directions" / the phone number were 12px text with no
   padding (17px tall) -- see TT-03. The 16px gap already clears the 24px
   spacing exception, so only height needs raising to reach the 44px floor. */
.store-row-links a {
  color: var(--ink);
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.store-row-phone {
  color: var(--muted);
  font-size: 13px;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--ink); margin-right: 8px; vertical-align: 1px; }
.dot.out { background: transparent; border: 1px solid var(--muted); }
/* Text equivalent alongside the dot -- stock state must not be colour-only
   (WCAG 1.4.1). See UX_AUDIT.md A11Y-05. */
.store-row-status { font-size: 13px; }

/* Zero-results state -- an empty region should read as an answer, not a
   failure. See UX_AUDIT.md UI-10. */
.empty-state { text-align: center; padding: 40px 16px; }
.empty-state p { margin: 0 0 16px; font: italic 400 20px/1.3 var(--serif); color: var(--ink); }
/* The row is the control (tap searches the watch's query), the star is the
   way out -- same shape as a result card, no separate actions row. See
   WATCHLIST_ROWS_PLAN.md. */
.wl-item {
  padding: 14px 0;
  margin: 0;
  border-bottom: var(--rule-row);
  position: relative;
  cursor: pointer;
}
.wl-meta { display: flex; justify-content: space-between; align-items: center; margin: 4px 0 0; font: 400 13px/1.45 var(--sans); color: var(--muted); }
/* The watch row's state line (design review finding 11): near = filled
   dot and bold, far = hollow dot, waiting = no dot and muted. */
.wl-state { display: flex; align-items: center; margin-top: 2px; font: 400 14px/1.3 var(--sans); color: var(--ink); }
.wl-state.near { font-weight: 600; }
.wl-state.waiting { color: var(--muted); }
.wl-state .dot { flex: none; }
.dot.none { background: transparent; border: none; }
.wl-sub { margin-top: 3px; padding-left: 16px; font: var(--font-caption); color: var(--muted); }
.p-head .wl-name { margin: 0 0 4px; padding-right: 0; min-width: 0; }
/* The star's confirm step reads "Remove?" in the error red; it widens for
   the word and snaps back to the glyph. */
.p-head .p-watch.confirm { width: auto; padding: 0 8px; font: var(--font-label); letter-spacing: var(--track-label); }
.spin { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--rule); border-top-color: var(--ink); border-radius: 50%; animation: spin .7s linear infinite; vertical-align: -2px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* toast */
/* `left: 50%` with no explicit width shrink-to-fits into roughly the
   remaining half of the viewport, not the full available width -- an
   18-word message wrapped to 5 lines in a 188px box. `width: max-content`
   sizes to the text (up to max-width) instead. See MT-03/D12. */
#toast {
  position: fixed;
  left: 50%;
  bottom: calc(80px + env(safe-area-inset-bottom));
  transform: translate(-50%, 20px);
  width: max-content;
  max-width: min(90vw, 420px);
  background: var(--ink);
  color: var(--paper);
  border-radius: 0;
  padding: 14px 16px;
  font: 400 14px/1.4 var(--sans);
  text-align: center;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .25);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s, transform .2s;
  z-index: 100;
}
#toast.show { opacity: 1; transform: translate(-50%, 0); }
/* 8.07:1 -- replaces the old white-on-red toast, which measured 3.29:1 and
   failed AA. */
#toast.err { background: var(--error); color: var(--paper); }

/* inline prompt */
.modal-back {
  position: fixed; inset: 0;
  background: var(--dim);
  display: flex; align-items: center; justify-content: center;
  padding: 24px; z-index: 60;
}
.modal {
  background: var(--paper);
  border: 1px solid var(--ink);
  border-radius: var(--radius);   /* one family with the buttons and the desktop sheet */
  padding: 20px;
  width: 100%; max-width: 340px;
  display: flex; flex-direction: column; gap: 10px;
  /* The one shadow besides the toast, per section 2 of LABEL_THEME_PLAN.md --
     a dialog floating over a dimmed backdrop still needs edge separation
     that a hairline border alone doesn't give at a glance. */
  box-shadow: 0 8px 28px rgba(0, 0, 0, .25);
}
.modal h3, .modal h2 { font: 400 22px/1.15 var(--serif); margin: 0 0 8px; }
.modal label { font-size: 13px; color: var(--muted); }
.modal input {
  background: var(--paper);
  border: 1px solid var(--ink);
  color: var(--ink);
  border-radius: 0;
  padding: 11px;
  font: 400 16px/1.45 var(--sans);
}
.modal .acts { justify-content: flex-end; margin-top: 4px; }
/* The confirming action and Cancel were visually identical button.mini
   elements -- see NV-05/B4. Seal fill marks the confirming button as the
   primary action, the same treatment used everywhere else in the app. */
/* (button.mini.primary became .btn-primary; see the button system above.) */

/* ── Sale pricing ─────────────────────────────────────────────────────────────
   The sale price takes the normal .p-price slot so the eye lands on what you
   would actually pay; the list price sits beside it, struck through and muted,
   because "was $2,105" is the part that makes a deal legible. Save n% carries
   the percentage rather than the word alone -- 47% off is information, "SALE"
   is decoration. No fill, no pill: the same treatment .p-price.on-sale gets,
   in --seal, since a badge would be a second accent colour. */
.sale-badge[hidden] { display: none; }
.sale-badge {
  font: var(--font-label);
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
  color: var(--seal);
}

/* The four animations in the app -- the loading spinner, the toast's
   slide/fade, the disclosure chevron's rotation, and a card's opacity fade
   -- are all small and short, but none checked prefers-reduced-motion. The
   spinner freezes rather than disappearing entirely: it's always paired
   with "Loading…" text, so a static ring still reads as a loading
   indicator. See MT-02/D11. */
@media (prefers-reduced-motion: reduce) {
  .spin { animation: none; }
  .skel-line { animation: none; }
  #toast { transition: none; }
  details.more > summary::before { transition: none; }
  .product { transition: none; }
  .detail-foot { transition: none; }
}

/* ── Find ─────────────────────────────────────────────────────────────────
   docs/plans/handoff-find-screen-2026-09-07.md. Reuses .product, the .p-
   classes, .rare-badge, .status, .banner and .modal-back wherever find.js
   does (never write "p-star-slash" literally in a comment: the star-slash
   closed this comment early and the parser ate the .loc-pill rule below,
   which stayed silently unstyled from slice 2 until the Hunter repaint); the classes
   below are new shapes the mockups draw that nothing above already covers
   (shelves, the typeahead list, the Filter/Sort/Where bottom sheets, the
   Filter|Sort bar and its applied chips). */

.loc-pill {
  display: inline-flex; align-items: center; gap: 6px;
  border: none; border-radius: var(--radius); min-height: 32px; padding: 0 10px;
  background: rgba(233, 226, 211, .12); color: var(--paper);   /* soft paper on the dark top bar */
  font: var(--font-kicker); letter-spacing: var(--track-kicker); text-transform: uppercase;
  white-space: nowrap; cursor: pointer;
}
.loc-pill[hidden] { display: none; }
/* 44px hit areas for the small controls that sit in fixed-height bars and
   text lines (design review finding 15): the area grows, the drawing does
   not, so the 68px top bar and the meta lines keep their rhythm. */
.loc-pill, .shelf-head .see, .store-row-links a, .topbar h1 a { position: relative; }
.loc-pill::before, .shelf-head .see::before, .store-row-links a::before, .topbar h1 a::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 44px; margin-top: -22px; }

.find-field {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--ink); border-radius: var(--radius); min-height: 48px; padding: 0 14px;
  margin-bottom: 8px;
}
.find-field:focus-within { outline: 2px solid var(--seal); outline-offset: 2px; }
.find-field .find-icon { display: inline-flex; color: var(--ink); flex: none; }
.find-field input {
  flex: 1; min-width: 0; border: none; background: transparent; padding: 0;
  font: 400 16px/1.45 var(--sans); color: var(--ink); min-height: auto;
}
.find-field input:focus { outline: none; }
/* Chrome's native type=search cancel-× duplicates our own .find-clear. */
.find-field input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.find-clear {
  border: none; background: none; color: var(--muted); padding: 0;
  min-height: 32px; width: 24px; font-size: 15px; cursor: pointer;
}
.find-clear[hidden] { display: none; }
.find-coverage[hidden] { display: none; }
.find-coverage { margin: -4px 0 14px; }

/* Storefront shelves (V1). */
.shelf-head { display: flex; justify-content: space-between; align-items: baseline; margin: 22px 0 10px; }
.shelf-empty p { margin: 0; }
/* Skeleton rails while the storefront loads (find.js storefrontSkeleton):
   the same .shelf-head / .shelf / .mini boxes the real rails use, with
   quiet --rule bars where text will be. The pulse is opacity only. */
.skel-line { background: var(--rule); height: 12px; animation: skel-pulse 1.4s ease-in-out infinite alternate; }
.skel-title { width: 58%; height: 20px; }
.skel-see { width: 48px; height: 11px; }
.skel-tile { cursor: default; min-height: 150px; }
.skel-badge { width: 64px; height: 14px; margin-bottom: 6px; }
.skel-name { width: 90%; height: 14px; }
.skel-name.short { width: 60%; }
.skel-price { width: 44%; height: 16px; margin-top: 4px; }
.skel-where { width: 70%; height: 11px; }
.find-browseall.skel-block { cursor: default; }
@keyframes skel-pulse { from { opacity: .55; } to { opacity: 1; } }
.shelf-head h3 { font: 400 20px/1.15 var(--serif); margin: 0; }
.shelf-head .see {
  font: var(--font-kicker); letter-spacing: var(--track-kicker); text-transform: uppercase;
  color: var(--ink); text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: 3px;
  white-space: nowrap; cursor: pointer;
}
.shelf { display: flex; gap: 12px; overflow-x: auto; margin: 0 -20px; padding: 0 20px 4px; -webkit-overflow-scrolling: touch; }
.mini {
  flex: none; width: 168px; background: rgba(23, 58, 48, .04); border-radius: var(--radius); padding: 12px;
  display: flex; flex-direction: column; gap: 6px; cursor: pointer;
}
.mini h4 { font: 400 16px/1.2 var(--serif); margin: 0; min-height: 38px; color: var(--ink); }
.mini .price { font: 600 16px/1 var(--sans); color: var(--ink); }
.mini .price.on-sale { color: var(--seal); }
.mini .where { font: 400 12px/1.3 var(--sans); color: var(--muted); }
/* The struck "was" price shares the where-line; .p-was carries its own 13px
   from the card rules, which reads a size too large tucked inside a tile. */
.mini .where .p-was { font-size: 12px; }
.shelf-badge {
  display: inline-flex; align-self: flex-start; align-items: center;
  background: var(--ink); border: 1px solid var(--ink); border-radius: 3px; padding: 2px 7px;
  font: 600 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold);
}
.shelf-badge[hidden] { display: none; }
/* The card's one mark (RARE at 8+, ONE STORE at 10; app.js setRareBadge).
   Its rule went out with the Browse CSS in slice 3 (420a48a) and the badge
   rendered as bare text until the Hunter repaint put it back: same anatomy
   as .shelf-badge, one size up for the card's meta line. */
.rare-badge {
  display: inline-flex;
  align-items: center;
  background: var(--ink);
  border: 1px solid var(--ink);
  color: var(--gold);
  border-radius: 3px;
  padding: 2px 8px;
  font: var(--font-label);
  letter-spacing: var(--track-kicker);
  text-transform: uppercase;
}
.rare-badge[hidden] { display: none; }
.find-browseall {
  margin-top: 22px; background: var(--soft); border-radius: var(--radius); padding: 14px 16px;
  display: flex; justify-content: space-between; align-items: baseline; cursor: pointer;
}
.find-browseall span:first-child { font: 400 18px/1.2 var(--serif); }
.find-chev { color: var(--muted); font: 300 22px/1 var(--sans); }

/* Typeahead (V2). */
.sug {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 52px; border-bottom: var(--rule-row);
  font: 400 16px/1.3 var(--sans); color: var(--ink); cursor: pointer;
}
.sug.muted-sug { color: var(--muted); }
.sug .txt { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sug .sub { font: 400 13px/1.3 var(--sans); color: var(--muted); white-space: nowrap; flex: none; }

/* Results (V3): the Filter|Sort bar, applied chips. Cards themselves reuse
   .product/.p-* via browseProductNode/arbitrageRowNode. */
.find-bar {
  display: grid; grid-template-columns: 1fr 1fr; margin-top: 8px;
  background: var(--soft); border-radius: var(--radius);
  /* Sticks under .topbar (--bar-top: 8px pad + 52px mark + 8px pad) while
     results scroll underneath. Measured, not derived: a 3px mismatch shows
     as a sliver of scrolling content between the bars. */
  position: sticky; top: var(--bar-top); z-index: 4;
}
/* .find-field always precedes .find-results in the DOM -- the other views
   between them are display:none when hidden, so this bar is always visually
   adjacent to the field. Weld the two into one block per the mockup instead
   of leaving the two 8px margins as a gap: the bar hangs off the field's
   bottom edge with square top corners, and the field squares its own bottom
   corners while the results view is up so the seam is a straight line. */
.find-field ~ .find-results .find-bar { margin-top: -8px; border-top-left-radius: 0; border-top-right-radius: 0; }
.find-field:has(~ .find-results:not([hidden])) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.find-bar-btn {
  background: none; border: none; border-radius: var(--radius);
  min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px;
  font: var(--font-label); letter-spacing: var(--track-kicker); text-transform: uppercase; color: var(--ink);
}
.find-bar-btn .n {
  display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px;
  border-radius: 9px; background: var(--ink); color: var(--paper); font: 600 11px/1 var(--sans); letter-spacing: 0;
}
.find-bar-btn .n[hidden] { display: none; }
.find-sort-value { color: var(--muted); text-transform: none; letter-spacing: normal; font-weight: 500; }
/* Applied chips wrap onto as many rows as they need (three filters plus
   Clear overflow a phone width on one row), and the row keeps clear air
   below it so the count kicker never sits against the chips. An empty row
   collapses, margins and all. */
.applied { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 18px; }
.applied:empty { display: none; }
.applied > div {
  display: inline-flex; align-items: center; gap: 8px; min-height: 34px; padding: 0 10px;
  background: var(--ink); color: var(--paper); border-radius: var(--radius);
  font: 500 13px/1 var(--sans); white-space: nowrap; cursor: pointer;
}
.applied > div span { font-size: 15px; line-height: 1; }
.applied > div.clear {
  background: transparent; color: var(--ink); border: 1px solid transparent;
  text-decoration: underline; text-underline-offset: 3px; padding: 0 4px;
}
.find-hidden-kicker { text-transform: uppercase; letter-spacing: .1em; font-size: 11px; }
/* The count kicker ("1,193 bottles near you") sat flush under the Filter|Sort
   bar's bottom rule; .card-heading's own margin is bottom-only. */
.find-count { margin-top: 16px; }
.find-more button { width: 100%; }
.find-widened:empty, .find-widened[hidden] { display: none; }

/* Bottom sheets (Where/Filter/Sort): a variant of .modal-back/.modal
   anchored to the bottom edge, full width, no centered dialog box. */
.sheet-back { align-items: flex-end; padding: 0; }
.sheet {
  width: 100%; max-width: 640px; margin: 0 auto;
  background: var(--paper); border: none; border-top: var(--rule-section);
  border-radius: 12px 12px 0 0;
  padding: 18px 20px calc(20px + env(safe-area-inset-bottom));
  max-height: 85vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
  box-shadow: 0 -8px 28px rgba(0, 0, 0, .18);
}
.sheet-top { display: flex; justify-content: space-between; align-items: baseline; }
.sheet-title { font: 400 22px/1.1 var(--serif); }
.sheet-action { cursor: pointer; }
.sheet-scroll { max-height: 50vh; overflow-y: auto; }
.sheet-locline { margin: -6px 0 0; }
.sheet-locline a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.opt {
  display: flex; align-items: center; justify-content: space-between; min-height: 44px;
  border-bottom: var(--rule-row); font: 400 16px/1.3 var(--sans); color: var(--ink); cursor: pointer;
}
.opt .sub { font: 400 13px/1.3 var(--sans); color: var(--muted); margin-left: 8px; }
.opt.sel { font-weight: 600; }
.opt .n { font: 400 13px/1.3 var(--sans); color: var(--muted); }
.opt .box {
  width: 20px; height: 20px; border: 1px solid var(--ink); border-radius: 3px; display: inline-flex;
  align-items: center; justify-content: center; margin-right: 12px; flex: none;
  font: 700 13px/1 var(--sans); color: var(--paper); box-sizing: border-box;
}
/* Checked: ink fill with a paper tick, the same fill the slider thumbs and
   the rarity meter use (design review finding 6). */
.opt .box.on { background: var(--ink); }
.opt .l { display: flex; align-items: center; }
.sheet.is-loading .opt .n { opacity: .4; }
.sec { display: flex; flex-direction: column; gap: 2px; }
.sec .card-heading { margin-bottom: 6px; }
.sec-head { display: flex; justify-content: space-between; align-items: baseline; }

/* Two overlapping native range inputs -- a native, functional double-thumb
   slider rather than a custom pointer-drag control. Restyled for the design
   review (finding 6): a 4px track in --rule with the selected span filled
   ink (.range-fill, positioned by find.js), and solid ink thumbs with a
   paper ring, the rarity meter's language rather than the browser's. */
.price-range { position: relative; height: 32px; margin: 6px 4px 0; }
/* The track is drawn once here, under both inputs, whose own tracks are
   transparent: otherwise the first input's track would paint over the fill. */
.price-range::before { content: ""; position: absolute; top: 14px; height: 4px; left: 0; right: 0; background: var(--rule); }
.price-range .range-fill { position: absolute; top: 14px; height: 4px; left: 0; right: 0; background: var(--ink); pointer-events: none; }
.price-range input[type="range"] {
  position: absolute; left: 0; right: 0; top: 8px; width: 100%; margin: 0;
  -webkit-appearance: none; appearance: none; background: transparent; pointer-events: none; min-height: auto;
  border: none; padding: 0;   /* the global input rule boxed the slider */
}
.price-range input[type="range"]::-webkit-slider-runnable-track { background: transparent; height: 4px; }
.price-range input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; pointer-events: auto; width: 22px; height: 22px; border-radius: 50%;
  background: var(--ink); border: 2px solid var(--paper); box-shadow: 0 0 0 1px var(--ink); margin-top: -9px; cursor: pointer;
}
.price-range input[type="range"]::-moz-range-track { background: transparent; height: 4px; }
.price-range input[type="range"]::-moz-range-thumb {
  pointer-events: auto; width: 22px; height: 22px; border-radius: 50%;
  background: var(--ink); border: 2px solid var(--paper); box-shadow: 0 0 0 1px var(--ink); cursor: pointer;
}
.find-apply { width: 100%; margin-top: 4px; }

/* ── Desktop ──────────────────────────────────────────────────────────────
   The app is phone-first and every rule above is the phone layout. Nothing
   here changes the theme: same paper, same ink, same rules-and-space
   construction. Three breakpoints, each adding only what a wider window and a
   pointer actually change:

     hover: hover     a pointer exists -- hover affordances only
     min-width: 768   tablet/laptop -- nav moves into the top bar, sheets
                      become centred dialogs, shelves stop scrolling sideways
     min-width: 1024  desktop -- wider column, two-column results, two-column
                      Bottle Detail

   Everything inside `@media (min-width: 768px)` assumes there is no bottom
   tab bar, so the body's bottom padding and the toast's offset are reset in
   the same block that removes it. */

@media (hover: hover) {
  /* Rows and tiles that open something: underline the name in the rule
     colour and darken the chevron, rather than tinting the row -- there are
     no surface steps on paper (see the :root comment). */
  .product:hover .p-name, .wl-item:hover .wl-name {
    text-decoration: underline;
    text-decoration-color: var(--muted);
    text-underline-offset: 4px;
  }
  .product:hover::after, .wl-item:hover::after, .find-browseall:hover .find-chev { color: var(--ink); }
  .mini:hover { background: rgba(23, 58, 48, .08); }
  .sug:hover, .opt:hover { background: rgba(23, 58, 48, .05); }
  .find-browseall:hover span:first-child { text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--muted); }
  /* Soft controls darken to ink at 14% over paper; filled (primary) ones go
     to --ink-soft and lighten their lettering to full paper. Text controls
     opt out with the element in the selector, so they outrank the generic. */
  button:hover:not(:disabled), .find-bar-btn:hover, .find-browseall:hover, .account-row .mini-link:hover { background: var(--soft-hover); }
  button.btn-primary:hover, .btn-primary:hover { background: var(--ink-soft); color: var(--paper); }
  button.btn-on-ink:hover, .btn-on-ink:hover, .detail-foot-watch button.mini:hover, button.rv-bar-clear:hover { background: rgba(233, 226, 211, .22); }
  button.rv-bar-btn:hover { background: var(--paper); }
  button.btn-quiet:hover, button.back:hover, button.rv-name:hover, button.rv-undo:hover, button.modal-x:hover { background: none; color: var(--ink); }
  button.p-watch:hover { color: var(--ink); background: none; }
  .tabbar button:hover { background: none; color: var(--paper); }
  button.loc-pill:hover { background: rgba(233, 226, 211, .22); }
  .shelf-head .see:hover, .applied > div.clear:hover, .store-row-links a:hover, .p-view:hover { color: var(--seal); border-color: var(--seal); text-decoration-color: var(--seal); }
  .applied > div:hover { background: var(--ink-soft); }
  button.find-clear:hover { color: var(--ink); background: none; }
}

@media (min-width: 768px) {
  /* The tab bar rides in the top bar. It stays the same <nav> element (app.js
     still finds `.tabbar button`); it is simply pinned to the top edge,
     centred over the header, instead of the bottom. */
  body { padding-bottom: 32px; }
  .tabbar {
    top: 0; bottom: auto; left: 50%; right: auto;
    transform: translateX(-50%);
    height: var(--bar-top);                 /* the .topbar's height, see .find-bar */
    background: transparent;
    z-index: 6;
  }
  .tabbar button { flex: none; padding: 0 18px; font-size: 12px; height: 100%; }
  .tabbar button.active { box-shadow: inset 0 -3px 0 0 var(--gold); }
  .topbar { padding-left: 28px; padding-right: 28px; }
  #toast { bottom: 40px; }

  main { padding: 28px 32px 40px; }
  .screen-title { margin-top: 12px; }

  /* The Filter|Sort bar is welded to the search field on a phone (one
     block, see .find-field ~ .find-results .find-bar above). With a pointer
     and room to spare that reads as three controls crammed into one box, so
     from here up the bar stands on its own: a gap below the field, its own
     corners, and the two halves given a little more height. */
  .find-field ~ .find-results .find-bar { margin-top: 16px; border-radius: var(--radius); }
  .find-field:has(~ .find-results:not([hidden])) { border-radius: var(--radius); }
  .find-bar-btn { min-height: 48px; }

  /* Shelves: still a rail, now a carousel. Mouse wheels don't scroll
     sideways, so each shelf gets a prev/next arrow (find.js shelfNode) that
     pages it a viewport at a time; tiles snap to the left edge; the native
     scrollbar is hidden because the arrows are the affordance. The rail
     loads more tiles as it nears its end (the shelf's `All n` is the cap),
     so it is not a fixed eight. */
  .shelf-wrap { position: relative; }
  .shelf {
    margin: 0; padding: 0 0 4px;
    gap: clamp(12px, 1vw, 24px);
    scroll-snap-type: x mandatory; scroll-behavior: smooth;
    scrollbar-width: none;
  }
  .shelf::-webkit-scrollbar { display: none; }
  .shelf .mini { width: clamp(200px, 12vw, 280px); scroll-snap-align: start; }
  .shelf-arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 44px; height: 44px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--paper); border: 1px solid var(--ink); border-radius: 0; color: var(--ink);
    font: 300 26px/1 var(--sans); letter-spacing: 0;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .12);
    z-index: 2;
  }
  .shelf-arrow.prev { left: -22px; }
  .shelf-arrow.next { right: -22px; }
  /* The last tile fades under the paper before the arrow (finding 16). */
  .shelf-wrap::after { content: ""; position: absolute; top: 0; bottom: 4px; right: 0; width: 48px; background: linear-gradient(to right, transparent, var(--paper)); pointer-events: none; }
  .shelf-arrow[disabled] { visibility: hidden; }
  .shelf-more { flex: none; width: 44px; display: flex; align-items: center; justify-content: center; }

  /* Bottom sheets become centred dialogs; their contents are unchanged. */
  .sheet-back { align-items: center; padding: 24px; }
  .sheet {
    max-width: 520px;
    border: 1px solid var(--ink);
    border-radius: var(--radius);
    padding: 22px 24px 24px;
    max-height: min(85vh, 760px);
    box-shadow: 0 8px 28px rgba(0, 0, 0, .25);
  }
  .sheet-scroll { max-height: 44vh; }
  .modal { max-width: 400px; }

  /* Bottle Detail: a little more room for the headline. */
  .bottle-name { font-size: 36px; }
  button.back { margin-bottom: 4px; }
}

@media (min-width: 1024px) {
  /* No fixed column. The page fills the window with a side gutter that
     grows with it (4vw, clamped), and every list below is an auto-fill grid
     that adds columns as the window widens -- 2 at a laptop, 4 at 1440p,
     7-8 across a 4K display. Type scales gently with the viewport (clamp)
     so a 4K window doesn't read as a laptop layout with more whitespace. */
  main { max-width: none; padding: 32px clamp(32px, 4vw, 120px) 48px; }
  /* Room to spare here: the arrows sit fully outside the rail. */
  .shelf-arrow.prev { left: -40px; }
  .shelf-arrow.next { right: -40px; }
  .find-field { min-height: clamp(52px, 3vw, 64px); }
  .find-field input { font-size: clamp(16px, .9vw + 6px, 20px); }
  .p-name { font-size: clamp(22px, .7vw + 14px, 32px); }
  .wl-name { font-size: clamp(20px, .6vw + 13px, 28px); }
  .shelf-head h3, .screen-title { font-size: clamp(20px, .8vw + 12px, 34px); }
  .mini h4 { font-size: clamp(16px, .4vw + 12px, 20px); }
  .mini .price { font-size: clamp(16px, .4vw + 12px, 20px); }
  .p-price { font-size: clamp(18px, .5vw + 12px, 24px); }
  .p-meta, .p-price-line, .p-stock, .wl-meta, .small, .mini .where { font-size: clamp(13px, .3vw + 10px, 16px); }
  .p-caption { font-size: clamp(12px, .25vw + 9px, 14px); }
  .find-browseall span:first-child { font-size: clamp(18px, .6vw + 12px, 26px); }

  /* Results and the watchlist: auto-fill columns of readable width. Each
     row keeps its own bottom rule; the gap is wide enough that the rules
     read as separate lists, not one broken one. `align-items: start` stops
     a short card stretching to its neighbour's height. */
  .find-cards, #watchlist {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(clamp(380px, 24vw, 520px), 1fr));
    column-gap: clamp(40px, 3vw, 80px);
    align-items: start;
  }
  #watchlist > * { min-width: 0; }
  .find-nearmiss, .find-more, .push { max-width: 520px; }
  .find-more button { width: auto; min-width: 240px; }

  /* Bottle Detail: the facts (actions, Rarity, Price, Reputation, About,
     Also sold as, retailer links) on the left; the store list on the right,
     where it can be read against the price card instead of after a long
     scroll. The store column takes about a third of the width and the facts
     column caps its line length so paragraphs stay readable on a wide
     display. app.js builds .detail-main / .detail-side; below this width
     .detail-cols is display: contents (see the rule after this block), so
     the phone order is exactly what it was. */
  .detail-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, 38%); column-gap: clamp(56px, 5vw, 160px); align-items: start; max-width: 1600px; }
  .detail-main { max-width: 80ch; }
  .detail-side { position: sticky; top: calc(var(--bar-top) + 32px); /* topbar + main's top padding */ }
  .detail-side .card:first-child, .detail-main .card:first-child { padding-top: 0; }
  .bottle-name { font-size: clamp(36px, 1.4vw + 22px, 56px); max-width: 20ch; }
  .score-number { font-size: clamp(64px, 2vw + 44px, 88px); }
  .score-hero-rationale, .store-row-name { font-size: clamp(15px, .3vw + 12px, 18px); }
}
/* Below the tablet breakpoint the shelf is the touch rail and needs no arrows. */
@media (max-width: 767px) { .shelf-arrow { display: none; } }
/* Below the desktop breakpoint the detail columns are transparent to layout. */
@media (max-width: 1023px) {
  .detail-cols, .detail-main, .detail-side { display: contents; }
}

/* Very wide windows: scale the whole interface up in steps so a 1440p or 4K
   display gets larger type and controls, not just more columns of the
   laptop size. `zoom` scales every px value together (layout, sticky
   offsets, the tab bar), and the auto-fill grids above still add columns
   in whatever width remains. The steps are gentle on purpose: the point of
   a big display is to see more, so the layout width is never scaled back
   down to a laptop's. */
@media (min-width: 1920px) { html { zoom: 1.1; } }
@media (min-width: 2560px) { html { zoom: 1.25; } }
@media (min-width: 3200px) { html { zoom: 1.5; } }

/* ── Detail, round two (docs/plans/plan-detail-page-2026-09-17.md) ──────── */
/* Facts parsed from the name, under the meta line, in the meta vocabulary. */
.p-facts { color: var(--ink-soft); font: 400 14px/1.6 var(--sans); margin: 6px 0 14px; }
.p-facts:empty { display: none; }
/* Rarity and Price side by side. The rarity hero's 64px numeral is scaled
   down so both columns share a baseline at phone width. */
.detail-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.detail-pair .score-number { font-size: 48px; }
/* Fewer stores than before is bad news for a hunter, so red stays; the
   copy says "down from" so colour is not the only signal. */
.trend-down { color: var(--error); }
/* About this whisky: the description, then one block per entity. */
.about-desc { font: 400 15px/1.5 var(--sans); color: var(--ink-soft); margin: 0; }
.score-hero + .about-desc { margin-top: 10px; }
.about-block { margin-top: 14px; }
.about-block-name { margin: 0 0 2px; font: 600 14px/1.3 var(--sans); color: var(--ink); }
.about-block-text { margin: 0; font: 400 14px/1.5 var(--sans); color: var(--ink-soft); }
.lookup { margin: 14px 0 0; }
.lookup a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
/* Similar whiskies: rows are links into Detail. */
.sim-list { display: flex; flex-direction: column; }
.sim-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; min-height: 48px; padding: 8px 0; border-bottom: var(--rule-row); text-decoration: none; color: var(--ink); }
.sim-row:last-child { border-bottom: none; }
.sim-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sim-name { font: 400 15px/1.3 var(--serif); }
.sim-tag { font: 400 12px/1.2 var(--sans); color: var(--muted); }
.sim-side { display: flex; align-items: center; gap: 12px; font: 400 13px/1 var(--sans); color: var(--muted); white-space: nowrap; }
.sim-chev { font: 400 20px/1 var(--sans); color: var(--ink); }
/* Pressed: the row fills with the paper's darker tone until the new page opens. */
.sim-row:active { background: color-mix(in srgb, var(--ink) 10%, var(--paper)); margin: 0 -8px; padding-left: 8px; padding-right: 8px; }
/* A good price is good news: seal, the sale colour, not the error red
   (design review finding 17). */
.price-flag { color: var(--seal); }
/* Footer: last in the flow, sticks to the bottom above the tab bar on a
   phone until its own place scrolls into view. Wide displays: a plain row. */
.detail-foot { position: sticky; bottom: var(--bar-tab); margin-top: 18px; background: var(--ink); padding: 6px 16px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; z-index: 5; transition: transform .25s ease, opacity .25s ease; }
/* Hidden while the headline is on screen (app.js IntersectionObserver): it
   slides down out of view, still in the flow so nothing else moves. */
.detail-foot.detail-foot-hidden { transform: translateY(calc(100% + var(--bar-tab))); opacity: 0; pointer-events: none; }
.detail-foot-btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius); color: var(--paper); text-decoration: none; font: var(--font-label); letter-spacing: var(--track-label); text-transform: uppercase; }
.detail-foot-btn[hidden] { display: none; }
.detail-foot-watch { display: flex; }
.detail-foot-watch:empty { display: none; }
/* The foot's Watch is a button.mini drawn on the ink bar: the on-ink fill. */
.detail-foot-watch button.mini { flex: 1; min-height: 44px; background: rgba(233, 226, 211, .14); color: var(--paper); }
.detail-foot:has(.detail-foot-btn[hidden]), .detail-foot:has(.detail-foot-watch:empty) { grid-template-columns: minmax(0, 1fr); }
.detail-foot:has(.detail-foot-btn[hidden]):has(.detail-foot-watch:empty) { display: none; }
@media (display-mode: standalone) { .detail-foot { bottom: calc(var(--bar-tab) + env(safe-area-inset-bottom)); } }
@media (min-width: 1024px) { .detail-foot { position: static; } .detail-foot.detail-foot-hidden { transform: none; opacity: 1; pointer-events: auto; } }

/* ── Review tab (owner only): one card per pair the matcher could not call ──
   Names are buttons that open Detail; Same is the filled primary, Different
   the soft secondary, Later quiet. The batch bar sits above the tab bar like
   .detail-foot and appears only while something is checked. */
.tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; margin-left: 6px; border-radius: 9px; background: var(--gold); color: var(--ink); font: 700 10px/1 var(--sans); letter-spacing: 0; vertical-align: middle; }
.tab-badge[hidden] { display: none; }
.rv-head { display: flex; flex-direction: column; gap: 12px; padding-bottom: 14px; border-bottom: var(--rule-row); }
.rv-line { margin: 0; font: 400 14px/1.5 var(--sans); color: var(--muted); }
.rv-line strong { color: var(--ink); font-weight: 600; }
.rv-head button.mini { align-self: flex-start; }
.rv-list { display: flex; flex-direction: column; }
.rv-card { display: flex; flex-direction: column; gap: 10px; padding: 16px 0; border-bottom: var(--rule-row); }
.rv-card.rv-deferred { opacity: .72; }
.rv-top { display: flex; gap: 12px; align-items: flex-start; }
.rv-check { display: flex; align-items: center; min-width: 44px; min-height: 44px; margin: -10px 0 0 -10px; padding: 10px; }
.rv-check input { width: 20px; height: 20px; margin: 0; accent-color: var(--ink); }
.rv-names { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1; }
.rv-name-row { display: flex; align-items: baseline; gap: 10px; }
.rv-kicker { flex: 0 0 52px; font: 600 11px/1.4 var(--sans); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
button.rv-name { background: none; border: 0; padding: 0; margin: 0; text-align: left; font: 400 16px/1.3 var(--serif); color: var(--ink); letter-spacing: 0; text-transform: none; min-height: 0; text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: 3px; }
.rv-why { margin: 0; font: 400 13px/1.5 var(--sans); color: var(--muted); }
.rv-why strong { color: var(--ink); font-weight: 600; }
.rv-err { margin: 0; }
.rv-err:empty { display: none; }
.rv-acts { display: flex; gap: 10px; }
.rv-btn { min-height: 44px; padding: 0 10px; flex: 1 1 0; }
.rv-btn.btn-quiet { flex: 0 0 auto; }
.rv-card.rv-settled { padding: 14px 0; }
.rv-settled-line { display: flex; justify-content: space-between; align-items: center; gap: 12px; font: 400 14px/1.4 var(--sans); color: var(--muted); }
button.rv-undo { background: none; border: 0; padding: 8px 0; margin: 0; min-height: 44px; font: 500 14px/1 var(--sans); color: var(--ink); text-decoration: underline; text-underline-offset: 3px; letter-spacing: 0; text-transform: none; }
.rv-bar { position: sticky; bottom: var(--bar-tab); margin-top: 18px; background: var(--ink); padding: 10px 16px; display: flex; gap: 10px; align-items: center; z-index: 5; }
.rv-bar[hidden] { display: none; }
/* On the ink bar: the confirming button is a paper fill, Clear the on-ink fill. */
.rv-bar-btn { flex: 1; min-height: 44px; background: var(--paper); color: var(--ink); font: 600 12px/1 var(--sans); letter-spacing: var(--track-label); text-transform: uppercase; }
.rv-bar-clear { flex: 0 0 auto; min-height: 44px; background: rgba(233, 226, 211, .14); color: var(--paper); font: 500 12px/1 var(--sans); letter-spacing: var(--track-label); text-transform: uppercase; padding: 0 14px; }
@media (display-mode: standalone) { .rv-bar { bottom: calc(var(--bar-tab) + env(safe-area-inset-bottom)); } }
.rv-below { margin-top: 18px; border-top: var(--rule-row); }
.rv-below summary { display: flex; justify-content: space-between; align-items: center; gap: 10px; min-height: 48px; padding: 8px 0; cursor: pointer; list-style: none; }
.rv-below summary::-webkit-details-marker { display: none; }
.rv-below-title { font: 400 16px/1.3 var(--serif); color: var(--ink); }
.rv-below summary::after { content: "›"; font: 400 20px/1 var(--sans); color: var(--ink); transition: transform .15s; }
.rv-below[open] summary::after { transform: rotate(90deg); }
.rv-note { margin: 0 0 4px; }

/* "Link to another bottle" on Detail's "Also sold as" card (owner): a quiet
   search field under the suggestions; hits render as ordinary .link-rows. */
.link-search { margin-top: 12px; }
.link-search input { width: 100%; box-sizing: border-box; min-height: 44px; padding: 10px 12px; border: 1px solid var(--rule); border-radius: 0; background: transparent; color: var(--ink); font: 400 15px/1.3 var(--sans); }
.link-search input:focus { outline: none; border-color: var(--ink); }
.link-search-results { display: flex; flex-direction: column; }
.link-search-results .link-row:last-child { border-bottom: 0; }
.rv-check-off { visibility: hidden; }
.rv-blocked strong { color: var(--error); }

/* ── Account row at the foot of Find (app.js renderAccountRow) ──────────── */
.account-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
  margin: 28px 0 8px; padding: 14px 0 0;
  border-top: var(--rule-row);
}
/* Signed out: a single left-aligned block under a section rule, capped at
   420px so the sentence and the buttons never drift apart on a wide window
   (design review finding 9). */
.account-row-anon { flex-direction: column; align-items: stretch; gap: 10px; max-width: 420px; padding-top: 18px; border-top: var(--rule-section); }
.account-row-anon .card-heading { margin: 0; }
.account-row-anon p { margin: 0; }
.account-row-anon .signin-buttons { max-width: none; }
.account-who { display: flex; align-items: center; gap: 10px; min-width: 0; }
.account-avatar { width: 32px; height: 32px; border-radius: 50%; flex: none; }
.account-acts { flex-direction: column; align-items: stretch; justify-content: flex-start !important; margin-top: 6px; }
.account-acts button { width: 100%; }
.account-acts button:disabled { opacity: .45; }
.account-danger { border-top: var(--rule-row); margin-top: 14px; padding-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.account-danger button { width: 100%; }
.account-danger p { margin: 0; }
/* The corner close of the account sheet: 44px hit area, hairline icon. */
.modal { position: relative; }
.modal-x { position: absolute; top: 2px; right: 2px; width: 44px; height: 44px; min-height: 0; padding: 0; border: none; background: none; color: var(--muted); display: inline-flex; align-items: center; justify-content: center; }
.modal-x:hover { color: var(--ink); }
.modal:has(.modal-x) h3 { padding-right: 40px; }
/* An <a> styled like button.mini: the account row's Admin link. */
.account-row .mini-link {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 12px;
  background: var(--soft); border-radius: var(--radius); color: var(--ink); text-decoration: none;
  font: var(--font-label); letter-spacing: var(--track-label); text-transform: uppercase;
}

/* ── Sign-in buttons: the account row and the sign-in sheet ─────────────── */
.signin-buttons { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 280px; }
.signin-link {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 44px; padding: 0 16px; text-decoration: none; border-radius: var(--radius);
  font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
}
/* Google's rules: white ground, #747775 hairline, #1F1F1F sentence-case
   label, the four-colour G. Fixed colours on purpose: the brand button
   does not follow the theme, the same way the Apple one stays black. */
.signin-google { background: #fff; color: #1f1f1f; border: 1px solid #747775; text-transform: none; letter-spacing: 0; font: 500 15px/1 var(--sans); }
.signin-google svg { flex: none; }
/* Apple's rules: black, white mark and words, "Sign in with Apple" verbatim,
   at least 44 px tall. Both brands allow the house 6px corner; the colours
   and outlines are theirs and stay. */
.signin-apple { background: #000; color: #fff; border: 1px solid #000; text-transform: none; letter-spacing: 0; font: 500 15px/1 var(--sans); }
.signin-apple svg { flex: none; }
.modal .signin-buttons { max-width: none; margin: 6px 0 2px; }
/* The privacy line under the account row, the account sheet and the sign-in sheet. */
.account-row p a, .modal p a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
