:root{
  --paper:oklch(96.5% .013 78);--surface:oklch(98.6% .008 80);--ink:oklch(24% .022 55);
  --muted:oklch(47% .025 60);--faint:oklch(60% .02 65);--line:oklch(89.5% .017 78);
  --ember:oklch(56% .17 40);--ember-soft:oklch(94% .03 45);
  --drop:oklch(50% .11 155);--drop-soft:oklch(94% .04 155);
  --restock:oklch(50% .10 250);--restock-soft:oklch(94% .03 250);
  --fd:"Bricolage Grotesque",sans-serif;--fu:"Hanken Grotesk",sans-serif;
  --space-xs:4px;--space-sm:8px;--space-md:12px;--space-lg:16px;--space-xl:24px;--space-2xl:40px;--space-3xl:64px;
}
@media (prefers-color-scheme:dark){:root{
  /* Readability rework: the old paper(22%)/surface(24%) sat only 2% L apart, so
     cards melted into the background and the page read as one flat dark void.
     Paper lifted to 25%, surface to 30% (a real 5% gap = clear card elevation),
     line 30→40% so borders are actually visible. Ink softened 95→94 to cut
     glare. Still a warm dark-roast palette, just legible. */
  --paper:oklch(25% .016 62);--surface:oklch(30% .016 62);--ink:oklch(94% .012 82);
  --muted:oklch(75% .018 72);--faint:oklch(67% .015 68);--line:oklch(40% .016 70);
  --ember:oklch(73% .16 45);--ember-soft:oklch(33% .06 42);
  --drop:oklch(73% .14 155);--drop-soft:oklch(33% .05 155);
  --restock:oklch(73% .12 250);--restock-soft:oklch(33% .045 250);
  --plate:oklch(92% .012 80); /* light plate under product photos */
}
/* Product imagery reads on a light plate, never on dark paper — shops shoot
   packs for white; transparent PNGs turn to sludge on dark backgrounds. */
.thumb,div.thumb{background:var(--plate)}
div.thumb{color:oklch(75% .02 75)}
.gthumb,.gthumb img{background:var(--plate)}
.relcard img,.relswatch{background:var(--plate)}
.brandlogo{background:var(--plate)}
}
*{box-sizing:border-box;margin:0}
body{background:var(--paper);color:var(--ink);font:15px/1.55 var(--fu);font-variant-numeric:tabular-nums}
main{max-width:76rem;margin:0 auto;padding:0 var(--space-lg) var(--space-2xl)}
::selection{background:var(--ember);color:var(--paper)}
/* iOS: kill the grey tap flash and the 300ms double-tap-zoom delay on every
   interactive control (the .card tile carries role=button). */
a,button,select,input,[role="button"]{-webkit-tap-highlight-color:transparent;touch-action:manipulation}

header.site{position:sticky;top:0;z-index:5;background:var(--paper);max-width:76rem;margin:0 auto;display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:var(--space-sm);padding:var(--space-xl) var(--space-lg) var(--space-md)}
.wordmark{font:800 clamp(1.9rem,4vw,2.6rem)/.95 var(--fd);letter-spacing:-.03em}
.wordmark a{color:inherit;text-decoration:none}
.wordmark span{color:var(--ember)}
.meter{font:600 12.5px/1.55 var(--fu);color:var(--muted);text-align:right;letter-spacing:.01em}
.meter b{color:var(--ink);font-weight:700}
.meter a{color:inherit}
.lsw{font:600 11.5px/1 var(--fu);color:var(--muted);margin-right:var(--space-md)}
.lsw a{color:var(--muted);text-decoration:none}
.lsw a:hover{color:var(--ink)}
.lsw b{color:var(--ink)}
/* #716 part 2: persistent header-level login entry point for roaster owners
   / team members (footer's link, reworded by #708, was still undiscoverable
   — ~26 links deep). Quiet utility text, same weight as .lsw, so it reads as
   header chrome not a nav item. Mobile: drops to its own full-width row
   (order:3, after .hsearch's order:2) so it never competes with or shifts
   the search box or the cart chip. */
.hdrlogin{font:600 11.5px/1 var(--fu);color:var(--muted);text-decoration:none}
.hdrlogin:hover{color:var(--ink)}
@media(max-width:640px){.hdrlogin{order:3;flex-basis:100%;text-align:right;margin-top:var(--space-xs)}}
.pulse{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--drop);margin-right:.45em;animation:pulse 2.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.2}}
@media (prefers-reduced-motion:reduce){.pulse{animation:none}}

/* Header search (docs/plans/coffee-search.md "UX spec"): instrument styling,
   no thumbnails, no entrance motion — same hairline/pill grammar as .find /
   .chip. Sits between the wordmark and the meter block, per DESIGN.md's
   "wordmark left; meter block right" — search is a meter function. */
.hsearch{position:relative;flex:1 1 16rem;max-width:22rem;margin:0 var(--space-lg)}
.hq{width:100%;font:500 13.5px/1 var(--fu);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:.55rem 2.1rem .55rem .75rem;min-height:38px;transition:border-color .15s ease-out}
.hq::placeholder{color:var(--faint)}
.hq:focus{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.hkey{position:absolute;right:.6rem;top:50%;transform:translateY(-50%);font:600 10.5px/1 var(--fu);color:var(--faint);border:1px solid var(--line);border-radius:4px;padding:.2rem .4rem;pointer-events:none;background:var(--paper)}
.hq:focus~.hkey{display:none}
/* grouped, micro-labeled suggest panel — hairline dividers, tabular-nums
   counts, keyboard-navigable rows (.hsopt.active mirrors :hover) */
.hsuggest{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:20;background:var(--surface);border:1px solid var(--line);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:70vh;overflow-y:auto;padding:var(--space-xs) 0}
.hsgroup+.hsgroup{border-top:1px solid var(--line);margin-top:var(--space-xs);padding-top:var(--space-xs)}
.hsglabel{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint);padding:.3rem var(--space-md)}
.hsopt{display:flex;align-items:center;justify-content:space-between;gap:var(--space-sm);width:100%;text-align:left;font:500 13px/1.4 var(--fu);color:var(--ink);background:none;border:0;padding:.5rem var(--space-md);cursor:pointer;min-height:36px}
.hsopt:hover,.hsopt.active{background:var(--paper)}
.hsopt b{font-weight:700}
.hsopt .hscount{color:var(--faint);font-variant-numeric:tabular-nums;font-weight:600;font-size:12px;flex:none}
.hsempty{font:500 13px/1.5 var(--fu);color:var(--faint);padding:.5rem var(--space-md)}
@media(max-width:640px){
.hsearch{order:2;flex-basis:100%;max-width:none;margin:var(--space-sm) 0 0}
.hq{font-size:16px;min-height:44px}
}

.rule{max-width:76rem;margin:0 auto;padding:0 var(--space-lg)}
.rule::after{content:"";display:block;height:3px;background:var(--ink);border-radius:2px}
/* task #1: read as an alert callout, not a thin full-bleed rule — taller, bolder,
   ink text on ember-soft (DESIGN.md: stale = ember-soft bg + ember border) */
.stale{max-width:76rem;margin:var(--space-md) auto 0;display:flex;align-items:center;gap:.5rem;font:700 13px/1.45 var(--fu);color:var(--ink);background:var(--ember-soft);border:1px solid var(--ember);border-radius:6px;padding:.75rem 1rem}

.toolbar{max-width:76rem;margin:0 auto;display:flex;flex-wrap:wrap;gap:var(--space-xs);align-items:center;padding:var(--space-md) var(--space-lg)}
.find{flex:1 1 15rem;min-width:12rem;font:500 13.5px/1 var(--fu);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:.55rem .75rem;transition:border-color .15s ease-out}
.find::placeholder{color:var(--faint)}
.find:focus{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.chip{font:600 12px/1 var(--fu);border:1px solid var(--line);background:var(--surface);color:var(--muted);border-radius:99px;padding:.45rem .75rem;cursor:pointer;transition:all .15s ease-out}
.chip:hover{border-color:var(--muted);color:var(--ink)}
.chip:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
.chip[aria-pressed="true"]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
/* recent searches (F6): a compact chip row under the find bar, shown only on
   focus of an empty search box — never competes with live filter results.
   Base visibility is the inline style="display:none" the template renders
   (same convention as .suggest/.hidebar) — JS toggles it to 'flex' to show,
   so an empty/collapsed panel never eats layout space it shouldn't. */
.recent{flex-basis:100%;flex-wrap:wrap;align-items:center;gap:var(--space-xs);margin-top:var(--space-xs)}
.recentlabel{font:600 11.5px/1 var(--fu);color:var(--muted)}
.recent .chip{padding:.35rem .65rem;font-size:11px}
.obtn{border-color:var(--ember);border-width:2px;color:var(--ink);background:var(--ember-soft);font-weight:700}
.obtn b{font-weight:700}
.obtn:hover{border-color:var(--ember);background:var(--ember-soft);color:var(--ember)}
.obtn:focus-visible{outline:2px solid var(--ember);outline-offset:1px;border-color:var(--ember)}
.belt{max-width:76rem;margin:0 auto;padding:var(--space-md) var(--space-lg) var(--space-lg);border-bottom:1px solid var(--line)}
.beltmap{width:100%;display:block;color:var(--muted)}
.beltcap{display:flex;align-items:baseline;gap:var(--space-sm);font:500 12px/1.5 var(--fu);color:var(--muted);margin-bottom:var(--space-sm)}
.beltcap b{font:700 13px/1.3 var(--fd);color:var(--ink)}
.belthelp{position:relative;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;border:1px solid var(--faint);color:var(--faint);font:700 10px/1 var(--fu);cursor:help}
.belthelp .tip{display:none;position:absolute;left:50%;top:135%;transform:translateX(-50%);z-index:6;width:min(16rem,calc(100vw - 2*var(--space-lg)));background:var(--ink);color:var(--paper);font:500 11.5px/1.5 var(--fu);border-radius:4px;padding:.55rem .7rem;text-align:left}
.belthelp:hover .tip,.belthelp:focus .tip,.belthelp.tipopen .tip{display:block}
.hastip{position:relative;cursor:help}
.hastip .tip{display:none;position:absolute;left:50%;top:135%;transform:translateX(-50%);z-index:20;width:min(16rem,calc(100vw - 2*var(--space-lg)));max-width:min(240px,70vw);white-space:normal;background:var(--ink);color:var(--paper);font:500 11.5px/1.5 var(--fu);border-radius:4px;padding:.55rem .7rem;text-align:left}
.hastip:hover .tip,.hastip:focus .tip,.hastip.tipopen .tip{display:block}
/* #187: when the below-host default (top:135%) would overflow the viewport
   bottom, clampTip flips the bubble above the host so it stays in-viewport. */
.tip.tipup{top:auto;bottom:135%}
.landmass{fill:currentColor;opacity:.13;stroke:none}
.belttxt{font:600 4.5px var(--fu);text-transform:uppercase;letter-spacing:.12em;fill:var(--faint)}
/* task #12: kill the browser's default focus box on the focusable SVG dot;
   keyboard focus shown on the circle's stroke instead. Fallback to :focus for
   older browsers; @supports suppresses mouse-focus outline in modern ones. */
.beltdot{cursor:pointer;outline:none}
.beltdot circle{fill:var(--ember);opacity:.55;transition:opacity .15s ease-out,r .15s ease-out}
.beltdot:hover circle,.beltdot:focus circle{opacity:1}
.beltdot:focus circle{stroke:var(--ink);stroke-width:1.5}
.beltdot.on circle{opacity:1;stroke:var(--ink);stroke-width:1.5}
@supports selector(:focus-visible) {
  .beltdot:focus:not(:focus-visible) circle{stroke:none}
  .beltdot:hover circle,.beltdot:focus-visible circle{opacity:1}
}
.beltchips{display:flex;flex-wrap:wrap;gap:var(--space-xs);margin-top:var(--space-md)}
/* Crawlable origin links. The belt chips are <button>s driven by site.js and
   live inside #beltpanel, which ships `hidden` — so these are both the real
   <a>s Google follows to /origin/{country}/ AND the only always-visible way
   to reach them. They sit OUTSIDE the collapsed panel for that reason and
   carry the page container themselves. Micro-label voice (.railhead/.secnav),
   not a second chip row — two chip rows read as a duplicated control. */
.origlinks{max-width:76rem;margin:0 auto;padding:0 var(--space-lg) var(--space-md);font:700 10.5px/1.7 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}
.origlinks a{color:var(--muted);text-decoration:none;border-bottom:1px solid var(--line)}
.origlinks a:hover{color:var(--ember);border-bottom-color:var(--ember)}
.bchip.on{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.bcount{opacity:.65;font-weight:600;margin-left:.15em}

/* secnav: jump-nav rail for the home page's sections — pure state highlight
   (IntersectionObserver toggles .active), never an animated fill; DESIGN.md
   bans progress/scroll choreography, this is wayfinding only. Built entirely
   by site.js from whatever sections actually render, so :empty hides it
   before JS runs (and if JS never runs). */
.secnav{position:sticky;top:var(--header-h,0px);z-index:4;max-width:76rem;margin:0 auto;display:flex;gap:var(--space-lg);overflow-x:auto;padding:var(--space-sm) var(--space-lg);background:var(--paper);border-bottom:1px solid var(--line);scrollbar-width:none}
.secnav::-webkit-scrollbar{display:none}
.secnav:empty{display:none;border-bottom:0;padding:0}
.secnav a{flex:0 0 auto;font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--muted);text-decoration:none;padding-bottom:.3rem;border-bottom:2px solid transparent;white-space:nowrap}
.secnav a:hover{color:var(--ink)}
.secnav a.active{color:var(--ember);border-bottom-color:var(--ember)}
@media(max-width:640px){.secnav{display:none}}

section.feed{margin-top:var(--space-2xl)}
.feed h2{font:700 clamp(1.25rem,2.2vw,1.55rem)/1.1 var(--fd);letter-spacing:-.015em;display:flex;align-items:baseline;gap:.55rem}
.feed h2 .count{font:700 12px/1 var(--fu);color:var(--paper);background:var(--muted);border-radius:99px;padding:.25rem .5rem;letter-spacing:.02em;align-self:center}
.feed .sub,main .sub{font:500 12.5px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-xs)}
.taglinewrap{max-width:76rem;margin:0 auto;padding:0 var(--space-lg)}
h1.tagline{font:500 13px/1.5 var(--fu);color:var(--muted);margin:var(--space-sm) 0 0;max-width:52ch}
.buytrust{font:400 12px/1.6 var(--fu);color:var(--faint);margin-top:var(--space-sm);max-width:52ch}
.buytrust a{color:inherit;text-decoration:underline}
.feed>.cards{margin-top:var(--space-lg)}
/* Updates (news-feed MVP slice 1, #146): smaller variant of the image-card
   tile grammar — same .card surface/radius, no click-to-inspect (not a
   product), so cursor/hover lift are dropped. Source pill reuses the solid
   "count" badge pill visual (muted bg, paper text, 99px radius) rather than
   the event trio's colors — this is provenance, not a radar event. Owner
   posts render no pill (first-person voice). No new color or font. */
#updates .cards{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.upcard{cursor:default}
.upcard:hover{transform:none;border-color:var(--line)}
.upcard .thumb{height:110px}
.upcard .body{gap:var(--space-sm)}
.srcpill{align-self:flex-start;font:700 10px/1 var(--fu);color:var(--paper);background:var(--muted);border-radius:99px;padding:.22rem .55rem;text-transform:uppercase;letter-spacing:.06em}
.updbody{font:500 13.5px/1.5 var(--fu);color:var(--ink)}
.updtime{font:600 11px/1 var(--fu);color:var(--faint);font-variant-numeric:tabular-nums;text-transform:uppercase;letter-spacing:.04em}
.updmore{font:700 12px/1 var(--fu);color:var(--ember);text-decoration:none;align-self:flex-start}
.updmore:hover{text-decoration:underline}
.updempty{grid-column:1/-1;font:500 13px/1.5 var(--fu);color:var(--muted);padding:var(--space-sm) 0}
/* featured: horizontal scroll rail (#136a) — same touch/keyboard pattern as
   .chiprail (hidden scrollbar, scroll-snap-align, -webkit-overflow-scrolling)
   instead of a grid, so the editorial band reads as its own row at any width.
   Paid/sponsored placement (#136b) is Phase 5, out of scope here. */
#featured .cards{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:var(--space-md);padding-bottom:2px;-webkit-overflow-scrolling:touch;scrollbar-width:none;scroll-snap-type:x proximity}
#featured .cards::-webkit-scrollbar{display:none}
#featured .cards .card{flex:0 0 200px;scroll-snap-align:start}
/* radar category rails: micro-label heading + hairline, cards regrouped below */
.rail{margin-top:var(--space-xl)}
.railhead{font:700 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--muted);display:flex;align-items:center;gap:var(--space-sm)}
.railhead::after{content:"";flex:1;height:1px;background:var(--line)}
.railcount{color:var(--faint);font-weight:600;font-variant-numeric:tabular-nums}
.rail>.cards{margin-top:var(--space-md)}
/* "view all (N)" escape valve to /catalog/{id}/ — same micro-label voice as
   railcount, ember for the link cue. In a railhead it sits before the
   hairline divider; in a section h2 (.hmore) it's pushed flush right. */
.railmore{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--ember);text-decoration:none;white-space:nowrap}
.railmore:hover{text-decoration:underline}
.feed h2 .hmore{margin-left:auto}
/* sort control: real <select> kept fully native for semantics/keyboard/SR
   behavior (F8) — only the closed-state chrome is restyled, via
   appearance:none + a CSS-drawn chevron (border-triangle trick, no SVG
   data-uri, so it inherits --muted and just works in dark mode), same
   ember-focus-ring / chip language as .bchip. The native open-menu popup
   itself is intentionally left to the OS/browser: fully re-skinning it
   isn't reliably possible with CSS and would risk the keyboard/SR
   semantics a plain <select> gives for free. */
/* never-dead-end panel: on zero matches, offer one-tap relaxations instead
   of a bare "not found" (each chip names what it unlocks + a count) */
.suggest{margin-top:var(--space-xl);border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:var(--space-lg)}
.sughead{font:600 13.5px/1.5 var(--fu);color:var(--ink)}
.sughead b{color:var(--ember)}
.sugrow{display:flex;flex-wrap:wrap;gap:var(--space-xs);margin-top:var(--space-md)}
.sugrow .chip{color:var(--ink)}
.sugmiss{margin-top:var(--space-md);font:400 12.5px/1.5 var(--fu);color:var(--muted)}
.sugmiss a{color:var(--ember)}
.sugfeat{margin-top:var(--space-lg);padding-top:var(--space-lg);border-top:1px solid var(--line)}

/* /search results page: "N readings match" voice (never "results found"),
   removable applied-facet chips, zero-result relaxations reuse .sugrow. */
.searchhead{margin-top:var(--space-lg)}
.shq{font:700 clamp(1.15rem,2.4vw,1.5rem)/1.25 var(--fd);letter-spacing:-.01em}
.shq b{color:var(--ember);font-variant-numeric:tabular-nums}
.shsub{font:500 13px/1.5 var(--fu);color:var(--muted);margin-top:.2rem}
.facetchips{display:flex;flex-wrap:wrap;gap:var(--space-xs);margin-top:var(--space-md)}
.fchip{display:inline-flex;align-items:center;gap:.4em;font:600 12px/1 var(--fu);color:var(--ink);background:var(--ember-soft);border:1px solid var(--ember);border-radius:99px;padding:.45rem .5rem .45rem .75rem;cursor:pointer;min-height:32px;transition:opacity .15s ease-out}
.fchip:hover{opacity:.8}
.fchip .x{font-weight:700;color:var(--ember)}
.sponsored{font:700 9.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.07em;color:var(--paper);background:var(--muted);border-radius:99px;padding:.25rem .45rem}
@media(max-width:640px){.fchip{min-height:44px}}
.sugfeat .cards{margin-top:var(--space-md)}
.sortwrap{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font:600 11.5px/1 var(--fu);color:var(--muted);letter-spacing:.01em;position:relative}
.sortsel{font:600 12px/1 var(--fu);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:99px;padding:.4rem 1.7rem .4rem .65rem;cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}
.sortsel:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
.sortwrap::after{content:"";position:absolute;right:.7rem;top:50%;width:6px;height:6px;border-right:1.5px solid var(--muted);border-bottom:1.5px solid var(--muted);transform:translateY(-65%) rotate(45deg);pointer-events:none}
/* €/cup at a personal dose: dose slider (DESIGN.md 2026-07-10 — replaces the
   dropdown; direct manipulation of "how much am I brewing" reads better as a
   range than a picked list, same ember-accent chip family as .sortsel),
   filled client-side from data-eurg (micro-euros/gram) — sparse bean data
   omits the whole element, never a guessed number. */
.cupline{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
.doseslider-wrap{display:flex;align-items:center;gap:.5rem;flex:0 0 auto;min-width:120px}
.doseslider{-webkit-appearance:none;appearance:none;flex:0 0 auto;width:180px;height:4px;border-radius:99px;background:linear-gradient(to right,var(--ember) 0%,var(--ember) var(--fillpct,50%),var(--line) var(--fillpct,50%),var(--line) 100%);cursor:pointer;accent-color:var(--ember)}
.doseslider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:var(--ember);border:2px solid var(--paper);box-shadow:0 0 0 1px var(--line);cursor:pointer;transition:transform .15s ease-out}
.doseslider::-webkit-slider-thumb:hover{transform:scale(1.15)}
.doseslider::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--ember);border:2px solid var(--paper);box-shadow:0 0 0 1px var(--line);cursor:pointer}
.doseslider::-moz-range-track{height:4px;border-radius:99px;background:var(--line)}
.doseslider::-moz-range-progress{height:4px;border-radius:99px;background:var(--ember)}
.doseslider:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.dosereadout{font:700 13px/1 var(--fu);font-variant-numeric:tabular-nums;color:var(--ink);min-width:3ch}
.doseshint{font:600 10.5px/1 var(--fu);letter-spacing:.03em;color:var(--faint);white-space:nowrap;flex-basis:100%;margin-top:2px}
.doseshint:empty{display:none}
/* #184: the hint sits on its own flex line in the cupline. Collapsing that
   line (:empty display:none, above) on every preset crossing reflows the row
   and, in the sticky split inspector, jitters everything below the slider up
   and down ~20px mid-drag. Inside the inspector keep the empty hint in flow
   and reserve its line height (10.5px = the hint's font/line-box) so the
   column stays put while dragging. The full-width product-page spec plate
   keeps the collapse — its slider was never in this reflow path. */
.inspect .doseshint:empty{display:block;min-height:10.5px}
.cupamt{font:700 15px/1 var(--fu);font-variant-numeric:tabular-nums;color:var(--ink)}
.cupunit{font:600 12px/1 var(--fu);color:var(--muted)}
.atdose{font:700 10.5px/1 var(--fu);letter-spacing:.04em;color:var(--faint);font-variant-numeric:tabular-nums}
@media(max-width:640px){.doseslider-wrap{flex-basis:100%}.doseslider{width:100%;flex:1 1 auto}.doseslider::-webkit-slider-thumb{width:22px;height:22px}.doseslider::-moz-range-thumb{width:22px;height:22px}}
@media (prefers-reduced-motion:reduce){.doseslider::-webkit-slider-thumb{transition:none}}
/* watch star: quiet on the thumb, ember when armed */
.star{position:absolute;top:var(--space-sm);right:var(--space-sm);width:26px;height:26px;border-radius:99px;border:1px solid var(--line);background:var(--surface);color:var(--muted);font:600 13px/1 var(--fu);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease-out}
.star:hover{color:var(--ember);border-color:var(--ember)}
.star:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.star[aria-pressed="true"]{color:var(--paper);background:var(--ember);border-color:var(--ember)}
/* cart add: sits beside the star, same chip language, on the .card thumb */
.cartadd{position:absolute;top:var(--space-sm);right:calc(26px + var(--space-sm) + 6px);width:26px;height:26px;border-radius:99px;border:1px solid var(--line);background:var(--surface);color:var(--muted);font:700 14px/1 var(--fu);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease-out}
.cartadd:hover{color:var(--ember);border-color:var(--ember)}
.cartadd:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.cartadd.added{color:var(--paper);background:var(--ember);border-color:var(--ember)}
.detail .cartadd,.foot .cartadd{position:static;width:auto;height:auto;border-radius:99px;padding:.5rem .85rem;font:700 12.5px/1 var(--fu)}
/* header cart chip: hidden at zero, mirrors .lsw's quiet weight. Now a
   <button> (was a <span>) so it can open the cart drawer (#135) — reset the
   button chrome back to the original inline-text look. The #134 interim
   `display:none` rule that hid this + .cartadd until the drawer shipped has
   been removed (#142) — the drawer landed, so #134's job is done. */
.cartchip{font:700 12px/1 var(--fu);color:var(--ink);margin-right:var(--space-md);background:none;border:0;padding:0;cursor:pointer;display:inline-flex;align-items:center;gap:.3em}
.cartchip:hover{color:var(--ember)}
.cartchip:focus-visible{outline:2px solid var(--ember);outline-offset:2px;border-radius:2px}
.cartchip .ccount{font-variant-numeric:tabular-nums}

/* Universal cart drawer (#135) — side panel per DESIGN.md ("no modal, ever";
   the drawer is the one explicit exception to that rule, same footing as
   the split-view .inspect above). Slides in from the header chip, never
   centered/full-bleed. Scrim is a light dim only, not an opaque modal veil. */
.cartdrawer-scrim{position:fixed;inset:0;z-index:70;background:oklch(from var(--ink) l c h / 22%);opacity:0;pointer-events:none;transition:opacity .15s ease-out}
.cartdrawer-scrim.show{opacity:1;pointer-events:auto}
.cartdrawer{position:fixed;top:0;right:0;bottom:0;z-index:71;width:min(380px,100vw);background:var(--surface);border-left:1px solid var(--line);box-shadow:-8px 0 24px oklch(from var(--ink) l c h / 12%);display:flex;flex-direction:column;transform:translateX(100%);transition:transform .2s cubic-bezier(.22,1,.36,1)}
.cartdrawer.show{transform:none}
@media (prefers-reduced-motion:reduce){.cartdrawer-scrim,.cartdrawer{transition:none}}
.cartdrawer-head{display:flex;align-items:center;gap:var(--space-md);padding:var(--space-lg);border-bottom:1px solid var(--line)}
.cartdrawer-head h2{font:800 15px/1 var(--fd);letter-spacing:-.01em;margin-right:auto}
/* links out to the full /cart page (#54) from the quick-view drawer */
.cartviewfull{font:600 11.5px/1 var(--fu);color:var(--ember);text-decoration:none;border-bottom:1.5px solid currentColor;padding-bottom:1px;white-space:nowrap}
.cartviewfull:hover{opacity:.75}
.cartdrawer-close{font:700 13px/1 var(--fu);color:var(--muted);background:none;border:0;cursor:pointer;padding:.3rem}
.cartdrawer-close:hover{color:var(--ink)}
.cartdrawer-close:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.cartlist{list-style:none;overflow-y:auto;flex:1;padding:var(--space-md) var(--space-lg)}
.cartrow{display:flex;gap:var(--space-md);padding:var(--space-md) 0;border-bottom:1px solid var(--line)}
.cartrow:last-child{border-bottom:0}
.cartrow img.cartimg,.cartrow div.cartimg{width:56px;height:56px;flex:none;border-radius:5px;object-fit:cover;background:var(--plate,var(--surface))}
.cartrow div.cartimg{display:flex;align-items:center;justify-content:center;color:var(--faint);font-size:20px}
.cartrowbody{flex:1;min-width:0}
.cartwho{font:600 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.cartbean{font:700 13.5px/1.3 var(--fd);margin:.2rem 0;overflow-wrap:anywhere}
.cartrowfoot{display:flex;align-items:center;justify-content:space-between;gap:var(--space-sm);margin-top:.3rem}
.cartprice{font:700 13px/1 var(--fu);font-variant-numeric:tabular-nums}
.qtybox{display:flex;align-items:center;gap:.4rem}
.qtybtn{width:22px;height:22px;line-height:20px;text-align:center;font:700 14px/1 var(--fu);color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:4px;cursor:pointer;padding:0}
.qtybtn:hover{border-color:var(--ember)}
.qtyval{min-width:1.2em;text-align:center;font-variant-numeric:tabular-nums;font:600 13px/1 var(--fu)}
.cartrowactions{display:flex;align-items:center;justify-content:space-between;gap:var(--space-sm);margin-top:.4rem}
.cartbuy{font:700 12px/1 var(--fu);color:var(--ember);text-decoration:none;border-bottom:1px solid transparent}
.cartbuy:hover{border-bottom-color:var(--ember)}
.cartremove{font:600 11.5px/1 var(--fu);color:var(--faint);background:none;border:0;cursor:pointer;padding:0;text-decoration:underline}
.cartremove:hover{color:var(--ink)}
.cartrow.unavailable{opacity:.6}
.cartrow.unavailable .cartbean::after{content:" — "attr(data-unavail);color:var(--faint);font:600 11px var(--fu);text-transform:none;letter-spacing:0}
.cartempty{padding:var(--space-2xl) var(--space-lg);text-align:center;color:var(--muted);font:500 13.5px/1.5 var(--fu)}

/* /cart page (docs/plans/universal-cart.md rung 1, #54): grouped-by-roaster
   totals + handoff. Reuses .cartlist/.cartrow/.qtybox/.cartempty grammar
   above verbatim (the drawer's buildCartRow renders the rows here too) — new
   rules below are page chrome only (h1/groups/subtotal/grand total), no new
   colour; .cartcheckout reuses the already-approved .fetchcta button look. */
.cartpage{max-width:60rem;margin:0 auto}
.carth1{font:800 clamp(1.9rem,4vw,2.6rem)/1.02 var(--fd);letter-spacing:-.02em;color:var(--ink);margin-bottom:var(--space-sm)}
.cartsub{color:var(--muted);font:500 13.5px/1.5 var(--fu)}
.cartpageempty{padding:var(--space-2xl) 0;text-align:center;color:var(--muted);font:500 14px/1.6 var(--fu)}
.cartpageempty .fetchcta{margin-top:var(--space-md)}
.cartgroup{margin-top:var(--space-xl);padding-top:var(--space-lg);border-top:1px solid var(--line)}
.cartgroup:first-child{margin-top:var(--space-lg)}
.cartgroup h2{font:700 clamp(1.1rem,2vw,1.3rem)/1.2 var(--fd);margin-bottom:var(--space-sm)}
.cartgroup h2 a{color:inherit;text-decoration:none}
.cartgroup h2 a:hover{color:var(--ember)}
.cartgroup .cartlist{padding:0;overflow:visible}
.cartgroupfoot{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-md);padding-top:var(--space-md)}
.cartsubtotal{font:600 13.5px/1 var(--fu);color:var(--muted);display:flex;gap:.4em;margin-right:auto}
.cartsubtotal b{color:var(--ink);font-variant-numeric:tabular-nums}
.cartshiphint{width:100%;order:1;font:500 12px/1.5 var(--fu);color:var(--faint)}
/* rung 1.5 commission disclosure (docs/plans/cart-affiliate-attribution.md
   v1 item 3) — dormant today (site.js's COMMISSIONED_ROASTERS is empty), so
   this rule has no live instance to render yet. Same small-print treatment
   as .cartshiphint, one row below it when both are present. */
.cartdisclose{width:100%;order:2;font:500 12px/1.5 var(--fu);color:var(--faint)}
.cartsummary{margin-top:var(--space-xl);padding-top:var(--space-lg);border-top:3px solid var(--ink)}
.cartgrand{display:flex;align-items:baseline;justify-content:space-between;font:700 15px/1 var(--fu);color:var(--ink)}
.cartgrand b{font:800 1.4rem/1 var(--fd);font-variant-numeric:tabular-nums}
.carthonest{color:var(--muted);font:500 13px/1.5 var(--fu);margin-top:var(--space-sm)}

/* /my-coffees (#213, epic #206): page chrome only, verbatim copy of the
   /cart page's own h1/sub/empty-state rules just above (.cartpage/.carth1/
   .cartsub/.cartpageempty) — same page shape, no new visual language. The
   log itself renders in .cards, the existing tile grid already used
   everywhere else. .mycnote is the one genuinely new rule: a quiet,
   --faint caption for the user's own private note, same treatment as
   .listed/.unverified elsewhere on a card. */
.mycpage{max-width:60rem;margin:0 auto}
.mych1{font:800 clamp(1.9rem,4vw,2.6rem)/1.02 var(--fd);letter-spacing:-.02em;color:var(--ink);margin-bottom:var(--space-sm)}
.mycsub{color:var(--muted);font:500 13.5px/1.5 var(--fu)}
.mycpageempty{padding:var(--space-2xl) 0;text-align:center;color:var(--muted);font:500 14px/1.6 var(--fu)}
.mycpageempty .fetchcta{margin-top:var(--space-md)}
.mycgrid{margin-top:var(--space-lg)}
.mycnote{font:500 11px/1.5 var(--fu);color:var(--faint);margin-top:var(--space-xs)}

/* wayfinding: one hairline break between the "what changed" feed (new/drops/
   restock/featured) and the full browsable catalog (radar) below it — the
   only structural cue separating the two, per the anti-decoration rule. */
.feedbreak{position:relative;margin:var(--space-3xl) 0 0;border-top:1px solid var(--line);text-align:center}
.feedbreak span{position:relative;top:-.6em;background:var(--paper);padding:0 var(--space-md);font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}

/* quiet sections collapse to one status line each — no acres of empty paper */
.quietgroup{margin-top:var(--space-xl);border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:var(--space-sm) var(--space-lg) var(--space-xs)}
/* task #2: label the collapsed group so it reads as intentional, not disabled */
.quietcap{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--muted);padding:var(--space-xs) 0 var(--space-sm);border-bottom:1px solid var(--line)}
/* homepage declutter: when the cap is the only line (nothing else quiet
   left to list, #polish item 3), drop the divider it normally draws above
   the per-section rows — a rule with nothing under it reads as a bug. */
.quietcap:only-child{border-bottom:0;padding-bottom:var(--space-xs)}
.quietstep{margin-top:var(--space-xs);color:var(--muted);font-size:13px}
section.quiet{display:flex;align-items:baseline;gap:var(--space-md);padding:var(--space-sm) 0;border-bottom:1px solid var(--line)}
section.quiet:last-child{border-bottom:0}
section.quiet h2{font:700 14px/1.4 var(--fd);letter-spacing:-.01em;white-space:nowrap;min-width:8.5rem;color:var(--ink)}
section.quiet .empty{font:500 13px/1.5 var(--fu);color:var(--muted);padding:0}

.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:var(--space-md)}
.card{position:relative;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);border-radius:6px;cursor:pointer;transition:border-color .15s ease-out,transform .15s cubic-bezier(.22,1,.36,1)}
.card:hover{border-color:var(--muted);transform:translateY(-2px);z-index:2}
.card[hidden]{display:none}
.card:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.thumb{width:100%;height:auto;max-height:150px;object-fit:contain;background:var(--paper);border-radius:6px 6px 0 0}
.thumb.noimg{height:150px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}
.thumb.noimg img{width:64px;height:64px;object-fit:contain;display:block;border-radius:4px}
.noimg-t{font:600 10.5px/1 var(--fu);color:var(--faint);text-transform:uppercase;letter-spacing:.07em}
div.thumb{display:flex;align-items:center;justify-content:center;color:var(--faint);font:700 30px/1 var(--fd)}
/* roaster-card banner: blurred cover backdrop + contained foreground, so wide
   photos go full-bleed and logos sit on a soft blur of themselves — never
   cropped, never a bare letterbox gap. Banners are og:images of mixed shapes;
   the box height comes inline (--b carries the image url). */
.rbanner{position:relative;overflow:hidden;border-radius:6px 6px 0 0;background:var(--surface)}
.rbanner::before{content:"";position:absolute;inset:0;background:var(--b) center/cover no-repeat;filter:blur(16px);transform:scale(1.15);opacity:.5}
.rbanner img{position:relative;width:100%;height:100%;object-fit:contain;display:block}
.evchip{position:absolute;top:var(--space-sm);left:var(--space-sm);font:700 10.5px/1 var(--fu);letter-spacing:.06em;text-transform:uppercase;color:var(--paper);border-radius:99px;padding:.3rem .55rem}
.ev-new .evchip{background:var(--ember)}
.ev-drop .evchip{background:var(--drop)}
.ev-restock .evchip{background:var(--restock)}
/* #49: tile "deal" pill sits alongside new/drop/restock. .tilechips replaces
   .evchip's own absolute positioning with a flex column so a bean that is
   both e.g. "new" AND has a live discount stacks two pills instead of them
   overlapping — .dealchip reuses the ember accent (DESIGN.md decision log
   2026-07-07: "deal" events join the trio via ember, no 4th color). */
.tilechips{position:absolute;top:var(--space-sm);left:var(--space-sm);display:flex;flex-direction:column;align-items:flex-start;gap:6px;z-index:1}
.tilechips .evchip{position:static}
.evchip.dealpill{background:var(--ember)}
.card .body{padding:var(--space-md);display:flex;flex-direction:column;gap:var(--space-xs);flex:1;border-top:1px solid var(--line)}
.who{font:700 10.5px/1.3 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}
.bean{font:600 15.5px/1.25 var(--fd);letter-spacing:-.01em}
/* Reference tier (#212, docs/design-decisions/2026-07-25-reference-tier-card-
   styling.md): a commodity/off-radar bag resolved via Open Food Facts. Reuses
   .card — no new card type, no side-stripe. It never carries an .evchip (that
   absence, on a card type that otherwise looks identical to a radar tile, is
   the strongest "not tracked" tell); the off-radar pill uses the existing
   .srcpill grammar instead — already styled generically, no rule needed here
   — rendered as the .body's first child, reading as the evchip's replacement
   without copying its absolute over-the-image positioning. It also opts out
   of the standard .card hover lift/pointer cursor — nothing here is a link
   to a bean page — and desaturates the product photo so a real bag still
   reads as a real bag, just visibly drained of the radar's color pop.
   Markup source: internal/site/assets/site.js buildReferenceCard (#213,
   /my-coffees) and internal/site/assets/scan.js's /scan reference-hit
   renderer (#212) — both emit this same .card.reference shape. */
.card.reference{cursor:default}
.card.reference:hover{border-color:var(--line);transform:none}
.card.reference .thumb.refimg{height:150px;overflow:hidden}
.card.reference .thumb.refimg img{display:block;width:100%;height:100%;object-fit:contain;filter:grayscale(60%) saturate(55%)}
.card.reference .bean{color:var(--faint)}
.card.reference .refattr{font:600 10.5px/1.3 var(--fu);color:var(--muted)}
/* #233: compact aggregate-star badge (catalog tile + coffee-page "more like
   this" related cards) — hidden entirely when a bean has no reviews yet
   (Rating.HasData() gate in the template, not CSS), so an unreviewed card is
   pixel-identical to before this shipped. Same ember-filled/faint-unfilled
   star convention as the #231 coffee-page reviews section and #228 roaster
   shopRating line (stars() template func) — no new color, no pill, no
   side-stripe. Kept as its own .cardrating class rather than reusing
   .revstars (now defined in this shared stylesheet, below) because this is a
   deliberately compact 11px variant vs .revstars' 15px — same ember-filled/
   faint-unfilled convention, smaller footprint for the tile — see
   docs/design-decisions/2026-07-18-reviews-section.md. */
.cardrating{display:flex;align-items:center;gap:4px;font-size:11px;letter-spacing:1px}
.cardrating .on{color:var(--ember)}
.cardrating .off{color:var(--faint)}
.cardrating-n{font:600 10px/1 var(--fu);letter-spacing:normal;color:var(--muted);font-variant-numeric:tabular-nums}
/* #138 (founder unify, 2026-07-15): the preview tag block now adopts the
   coffee-page spec-plate's CELL grammar, not just its typeface — an etched
   surface + 135° hatch + hairline-divided cells (the same instrument look as
   .specplate), capped to one compact wrapping row (a preview, not the full
   plate). The tier-coloured origin bubbles and uniform 99px pill radii (both
   DESIGN.md anti-patterns, the "AI slop" the founder flagged) are gone; no new
   colour. tabular-nums + uppercase micro-type mirror .sp-cell. No overflow:
   hidden — border-radius alone clips the hatch (as on .specplate), so the #187
   .tip bubbles hosted by the roast/decaf/origin cells still escape the plate. */
.tags{display:flex;flex-wrap:wrap;border:1px solid var(--line);border-radius:6px;background-color:var(--paper);background-image:repeating-linear-gradient(135deg,oklch(from var(--ink) l c h / 5%) 0 1px,transparent 1px 7px)}
.tcell{display:flex;align-items:center;gap:4px;padding:.3rem .5rem;border-right:1px solid var(--line);font:700 9.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.07em;color:var(--ink);font-variant-numeric:tabular-nums}
.tcell:last-child{border-right:0}
.tcell sup{font-size:8px;font-weight:600;margin-left:1px;color:var(--faint)}
.mic{margin-left:.4em;vertical-align:-3px;color:var(--muted)}
.micon{width:15px;height:15px}
/* per-roaster hide/only (#16, F1 recoverability pass #128): quiet icon
   buttons in the tile's roaster row. --muted (not --faint) as the idle
   color plus a soft background on hover/focus reads as an actual control
   instead of decoration — the founder-flagged "too small/unclear" fix.
   On mobile the box grows to a real 44px touch target (not just the visual
   glyph), same size discipline as .hidebtn's mobile floor below. */
.rhide,.ronly{margin-left:.35em;border:0;background:none;padding:0;color:var(--muted);cursor:pointer;font-size:11px;line-height:1;vertical-align:-2px;width:18px;height:18px;border-radius:99px;display:inline-flex;align-items:center;justify-content:center;transition:color .15s ease-out,background-color .15s ease-out}
.rhide:hover,.rhide:focus-visible{color:var(--drop);background:var(--drop-soft)}
.ronly:hover,.ronly:focus-visible{color:var(--ember);background:var(--ember-soft)}
@media(max-width:640px){.rhide,.ronly{width:44px;height:44px;font-size:15px}}
/* hide/only status line: never a dead end — stays put under the toolbar as
   long as anything is hidden or an "only" is active; undo/show-all always
   one tap away. Same chip vocabulary as the rest of the toolbar. Restyled
   (#128 F1) as a bordered surface, not bare text, so the recovery action
   reads as a persistent panel rather than a line easy to miss. */
/* Host wrapper is inserted after .toolbar (a full-width sibling), so it must
   re-apply the same 76rem/auto/side-padding cage or the bar goes full-bleed on
   desktop and left-aligns to the viewport edge instead of the content column. */
.hidebars{max-width:76rem;margin:0 auto;padding:0 var(--space-lg)}
.hidebar{display:none;align-items:center;gap:var(--space-sm);flex-wrap:wrap;margin-top:var(--space-sm);padding:var(--space-sm) var(--space-md);border:1px solid var(--line);border-radius:6px;background:var(--surface);font:600 12px/1.4 var(--fu);color:var(--muted)}
.hidebtn{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:99px;padding:.35rem .7rem;cursor:pointer;transition:border-color .15s ease-out}
.hidebtn:hover{border-color:var(--muted)}
.hidebtn.primary{border-color:var(--ember);color:var(--ember)}
.hidebtn.primary:hover{background:var(--ember-soft)}
@media(max-width:640px){.hidebtn{min-height:44px}}
/* empty-state recovery panel (#128 F1): only ever shown when hiding/only
   leaves zero visible beans anywhere on the page — a blank feed must never
   be the only thing a visitor sees. Same bordered-surface family as
   .hidebar/.suggest, ember CTA matches .fetchcta's weight without reusing
   its exact class (button, not an anchor). */
.hideempty{display:none;margin-top:var(--space-sm);border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:var(--space-lg);text-align:left}
.hideemptyhead{font:700 14px/1.5 var(--fu);color:var(--ink)}
.hideemptybtn{display:inline-block;margin-top:var(--space-md);font:700 13px/1 var(--fu);color:var(--paper);background:var(--ember);border:0;border-radius:6px;padding:.7rem 1.1rem;cursor:pointer;transition:opacity .15s ease-out}
.hideemptybtn:hover{opacity:.85}
.hideemptysub{margin-top:var(--space-sm);font:500 12.5px/1.6 var(--fu);color:var(--muted)}
@media(max-width:640px){.hideemptybtn{min-height:44px}}
.spark{color:var(--ember);margin-top:var(--space-sm)}
.sparkcap{font:600 10.5px/1.6 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}
.desc{font:400 15px/1.65 var(--fu);max-width:65ch;margin-top:var(--space-lg);color:var(--ink)}
.cwrap{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);gap:var(--space-2xl);margin-top:var(--space-xl);align-items:start}
/* hero must never crop the pack shot: contain, not cover */
.cwrap .thumb{height:auto;max-height:420px;object-fit:contain;background:var(--surface);border:1px solid var(--line);border-radius:6px}
@media (prefers-color-scheme:dark){.cwrap .thumb{background:var(--plate)}}
.cwrap .thumb.noimg{height:280px}
.cwrap .thumb.noimg img{width:100px;height:100px}
.cwrap .noimg-t{font-size:11.5px}
.gstrip{display:flex;gap:var(--space-sm);margin-top:var(--space-sm);flex-wrap:wrap}
.gthumb{width:64px;height:64px;object-fit:cover;border:1px solid var(--line);border-radius:4px;cursor:pointer;background:var(--surface);padding:0;transition:border-color .15s ease-out}
.gthumb.on,.gthumb:hover{border-color:var(--ink)}
.gthumb:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
.gthumb img{width:100%;height:100%;object-fit:cover;border-radius:3px;display:block}
.soldby{display:flex;align-items:center;gap:var(--space-sm);margin-top:var(--space-lg);font:500 13px/1.5 var(--fu);color:var(--muted)}
.soldby img{width:26px;height:26px;object-fit:contain;border:1px solid var(--line);border-radius:4px;padding:2px;background:var(--surface)}
.soldby a{color:var(--ember)}
@media(max-width:760px){.cwrap{grid-template-columns:1fr}}
.numbers{margin-top:auto;padding-top:var(--space-sm)}
.price{font:700 16px/1.2 var(--fu)}
.was{color:var(--faint);text-decoration:line-through;font-weight:500;font-size:12.5px;margin-right:.45em}
.delta{font:700 11.5px/1.5 var(--fu)}
.ev-drop .delta{color:var(--drop)}
.ev-restock .delta{color:var(--restock)}
.ev-new .delta{color:var(--ember)}
.norm{font:500 11.5px/1.5 var(--fu);color:var(--muted)}
.listed{font:500 11px/1.5 var(--fu);color:var(--faint)}
/* #315 stale-scrape freshness reading — reuses the .listed faint meter voice, no new color/chip */
.unverified{font:500 11px/1.5 var(--fu);color:var(--faint)}
.buy{font:600 12.5px/1 var(--fu);color:var(--ember);text-decoration:none;border-bottom:1.5px solid currentColor;padding-bottom:1px;transition:opacity .15s ease-out}
.buy:hover{opacity:.75}
/* prominent secondary CTA: ember-outline, hard 6px edge, so the inspector's
   link to the richer full coffee page reads as an obvious affordance instead
   of the buried plain-text link it replaces (founder UX feedback, 2026-07-08) */
.fullpage{display:inline-flex;align-items:center;gap:.3rem;font:700 12.5px/1 var(--fu);color:var(--ember);text-decoration:none;border:1.5px solid var(--ember);border-radius:6px;padding:.55rem .85rem;margin-bottom:var(--space-sm);transition:background .15s ease-out,color .15s ease-out;min-height:44px}
.fullpage:hover{background:var(--ember);color:var(--paper)}

/* split view — list keeps flowing left, focused item lives in a sticky
   inspector on the right (asymmetric 5:3). no modals, no overlays. */
.layout{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-xl);align-items:start}
.layout.split{grid-template-columns:minmax(0,5fr) minmax(18rem,3fr)}
.inspect{display:none}
.layout.split .inspect{display:block;position:sticky;top:calc(var(--header-h, 0px) + var(--secnav-h, 0px) + var(--space-md));max-height:calc(100vh - var(--header-h, 0px) - var(--secnav-h, 0px) - var(--space-md) - var(--space-lg));overflow:auto;background:var(--surface);border:1px solid var(--muted);border-radius:6px;animation:slidein .2s cubic-bezier(.22,1,.36,1)}
@keyframes slidein{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.layout.split .inspect{animation:none}}
.inspect .thumb{height:360px;max-height:none;border-bottom:1px solid var(--line);object-fit:contain;background:var(--surface)}
.inspect .body{padding:var(--space-lg);display:flex;flex-direction:column;gap:var(--space-xs);border-top:0}
.inspect .bean{font-size:19px}
.inspect .detail{border-top:1px solid var(--line);padding:var(--space-lg);background:var(--paper);display:block}
.inspect .evchip{position:static;align-self:flex-start;margin-bottom:var(--space-xs)}
.card.sel{border-color:var(--ink);transform:none}
.card .detail{display:none}
/* ≤860px there is no split view: tapping a tile navigates to /coffee/<slug>/
   instead (the sticky inspector 8000px up-page was invisible from deep in the
   list — P0, 2026-07-05 review). The detail panel stays server-rendered for
   the coffee page's own use; it is never shown inline here. */
@media(max-width:860px){
.layout.split{grid-template-columns:1fr}
.layout.split .inspect{display:none}
}
/* chips: wrapper is layout-invisible on desktop, a scrollable rail on phones
   (10 chips stacked 3 rows deep ate a third of the first screen) */
.chiprail{display:contents}
@media(max-width:640px){
.chiprail{display:flex;flex:1 1 100%;gap:var(--space-xs);overflow-x:auto;flex-wrap:nowrap;padding-bottom:2px;-webkit-overflow-scrolling:touch;scrollbar-width:none;scroll-snap-type:x proximity}
.chiprail::-webkit-scrollbar{display:none}
.chiprail .chip{flex:0 0 auto;scroll-snap-align:start;min-height:44px}
}
/* phone toolbar, header stack and origin belt — same 640px density line as
   the chip rail above, no third breakpoint. Search goes full-width on its own
   row (16px kills iOS zoom-on-focus); the belt collapses to its chip fallback
   since the 4.5px SVG labels are unreadable at 390px; the meter drops to a
   full-width ticker line; every control clears the 44px tap floor. */
@media(max-width:640px){
.find{flex-basis:100%;font-size:16px}
.sortsel{min-height:44px}
.beltmap,.beltcap .belthelp{display:none}
.railhelp .tip{max-width:min(240px,70vw);white-space:normal}
.bchip{min-height:44px}
.meter{flex:1 1 100%;text-align:left;order:3}
.lsw a{padding:16px 12px;margin:-16px -12px}
.fullpage{min-height:44px}
}
/* ≤640px density — 2-col compact tile grid (1 column shows ~1.5 tiles at
   375px, too airy for a ticker; 2 columns keep ~4 on screen), smaller
   thumbs/stars/padding, plus per-page trims: the coffee hero can't fill the
   first screen, the vtable drops its redundant availability column (the .oos
   dimming + avail pill already carry it), roaster chrome tightens. */
@media(max-width:640px){
.cards{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-sm)}
#featured .cards{gap:var(--space-sm)}
#featured .cards .card{flex:0 0 150px}
.thumb{height:110px}
.card .body{padding:var(--space-sm)}
.star{width:34px;height:34px}
.cartadd{width:34px;height:34px;right:calc(34px + var(--space-sm) + 16px)}
/* 44px tap targets (the 2026-07-05 mobile-first line) without growing the
   chips: two 44px circles would eat a 110px thumb, so the visual stays 34px
   and an invisible ::after carries the touch area. The sibling gap goes 6px →
   16px because 44px areas centred 40px apart overlap — at 16px the centres sit
   50px apart and the two targets clear each other by 6px.
   Scoped to `.card>` on purpose: `.detail .cartadd` / `.foot .cartadd` reset to
   position:static, so an absolute ::after there would anchor to some ancestor
   and drop a stray click-catcher on the page. */
.card>.star::after,.card>.cartadd::after{content:"";position:absolute;top:50%;left:50%;width:44px;height:44px;transform:translate(-50%,-50%);border-radius:99px}
.cwrap .thumb{max-height:48vh}
.vtable th:nth-child(2),.vtable td:nth-child(2){display:none}
.brandbar{height:120px}
.stats{gap:var(--space-lg)}
.stat b{font-size:22px}
}

.vtable{width:100%;border-collapse:collapse;font:500 12.5px/1.55 var(--fu)}
.vtable th{font:700 10.5px/1.3 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint);text-align:left;padding:.3rem .5rem .3rem 0;border-bottom:1px solid var(--line)}
.vtable td{padding:.4rem .5rem .4rem 0;border-bottom:1px solid var(--line)}
.vtable td:last-child,.vtable th:last-child{text-align:right;padding-right:0}
.oos{color:var(--faint)}
.detail .foot{display:flex;gap:var(--space-lg);flex-wrap:wrap;margin-top:var(--space-md);font:500 12px/1.6 var(--fu);color:var(--muted);align-items:baseline}
.detail .foot a{color:var(--ember)}
.close{font:600 11.5px/1 var(--fu);color:var(--muted);background:none;border:1px solid var(--line);border-radius:99px;padding:.35rem .65rem;cursor:pointer;margin-left:auto;transition:all .15s ease-out}
.close:hover{color:var(--ink);border-color:var(--muted)}
.empty{color:var(--faint);font:500 13px/1.6 var(--fu);padding:var(--space-sm) 0}
.nomatch{display:none;color:var(--muted);font:500 13.5px/1.6 var(--fu);padding:var(--space-lg) 0}
.nomatch a{color:var(--ember)}
.fetchcta{display:inline-block;font:700 14px/1 var(--fu);color:var(--paper)!important;background:var(--ember);border-radius:6px;padding:.7rem 1.1rem;text-decoration:none;transition:opacity .15s ease-out}
.fetchcta:hover{opacity:.85}

.crumb{font:600 12.5px/1 var(--fu);color:var(--muted);text-decoration:none;display:inline-block;margin-top:var(--space-lg);transition:color .15s ease-out}
.crumb:hover{color:var(--ink)}
/* profile brandbar: a fixed strip, always cover-cropped. Only genuinely wide
   art belongs here — site.js swaps anything squarer than 2:1 (logos, pack
   shots) for the roastery placeholder rather than butchering it. */
.brandbar{height:170px;overflow:hidden;background:var(--surface)}
.brandbar img{width:100%;height:100%;object-fit:cover;object-position:center;display:block}
.profile{display:flex;gap:var(--space-lg);align-items:center;flex-wrap:wrap;margin-top:var(--space-xl)}
.profile h1{font:800 clamp(1.7rem,3.6vw,2.4rem)/1.05 var(--fd);letter-spacing:-.025em}
.brandlogo{width:60px;height:60px;object-fit:contain;background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:5px}
.profile .domain{font:600 13px/1 var(--fu);color:var(--ember);text-decoration:none;border-bottom:1.5px solid currentColor;padding-bottom:1px}
.claim{font:700 10px/1 var(--fu);text-transform:uppercase;letter-spacing:.08em;color:var(--muted);border:1px dashed var(--faint);border-radius:6px;padding:.4rem .65rem;text-decoration:none;transition:all .15s ease-out}
.claim:hover{color:var(--ink);border-color:var(--ink)}
.claim.claimed{color:var(--drop);border:1px solid var(--drop);display:inline-flex;align-items:center;gap:.35em}
.claim.manage{border-style:solid}
.beancheck{width:15px;height:15px;flex:none;overflow:visible}
.pblurb{font:400 14.5px/1.6 var(--fu);color:var(--muted);max-width:62ch;margin-top:var(--space-md)}
.pmeta{display:flex;flex-wrap:wrap;gap:var(--space-lg);font:600 12.5px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-md)}
.pmeta a{color:var(--ember);text-decoration:none}
.pmeta a:hover{border-bottom:1.5px solid currentColor}
.locmeta{flex-basis:100%;order:99}
.locmeta>summary{cursor:pointer;list-style:none;color:inherit}
.locmeta>summary::-webkit-details-marker{display:none}
.locmeta>summary:hover{border-bottom:1.5px solid currentColor}
.locpanel{margin-top:var(--space-sm);padding:var(--space-md);border:1px solid var(--line);border-radius:6px;max-width:26rem}
.locactions{display:flex;flex-wrap:wrap;gap:var(--space-sm)}
.locaction{display:inline-flex;align-items:center;font:700 12px/1 var(--fu);color:var(--ember);text-decoration:none;border:1.5px solid var(--ember);border-radius:6px;padding:.5rem .85rem;background:none;cursor:pointer;transition:all .15s ease-out}
.locaction:hover{background:var(--ember);color:var(--paper)}
.locaction:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
/* Mobile: every location deep-link/copy control clears the 44px tap floor
   (DESIGN.md mobile-first pass, 2026-07-05). */
@media(max-width:640px){.locaction{min-height:44px}}
.contactrow{display:flex;flex-wrap:wrap;gap:var(--space-sm);margin-top:var(--space-md)}
.contactrow a{font:700 12.5px/1 var(--fu);color:var(--ember);text-decoration:none;border:1.5px solid var(--ember);border-radius:6px;padding:.5rem .85rem;transition:all .15s ease-out}
.contactrow a:hover{background:var(--ember);color:var(--paper)}
.promo{margin-top:var(--space-lg);border:1px solid var(--ember);border-left:4px solid var(--ember);border-radius:6px;padding:var(--space-md) var(--space-lg);background:oklch(from var(--ember) l c h / .06);font:500 14px/1.5 var(--fu);color:var(--ink);display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-sm)}
.promocode{font:700 13px/1 var(--fm,monospace);background:var(--surface);border:1px dashed var(--ember);border-radius:4px;padding:.3rem .5rem;color:var(--ember);display:inline-flex;align-items:center;gap:.4rem;cursor:pointer}
.promocode .pcode{filter:blur(4px);user-select:none;letter-spacing:.1em}
.promocode.revealed .pcode{filter:none;user-select:text}
.promocode .plabel{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.03em;opacity:.75}
/* #49: coffee/roaster-page ticket chip — same reveal/copy grammar as
   .promocode (dashed ember border, blurred code until revealed), ember
   accent per DESIGN.md decision log 2026-07-07 (deal events join the trio
   via ember, no 4th color). */
.dealchip{margin-top:var(--space-md);font:700 13px/1 var(--fu);background:var(--surface);border:1px dashed var(--ember);border-radius:4px;padding:.4rem .65rem;color:var(--ember);display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
.dealchip .dealtxt{font:700 12.5px/1 var(--fu)}
.dealchip .dealleft{font-variant-numeric:tabular-nums}
.dealchip .dcode{font:700 13px/1 var(--fm,monospace);filter:blur(4px);user-select:none;letter-spacing:.1em}
.dealchip.revealed .dcode{filter:none;user-select:text}
.dealchip .deallabel{font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.03em;opacity:.75}
.promo a{color:var(--ember);font-weight:700;text-decoration:none;border-bottom:1.5px solid currentColor}
.lead{margin-top:var(--space-xl);border:1px solid var(--line);border-radius:6px;padding:0 var(--space-lg)}
.lead>summary{font:700 13px/1 var(--fu);color:var(--ink);cursor:pointer;padding:var(--space-md) 0;list-style:none}
.lead>summary::-webkit-details-marker{display:none}
.lead>summary::before{content:"+ ";color:var(--ember)}
.lead[open]>summary::before{content:"– "}
.lead form{display:flex;flex-direction:column;gap:var(--space-sm);padding-bottom:var(--space-lg);max-width:34rem}
.lead label{font:600 11px/1.4 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.lead input,.lead select,.lead textarea{font:400 14px/1.4 var(--fu);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:5px;padding:.6rem .7rem;width:100%}
.lead textarea{min-height:5rem;resize:vertical}
.lead .hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.lead button{font:700 13px/1 var(--fu);color:var(--paper);background:var(--ink);border:0;border-radius:6px;padding:.7rem 1.2rem;cursor:pointer;align-self:flex-start;transition:opacity .15s ease-out}
.lead button:hover{opacity:.85}
.lead button:disabled{opacity:.5;cursor:default}
.lead .msg{font:600 12.5px/1.5 var(--fu);margin-top:.2rem}
.lead .msg.ok{color:var(--drop)}
.lead .msg.err{color:var(--ember)}
.stats{display:flex;gap:var(--space-2xl);flex-wrap:wrap;margin:var(--space-xl) 0 var(--space-sm)}
.stat b{display:block;font:800 26px/1.1 var(--fd);letter-spacing:-.02em}
.stat span{font:700 10.5px/2 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}

/* inline (non-modal) email opt-in beside the Watching strip (#59): built
   entirely by site.js (optinPanel/renderWatching), same bordered-surface
   family as .hidebar/.hideempty; ember CTA per DESIGN.md (accent = wordmark
   dot/buy links/decaf/NEW — the "email me" ask reuses that meaning, not a
   new colour). Declutter pass: compact single-line row (label + input +
   button) instead of a stacked paragraph-then-form — same function, a third
   the height; still wraps to stacked on narrow viewports via flex-wrap. */
.optin{margin-top:var(--space-sm);border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:var(--space-sm) var(--space-md)}
.optinform{display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap;margin:0}
.optinlabel{font:600 12px/1.4 var(--fu);color:var(--muted);flex:1 1 14rem}
.optininput{font:400 13px/1.3 var(--fu);color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:5px;padding:.45rem .6rem;min-width:11rem;flex:1 1 11rem}
.optininput:focus-visible{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.optin .hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
/* #637 alert kind/cadence radios: compact inline row, same density as the
   rest of the opt-in — no new visual language, native controls only. */
.optinprefs{display:flex;align-items:center;gap:var(--space-sm) var(--space-md);flex-wrap:wrap;border:0;margin:0;padding:0;flex:1 1 100%}
.optinprefs legend{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
/* Each of the two choices is its own radiogroup (kind, cadence) so assistive
   tech announces them as two questions, not one four-way pick. */
.optingroup{display:inline-flex;align-items:center;gap:var(--space-sm) var(--space-md);flex-wrap:wrap}
.optinradio{display:inline-flex;align-items:center;gap:.35em;font:400 12px/1.3 var(--fu);color:var(--muted);cursor:pointer}
.optinbtn{font:700 12px/1 var(--fu);color:var(--paper);background:var(--ember);border:0;border-radius:6px;padding:.55rem .85rem;cursor:pointer;transition:opacity .15s ease-out;white-space:nowrap}
.optinbtn:hover{opacity:.85}
.optinbtn:disabled{opacity:.5;cursor:default}
.optinstatus{font:600 12px/1.4 var(--fu);color:var(--muted);margin:var(--space-xs) 0 0}
.optinstatus:empty{margin:0;display:none}
@media(max-width:640px){.optininput,.optinbtn,.optinradio{min-height:44px}}
.totop{position:fixed;right:var(--space-lg);bottom:calc(var(--space-lg) + env(safe-area-inset-bottom,0px));font:700 12px/1 var(--fu);color:var(--paper);background:var(--ink);border:0;border-radius:6px;padding:.7rem 1rem;cursor:pointer;opacity:0;pointer-events:none;transform:translateY(6px);transition:opacity .2s ease-out,transform .2s cubic-bezier(.22,1,.36,1);z-index:5}
.totop.show{opacity:.92;pointer-events:auto;transform:none}
.totop:hover{opacity:1}
.sentinel{height:1px}
/* transient status toast (watchlist cap warning, task #10) */
.toast{position:fixed;left:50%;bottom:calc(var(--space-xl) + env(safe-area-inset-bottom,0px));transform:translate(-50%,var(--space-sm));z-index:60;max-width:calc(100vw - 2*var(--space-lg));text-align:center;font:600 12.5px/1.4 var(--fu);color:var(--paper);background:var(--ink);border-radius:99px;padding:var(--space-sm) var(--space-lg);opacity:0;pointer-events:none;transition:opacity .2s ease-out,transform .2s cubic-bezier(.22,1,.36,1)}
.toast.show{opacity:.97;transform:translate(-50%,0)}
.toast.actionable{pointer-events:auto;display:inline-flex;align-items:center;gap:var(--space-sm)}
.toast .toastact{font:inherit;font-weight:700;color:var(--ember);background:none;border:0;padding:0 var(--space-xs);cursor:pointer;text-decoration:underline}
/* one-question non-converter survey (CRO audit follow-up): a dismissible,
   non-blocking card — never a modal, page stays fully interactive under it. */
.fbcard{position:fixed;right:var(--space-lg);bottom:calc(var(--space-lg) + env(safe-area-inset-bottom,0px));z-index:55;width:min(280px,calc(100vw - 2*var(--space-lg)));background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:var(--space-md) var(--space-lg);box-shadow:0 2px 12px oklch(from var(--ink) l c h / 12%);opacity:0;transform:translateY(var(--space-md));pointer-events:none;transition:opacity .2s ease-out,transform .2s cubic-bezier(.22,1,.36,1)}
.fbcard.show{opacity:1;transform:none;pointer-events:auto}
@media (prefers-reduced-motion:reduce){.fbcard{transition:opacity .2s ease-out}}
.fbclose{position:absolute;top:var(--space-xs);right:var(--space-xs);font:13px/1 var(--fu);color:var(--faint);background:none;border:0;padding:var(--space-sm);cursor:pointer}
.fbclose:hover{color:var(--ink)}
.fbq{font:600 13px/1.4 var(--fu);color:var(--ink);margin:0 var(--space-lg) var(--space-sm) 0}
.fbchips{display:flex;flex-wrap:wrap;gap:var(--space-xs)}
/* the answered-state script sets [hidden] on the chip row; display:flex above
   would otherwise beat the UA [hidden]{display:none} rule and leave the chips
   sitting next to the "thanks" line. */
.fbchips[hidden]{display:none}
.fbchip{font:500 12px/1 var(--fu);color:var(--muted);background:var(--paper);border:1px solid var(--line);border-radius:6px;padding:.45rem .6rem;cursor:pointer}
.fbchip:hover{border-color:var(--ink);color:var(--ink)}
.fbthanks{font:600 13px/1.4 var(--fu);color:var(--drop);margin:0}
@media (prefers-reduced-motion:reduce){.toast{transition:opacity .2s ease-out}}
.card.reveal{animation:reveal .35s cubic-bezier(.22,1,.36,1) both}
@keyframes reveal{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.card.reveal{animation:none}.totop{transition:none}}

/* footer V3 "the instrument plate" (docs/design-decisions, 2026-07-22 —
   founder revisited the V2 "the index" hairline-row pick after seeing both
   live previews): the nav becomes a 5-column hairline-celled plate quoting
   the coffee-page spec-plate grammar — same 135° hatch + hairline cell
   dividers as .tags/.specplate above — a founder-blessed extension of the
   #138 tile-unification precedent ("a legible miniature of the instrument")
   to a new surface. <footer class="fplate"><div class="finner"> wraps four
   rows: .ftop (brand + meter — the header-scoped .pulse dot's second
   sanctioned site, same signature/reduced-motion handling as the header,
   not a new motion), the plate nav, .fbottom (page-specific fine print +,
   on JS-bundle pages, the email-capture form), .fbase (tagline + legal
   links). Plate collapses 5→2 columns ≤980px, 2→1 ≤640px. */
footer{font:500 12px/1.75 var(--fu);color:var(--muted)}
footer a{color:var(--ember)}
.fplate{margin-top:var(--space-3xl)}
.fplate .finner{max-width:76rem;margin:0 auto;padding:0 var(--space-lg) var(--space-2xl)}
.ftop{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:var(--space-sm);padding-bottom:var(--space-md)}
.fbrand{font:800 1.35rem/1 var(--fd);letter-spacing:-.02em}
.fbrand a{color:inherit;text-decoration:none}
.fbrand span{color:var(--ember)}
.fmeter{font:600 11.5px/1.6 var(--fu);color:var(--faint)}
.fmeter b{color:var(--muted);font-weight:700}
/* the plate: 1px --line "grout" grid; cells carry the paper + hatch, same
   recipe as .tags/.specplate above. */
.plate{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;background:var(--line);border:1px solid var(--line);border-radius:6px;overflow:hidden}
.pcell{background-color:var(--paper);background-image:repeating-linear-gradient(135deg,oklch(from var(--ink) l c h / 5%) 0 1px,transparent 1px 7px);padding:var(--space-lg)}
.pcell .flabel{font:700 10.5px/1.4 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint);display:block}
.pcell ul{list-style:none;padding:0;margin:var(--space-sm) 0 0;display:flex;flex-direction:column;gap:5px}
.pcell a{font:500 13px/1.5 var(--fu)}
/* nav links stay muted like the rest of the footer, ember + underline only
   on hover/focus, so the fine-print paragraph stays the one thing that
   reads as important — the fine-print row's own inline links sit outside
   <nav> and keep the plain footer a{color:var(--ember)} treatment above. */
footer nav a{color:var(--muted);text-decoration:none;border-bottom:1px solid transparent;transition:color .15s ease-out,border-color .15s ease-out}
footer nav a:hover,footer nav a:focus-visible{color:var(--ember);border-bottom-color:currentColor}
.fbottom{display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:var(--space-lg);border-top:1px solid var(--line);margin-top:var(--space-xl);padding-top:var(--space-lg)}
.fbottom .fdisc{margin:0;max-width:34rem}
.fbase{display:flex;justify-content:space-between;flex-wrap:wrap;gap:var(--space-sm);margin-top:var(--space-xl)}
.fbase a{color:var(--faint);text-decoration:none}
.fbase a:hover{color:var(--ember)}
@media(max-width:980px){.plate{grid-template-columns:repeat(2,1fr)}}
@media(max-width:640px){.plate{grid-template-columns:1fr}}
/* footer email capture (#516) — same "email me" ask, same restraint as
   .optin: a plain form, ember button (6px radius per the 2026-07-17 CTA
   de-pill sweep), no new colour. Sits inline in .fbottom's content column
   (label + input row side by side, wrapping); .fsub-wrap holds
   footersignup's two sibling elements (the form + the status paragraph JS
   toggles in) as one flex item so .fbottom's space-between doesn't spread
   them apart. Only the layout context has changed across footer recuts —
   wireFsub's markup/IDs are untouched. */
.fsub-wrap{display:flex;flex-direction:column;gap:var(--space-xs)}
.fsub{display:flex;flex-flow:row wrap;align-items:center;gap:var(--space-sm) var(--space-lg);text-align:left}
/* the success path sets [hidden] on the form; display:flex above would
   otherwise beat the UA [hidden]{display:none} rule and leave the form on
   screen next to "check your inbox" — same trap as .fbchips below. */
.fsub[hidden]{display:none}
.fsub-label{font:500 12px/1.5 var(--fu);color:var(--muted);max-width:32rem}
.fsub-row{display:flex;gap:var(--space-xs)}
.fsub-input{font:400 13px/1.4 var(--fu);color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:6px;padding:.55rem .7rem;min-width:12rem}
.fsub-input:focus-visible{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.fsub .hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.fsub-btn{font:700 12px/1 var(--fu);color:var(--paper);background:var(--ember);border:0;border-radius:6px;padding:.6rem .9rem;cursor:pointer;transition:opacity .15s ease-out}
.fsub-btn:hover{opacity:.85}
.fsub-btn:disabled{opacity:.5;cursor:default}
.fsub-status{font:600 12px/1.5 var(--fu);color:var(--muted);margin:var(--space-xs) 0 0}
.fsub-status:empty{margin:0;display:none}
@media(max-width:640px){.fsub-input,.fsub-btn{min-height:44px}}

/* ⋯ roaster menu popover — same fixed + body-detached pattern as .sortlist.
   The JS (openRoasterMenu) reparents .rmenupop to <body> so it escapes any
   .card overflow and can be positioned with getBoundingClientRect. */
.rmenuwrap{position:relative;display:inline-block}
.rmenu{font:700 14px/1 var(--fu);color:var(--muted);background:none;border:0;padding:0 .2rem;cursor:pointer;user-select:none}
.rmenu:hover,.rmenu:focus-visible{color:var(--ink)}
.rmenu[aria-expanded="true"]{color:var(--ember)}
.rmenupop{position:fixed;z-index:1000;list-style:none;margin:0;padding:4px;min-width:150px;background:var(--surface);border:1px solid var(--line);border-radius:8px;font:600 12px/1 var(--fu);color:var(--ink);box-shadow:0 4px 12px rgba(0,0,0,.08);display:none}
.rmenupop.open{display:block}
.rmenupop .rmenuitem{display:block;width:100%;text-align:left;border:0;background:none;padding:.55rem .7rem;border-radius:5px;cursor:pointer;white-space:nowrap}
.rmenupop .rmenuitem:hover{background:var(--ember-soft)}
.rmenupop .rmenuitem:focus{background:var(--ember-soft);outline:none}

/* price index page (/price-index/, #330) */
main.pi{max-width:44rem}
.pihead{font:800 clamp(1.7rem,4vw,2.3rem)/1.1 var(--fd);letter-spacing:-.02em;margin-top:var(--space-lg)}
main.pi>.sub{font:500 14px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-sm);max-width:52ch}
.pibig{margin-top:var(--space-xl);padding:var(--space-xl);background:var(--surface);border:1px solid var(--line);border-radius:6px}
.pinum{font:800 clamp(2.6rem,9vw,4.2rem)/1 var(--fd);letter-spacing:-.02em;color:var(--ember)}
.piunit{font:700 .38em/1 var(--fu);color:var(--muted);letter-spacing:0}
.picap{font:500 13px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-sm)}
.pitrend{font:600 12.5px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-xs)}
.piroasts,.pimethod{margin-top:var(--space-2xl)}
.piroasts h2,.pimethod h2{font:700 clamp(1.1rem,2vw,1.3rem)/1.1 var(--fd);letter-spacing:-.015em}
.picards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-md);margin-top:var(--space-md)}
.picard{padding:var(--space-md);background:var(--surface);border:1px solid var(--line);border-radius:6px}
.pilabel{font:600 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
.pival{font:700 1.4rem/1.3 var(--fd);margin-top:var(--space-xs)}
.picount{font:500 12px/1.5 var(--fu);color:var(--muted);margin-top:2px}
.pimethod p{font:400 13.5px/1.6 var(--fu);color:var(--muted);max-width:64ch;margin-top:var(--space-sm)}

/* cup index page (/cup-index/, #278) — reuses the .pi* grammar above; only
   the monthly-trend chart wrapper and the methodology version tag are new. */
.cichart{margin-top:var(--space-lg);color:var(--ember)}
.cichart svg{width:100%;height:64px;display:block}
.civer{font:600 11px/1 var(--fu);color:var(--faint);letter-spacing:.04em;font-variant-numeric:tabular-nums;text-transform:none}
/* cup index share/embed (#278 analytics): understated meter-native controls,
   not ember — this is metadata about the reading, not a buy/commerce CTA. */
.ciactions{display:flex;flex-wrap:wrap;gap:var(--space-sm);margin-top:var(--space-lg)}
.ciaction{font:600 11.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.04em;color:var(--muted);border:1px solid var(--line);border-radius:6px;padding:.5rem .85rem;background:var(--surface);cursor:pointer;transition:all .15s ease-out}
.ciaction:hover{color:var(--ink);border-color:var(--muted)}

/* non-bean inventory extra cards (task 3) */
.card.extra{display:flex;flex-direction:column}
.card.extra .face{width:100%;aspect-ratio:1;object-fit:contain;background:var(--paper,#fdf8f2)}
.card.extra .body{padding:.6rem .75rem;display:flex;flex-direction:column;gap:.35rem}
.card.extra .etitle{font-size:.95rem;line-height:1.25;margin:0}
.card.extra .eprice{font-weight:700}
.card.extra .ebuy{font-size:.85rem}
.card.extra.oos{opacity:.6}
.feed.extras h2{margin-top:1.5rem}

/* #231: native reviews section (coffee page) — same .beandesc heading
   grammar (uppercase micro-label + hairline underline), hairline-divided
   review rows (no side-stripe borders, no pills — stars reuse the existing
   ember accent instead of a generic yellow widget, per DESIGN.md "stars
   must fit the meter aesthetic"). See docs/design-decisions for the full
   rationale. Shared here (not page-scoped) since #228/#232 reuse the same
   grammar verbatim on the roaster page. */
.reviews{margin-top:var(--space-2xl);max-width:70ch}
.reviews h2{font:700 13px/1 var(--fu);text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:var(--space-lg);padding-bottom:var(--space-sm);border-bottom:1px solid var(--line)}
.revsummary{display:flex;align-items:baseline;gap:var(--space-sm);flex-wrap:wrap;margin-bottom:var(--space-lg)}
.revavg{font:800 22px/1 var(--fd);font-variant-numeric:tabular-nums}
.revstars{font-size:15px;letter-spacing:1px}
.revstars .on,.revitem-stars .on{color:var(--ember)}
.revstars .off,.revitem-stars .off{color:var(--faint)}
.revcount{font:600 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.revlist{display:flex;flex-direction:column}
.revitem{padding:var(--space-md) 0;border-top:1px solid var(--line)}
.revitem:first-child{border-top:0;padding-top:0}
.revitem-head{display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap}
.revitem-stars{font-size:12px;letter-spacing:1px}
.revitem-title{font:700 13px/1.3 var(--fd)}
.revitem-body{margin:var(--space-xs) 0 0;font:500 13px/1.5 var(--fu);color:var(--ink)}
.revitem-meta{margin-top:var(--space-xs);font:600 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--faint)}
/* #228: roaster-page own-rating readout — reuses .revstars/.revavg/.revcount
   verbatim (same instrument as the coffee-page reviews section, #231) so a
   shop-level number reads as the same system as a product-level one; only
   the flex wrapper + attribution link are new. */
.shoprating{display:flex;align-items:baseline;gap:var(--space-sm);flex-wrap:wrap;margin:var(--space-sm) 0 0}
.shopratingsrc{font:600 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);text-decoration:none}
.shopratingsrc:hover{color:var(--ember)}
/* #232: roaster-page recent-reviews feed attribution line ("reviews via
   judge.me") — reuses .shopratingsrc's muted micro-label typography, no
   link (this feed already sits under its own #reviews h2, unlike the
   header ShopRating line which doubles as the only pointer to its source). */
.revsrc{font:600 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-top:var(--space-sm)}
