:root{
  --paper:oklch(96.5% .013 78);--surface:oklch(98.6% .008 80);--ink:oklch(24% .022 55);
  /* --faint was 60% L (#897e74): 3.58:1 on paper, 3.80:1 on surface — below the
     4.5:1 AA floor on every one of its ~136 text runs (origin, per-250g price,
     freshness hints). Dropped to 54% L, the lightest value that clears 4.5:1 on
     the WORSE of the two grounds (paper 4.6:1, surface 4.9:1), so the hierarchy
     step below --muted survives. Dark mode's --faint already passes (5.35:1). */
  --muted:oklch(47% .025 60);--faint:oklch(54% .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)}
h1,h2,h3,.bean,.profile,.pblurb,.bstory,.locaddr,.pcell{overflow-wrap:break-word;word-break:normal}
::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}
/* type="search" inputs (#hq, .find, .rfind, .scaninput) get a native
   browser clear-button once they have a value — an unstyled OS-rendered
   glyph (blue on Chrome/Safari) that DESIGN.md's "decoration level:
   intentional... nothing else" doesn't allow. Not restyled: the native
   pseudo-element renders differently per browser and isn't reliably
   themeable with CSS alone, which would trade one inconsistency for
   several. Hidden outright — every one of these inputs already has its own
   way to clear (select-all+delete, or a dedicated clear affordance like
   .hscan's neighboring "/" hint area), so nothing here loses a capability,
   only an unstyled one. -ms- covers old Edge/IE; harmless elsewhere. */
input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
input[type="search"]::-ms-clear{display:none}

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:500 12px/1.45 var(--fu);color:var(--muted);letter-spacing:.01em;flex:1 1 100%;order:3;display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-xl);padding-top:var(--space-sm);border-top:1px solid var(--line);text-align:left}
.meter b{color:var(--ink);font-weight:700}
.meter a{color:inherit}
.metafresh{margin-left:auto;color:var(--faint);white-space:nowrap}
/* margin-left:auto collects the buyer utilities (language · watching · cart)
   into ONE cluster hard against the meter instead of letting
   justify-content:space-between strew them across the header's leftover
   width. Six items evenly spaced read as six decisions; two blocks —
   [wordmark + search] … [utilities + meter] — read as a header. */
.hutils{display:flex;align-items:center;gap:var(--space-md);margin-left:auto}
.lsw{font:600 11.5px/1 var(--fu);color:var(--muted)}
.lsw a{color:var(--muted);text-decoration:none}
.lsw a:hover{color:var(--ink)}
.lsw b{color:var(--ink)}
.watchchip,.cartlink{font:700 11.5px/1 var(--fu);color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;gap:.3em;min-height:32px}
.watchglyph{font-size:15px;color:var(--muted)}
.cartglyph{display:none;width:18px;height:18px;flex:none}
.watchchip:hover,.cartlink:hover{color:var(--ember)}
.watchchip:focus-visible,.cartlink:focus-visible{outline:2px solid var(--ember);outline-offset:2px;border-radius:2px}
.watchlabel,.cartlabel{font-weight:600;color:var(--muted)}
.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 4.6rem .55rem .75rem;min-height:38px;transition:border-color .15s ease-out}
/* All search inputs share one selector list so an input added later (including .optininput, which site.js injects at 2141) picks up --faint rather than silently falling back to the browser's neutral grey. */
.hq::placeholder,.rfind::placeholder,.scaninput::placeholder,.find::placeholder,.optininput::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}
/* .hscan — persistent scan-a-bag trigger, left of .hkey's "/" hint (which
   hides on focus; .hscan does not, it's a destination, not a shortcut).
   28px hit area meets the 44px mobile touch-target minimum only once the
   mobile media query below enlarges it — 28px is the ink-density-appropriate
   size at desktop's tighter header row. */
.hscan{position:absolute;right:2.35rem;top:50%;transform:translateY(-50%);width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--muted);border-radius:6px;transition:color .15s ease-out}
.hscan svg{width:18px;height:18px}
.hscan:hover,.hscan:focus-visible{color:var(--ember)}
.hscan:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
/* 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}
.hscan{width:44px;height:44px;right:2.35rem}
}

/* Compact header begins at tablet width, where the full utility labels and
   two-line telemetry previously fought for the same row. Navigation keeps
   the first two bands; operational reassurance gets a quiet third band.

   768, not 900 (#1407): this pairs with the reading band's min-width:769px
   below so one boundary decides "phone shape" vs "desktop shape". At 900 the
   two disagreed — a 900px viewport got the desktop rhythm (min-width:641px),
   this phone header, AND the phone single-column hero, i.e. icon-only header
   utilities and a ~370px hero with ~500px of unused width beside it. The
   641px rhythm boundary and the 640px phone block stay where they are: they
   carry 97+ rules between them and moving those would be a re-layout, not a
   boundary fix. */
@media(max-width:768px){
header.site{align-items:center;padding:var(--space-lg) var(--space-lg) var(--space-sm)}
.wordmark{flex:0 1 auto}
.hutils{order:1;gap:var(--space-xs)}
.hsearch{order:2;flex-basis:100%;max-width:none;margin:var(--space-sm) 0 0}
.hq{font-size:16px;min-height:44px}
.hscan{width:44px;height:44px;right:2.35rem}
.watchchip,.cartchip,.cartlink{display:flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0 5px}
.watchlabel,.cartlabel{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.cartglyph{display:block}
.meter{padding-top:var(--space-xs);border-top:0;gap:var(--space-md)}
.lsw a{padding:16px 12px;margin:-16px -12px}
}

.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}

/* readings strip: the three event counts, lifted above the fold so the first
   thing read is what the instrument saw, not its control panel. Numbers wear
   the event trio; each reading states its own window because "new" is a
   rolling 7 days while drops/restocks are this sweep. No pulse here — the
   header meter owns the one motion signature. .rlatest (the newest arrival)
   used to live in here too; it is now a sibling — see min-width:901px below
   for why. */
.readings{max-width:76rem;margin:0 auto;padding:var(--space-md) var(--space-lg) 0}
/* gauge cluster: three readings on one instrument face, hairline-divided like
   the spec-plate's cells rather than spaced apart as loose stat tiles — a
   1px --line rule (not a colour, not a status stripe) so the strip reads as
   one reading head with three needles instead of three unrelated numbers. */
.rlist{display:flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}
.rread{padding-right:var(--space-xl)}
.rread+.rread{padding-left:var(--space-xl);border-left:1px solid var(--line)}
/* .rstill is the same reading with nothing to jump to: a zero-count event
   renders no section, so the anchor would be dead. Same box, no affordance. */
.rread a,.rread .rstill{display:flex;flex-direction:column;gap:2px;text-decoration:none;color:inherit;transition:transform .15s cubic-bezier(.22,1,.36,1)}
.rr-new .rstill .rnum,.rr-drops .rstill .rnum,.rr-restocked .rstill .rnum{color:var(--faint)}
/* Whole reading moves as one object on hover/focus — matches the .card lift
   signature instead of recolouring only .rlab, which used to leave .rnum's
   event-trio colour inert and read as a half-applied hover. The numeral's
   colour stays put on purpose: it is the event trio's meaning, not a hover
   accent, so it never repaints to ember. */
.rread a:hover,.rread a:focus-visible{transform:translateY(-2px)}
.rnum{font:700 clamp(1.6rem,3.4vw,2.2rem)/1 var(--fd);font-variant-numeric:tabular-nums;letter-spacing:-.02em}
.rr-new .rnum{color:var(--ember)}
.rr-drops .rnum{color:var(--drop)}
.rr-restocked .rnum{color:var(--restock)}
.rlab{font:700 10.5px/1.2 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--ink)}
.rwin{font:500 10.5px/1.2 var(--fu);color:var(--muted)}
.rread a:hover .rlab{color:var(--ember)}
.rread a:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:4px}
/* newest arrival sits below its own hairline — a readout under the
   proposition, not a fourth stat crowding the readings row. Below
   min-width:901px it still sits under the gauges (a sibling in the same
   single-column stack); above it, it sits under the tagline+CTA instead —
   see min-width:901px below. */
.rlatest{font:500 12.5px/1.5 var(--fu);color:var(--muted);margin:var(--space-lg) 0 0;padding-top:var(--space-sm);border-top:1px solid var(--line)}
.rlkey{font:700 10.5px/1.2 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--muted);margin-right:var(--space-xs)}
.rlatest a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--line)}
.rlatest a:hover{color:var(--ember);border-bottom-color:var(--ember)}
.rlsep{color:var(--faint)}
/* quiet day: the meter reads real zeros instead of turning into a sentence
   (DESIGN.md "a meter that happens to be about coffee" — a meter that
   vanishes at zero isn't one), so .rlist above always renders. .rsweep is
   the one thing the old prose line carried that the numbers alone don't:
   the sweep timestamp, so a reader still gets proof the sweep ran. */
.rsweep{font:500 12.5px/1.5 var(--fu);color:var(--faint);margin:var(--space-xs) 0 0}
.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)}
/* "try:" seeds — worked examples of what the search box understands, aimed at
   origin intent. Real links to generated pages, so they work with JS off and
   stay crawlable; .origlinks below still carries the full origin list.
   flex-basis:auto (not 100%) so this shares a row with .filttoggle instead
   of forcing its own line — .find sits before both but is hidden on any page
   with a catalog grid (site.js: the header search box drives it in-place),
   so the row was otherwise just .filttoggle alone against a near-empty
   width. Phone keeps its own order/flex override below (max-width:640px). */
.tryrow{flex:1 1 auto;font:500 12.5px/1.6 var(--fu);color:var(--muted);margin:0}
.tryrow a{color:var(--muted);text-decoration:none;border-bottom:1px solid var(--line)}
.tryrow a:hover{color:var(--ember);border-bottom-color:var(--ember)}
.trysep{color:var(--line)}
/* filter disclosure: the 21-chip rail starts collapsed so proof precedes the
   control panel. .chiprail sets display:contents on desktop, which as an
   author rule beats the UA [hidden]{display:none} — without this explicit
   rule the "hidden" rail renders in full and nothing catches it. */
.filttoggle{display:inline-flex;align-items:center;gap:.4em;font:600 12px/1 var(--fu);border:1px solid var(--line);background:var(--surface);color:var(--muted);border-radius:6px;padding:.55rem .75rem;cursor:pointer;transition:all .15s ease-out}
/* Caret rotates open instead of swapping glyphs — a smooth turn reads as one
   affordance opening, where the old instant ▸→▾ swap was a jump cut. */
.filttoggle::before{content:"▸";color:var(--faint);display:inline-block;transition:transform .15s cubic-bezier(.22,1,.36,1)}
.filttoggle[aria-expanded="true"]::before{transform:rotate(90deg)}
.filttoggle:hover{border-color:var(--muted);color:var(--ink)}
.filttoggle:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
.chiprail[hidden]{display:none}
.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: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 10.5px/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}}

/* Anchor destinations must clear both sticky layers. Without this offset,
   scrollIntoView() aligns a feed's top with the viewport and its heading is
   covered by the site header + section nav, making the first card row appear
   to slide underneath the navigation. The live heights are published by
   site.js and also cover direct /#section fragment navigation. */
.page-home section[id]{scroll-margin-top:calc(var(--header-h, 0px) + var(--secnav-h, 0px) + var(--space-md))}

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)}
/* #1405 pass 1: promoted from a 13px muted utility-face caption to the page's
   only h1 — display face, ink, a size between the wordmark and .feed h2 so
   the hierarchy (wordmark > h1 > h2 > body) stays legible. max-width comes
   down from 52ch because the wider glyphs of the display face at this size
   made 52ch read as a ragged, overlong line. */
h1.tagline{font:500 clamp(1.25rem,2.2vw,1.375rem)/1.3 var(--fd);color:var(--ink);margin:var(--space-sm) 0 0;max-width:45ch}
/* #1405 pass 1: .homeconsole stopped splitting into two columns anywhere.
   The old min-width:901px grid gave .homediscovery a ~40%-width right column
   for three short lines (filter & sort / try: / browse by origin), vertically
   centred against a full-height hairline it shared no baseline with. That
   verdict still holds — .homediscovery stacks below .homebrief in source
   order at every width, full measure, unsplit, and .homebrief itself still
   carries no width rule of its own (see min-width:901px below for why).
   What changed (fold-uses-its-width, real data): the READING band itself —
   tagline, readings strip, NEWEST — left roughly 40% of a ≥1249px viewport
   empty, because the readings strip is three short numbers, not three
   sentences. So the band regains a split, via an inner wrapper scoped to
   that band only — see min-width:901px below. */
/* Desktop vertical rhythm for the seven bands above the fold (header · rule ·
   tagline · readings · NEWEST · toolbar+try · browse-by-origin · jumpnav).
   Every gap between them was 4–16px, so the eye read seven equal siblings
   instead of the three groups they actually are:

     A  identity + status   header chrome, closed by the 3px rule
     B  the reading         tagline · readings strip · NEWEST
     C  the controls        filter & sort · try: · browse by origin · jumpnav

   Proximity has to carry the grouping, so this is a strict two-step scale —
   12px WITHIN a group, 24px BETWEEN groups (both on the 4pt token scale, a
   2:1 ratio, the smallest step that reads as a boundary rather than a
   slightly bigger gap). Below min-width:901px, B is still one vertical
   stack (tagline · readings strip · NEWEST) and this rhythm is what holds it
   together; at min-width:901px the readings strip moves beside the tagline
   (below), so B's own vertical run shortens to tagline+CTA+NEWEST, but the
   same A→B / B→C proximity still applies to whatever remains stacked. The
   one exception stays: .rlatest keeps 16px above its hairline, because a
   rule needs more air than a plain stacked line. */
@media(min-width:641px){
h1.tagline{margin-top:var(--space-xl)}      /* A → B */
/* column-gap only (not the row-gap the base 4px `gap` shorthand also sets):
   once .find is hidden (site.js, any grid page) and .tryrow shares a row
   with .filttoggle (see .tryrow above), the base 4px between a bordered
   button and a word read as a collision, not a gap. Scoped to ≥641px so the
   phone density line (max-width:640px), which already tunes this same row
   for fold space, keeps its own tighter 4px untouched. */
.toolbar{padding-top:var(--space-xl);column-gap:var(--space-md)} /* B → C */
.tryrow{margin-top:var(--space-sm)}         /* 4px row gap was tighter than
                                               the two controls are related */
.origlinks{padding-bottom:var(--space-xl)}  /* C → sticky jumpnav */
}
/* fold-uses-its-width: with real data (41 roasters, 935 beans) the reading
   band's left half — tagline, CTA, NEWEST — read as a crowded column while
   roughly 40% of a ≥1249px viewport sat empty beside it, because the
   readings strip is three numbers, not three sentences. .briefband (a new
   wrapper around .homebrief's children) becomes the shared 76rem/gutter
   container — the job .taglinewrap and .readings used to do alone
   (tagline-alignment.spec.ts) — and splits into two columns: the
   proposition (tagline+CTA, then NEWEST below it) on the left,
   the readings strip on the right. Right-column content sits at the row's
   top edge (align-items:start), never centred — pass 1 dropped a
   justify-content:center for exactly that reason (#1405) and it does not
   come back. .rlatest is a DOM sibling of .readings (not nested inside it
   any more, see template.go) so it can sit in the left column while .rlist
   sits in the right one; it is placed on its own row under the tagline so
   visual order still matches source order. .stale gets its own full-width
   row above both columns — a page-level warning, not a column's content —
   which is the one place source order and visual order part ways here: it
   sits second in the DOM (after .taglinewrap) but first in the row order,
   because a mid-band interruption would break the two-row shape below.
   Below this width everything reverts to the plain single-column
   source-order stack, untouched.

   769, not 901 (#1407): the mirror of the compact header's max-width:768px
   above. Together they make a single tablet boundary, so no viewport can get
   a phone header wrapped around a two-column reading band. */
@media(min-width:769px){
/* .briefband, not .homebrief, carries the container+grid: pocket-sweep-
   journeys.spec.ts pins .homebrief and .homediscovery as two full-bleed
   siblings with matching x/width (#1405) — putting max-width/margin/grid on
   .homebrief itself would decentre it from .homediscovery, which has no
   such rule and stays full-bleed. .briefband is the wrapper that owns the
   centring and the split instead, and carries nothing below this width. */
.briefband{max-width:76rem;margin:0 auto;padding:0 var(--space-lg);display:grid;grid-template-columns:minmax(0,58%) minmax(0,1fr);column-gap:var(--space-2xl);align-items:start}
.taglinewrap{grid-row:2;grid-column:1;max-width:none;margin:0;padding:0}
/* .stale's base rule (used full-bleed on every other page) centres itself
   with margin:auto on both sides — harmless when it's the only thing in its
   row, but inside this grid the auto margins stop it stretching to fill its
   spanned column and instead shrink-wrap+centre it, so it floats mid-band
   instead of starting at the shared gutter with h1/.filttoggle/.origlinks.
   Zeroing the side margins here (not in the base rule, which every other
   page still relies on) lets the grid's own stretch default take over. */
.stale{grid-row:1;grid-column:1/-1;margin:var(--space-md) 0 0}
.readings{grid-row:2;grid-column:2;max-width:none;margin:0;padding:var(--space-xl) 0 0}
.rlatest{grid-row:3;grid-column:1}
}
.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 10.5px/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 10.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}}
/* #1404: the three tile action buttons are wrapped in .tileacts (see the
   "tile" partial in internal/site/template.go) instead of being individually
   position:absolute against .card. #1383 made .page-home .card transparent
   at phone widths, which left the old per-button absolute positioning
   floating three bare circles over the thumbnail with nothing under them —
   grouping them means phone can drop absolute positioning altogether (a real
   row in flow) while desktop keeps the classic over-the-thumb cluster, now
   carried by the wrapper's own backdrop instead of the .card surface #1383
   removed. Each button keeps position:relative (not absolute) purely so its
   own 44px touch-area ::after (below, phone only) still centers on the
   button's own box rather than on whichever ancestor happens to be
   positioned. DOM order matches visual order so keyboard focus follows what is seen. */
.tileacts{position:absolute;top:var(--space-sm);right:var(--space-sm);z-index:1;display:flex;flex-direction:row;align-items:center;gap:6px;padding:4px;border-radius:99px;background:oklch(from var(--paper) l c h / 88%);backdrop-filter:blur(2px)}
/* watch star: quiet on the thumb, ember when armed */
.star{position:relative;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, inside .tileacts */
.cartadd{position:relative;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)}
/* my-coffees log: the third control in the same row, same chip language as
   .star/.cartadd — no new colour token, no new shape (#1152). */
.tilemyc{position:relative;width:26px;height:26px;border-radius:99px;border:1px solid var(--line);background:var(--surface);color:var(--muted);font:600 12px/1 var(--fu);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease-out}
.tilemyc:hover{color:var(--ember);border-color:var(--ember)}
.tilemyc:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.tilemyc[aria-pressed="true"]{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);background:none;border:0;padding:0;cursor:pointer;display:inline-flex;align-items:center;gap:.3em;min-height:32px}
.cartchip[hidden]{display:none}
.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}
.cartdrawer-status{margin:var(--space-sm) var(--space-lg) 0;padding:var(--space-sm);font:600 12px/1.45 var(--fu);color:var(--ember);border:1px solid var(--ember);border-radius:4px}
.cartdrawer-status:empty{display:none}
/* 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}
/* One frame, many photos. `height:auto;max-height:150px` let every tile size
   its image block to whatever the roaster's photo happened to be, so a grid of
   bag shots, flag graphics and lifestyle photos read as an assorted scrape
   rather than one instrument's readings. Fixed 150px + contain on the shared
   --plate ground normalizes the RHYTHM without touching the photograph: no
   crop, no wash, no duotone — buyers identify beans by real bag colour, and a
   radar that recolours its own readings is lying about them. `.noimg` already
   used this exact height, so placeholder and photo tiles now match. */
.thumb{width:100%;height:150px;object-fit:contain;background:var(--plate);border-radius:6px 6px 0 0}
/* A product photo whose URL went dead after we stored it — sharedHead's
   capture-phase error handler swaps in the placeholder glyph and adds this.
   The glyph is artwork, not a photo, so it is inset onto the plate at roughly
   the size the authored div.thumb.noimg plate gives it rather than stretched
   across the whole slot. */
img.thumb.imgdead{height:150px;padding:22%;box-sizing:border-box;opacity:.9}
.cwrap img.thumb.imgdead{height:280px}
.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 10.5px/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 10.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)}
/* drop and restock keep their event colour because their delta carries the
   substantive number ("↓ €2.00"); the chip cannot say that, so the colour
   belongs on the line. A "new" tile is the opposite case: the .evchip pill
   already spends ember on the top-left of the thumb, and the delta only adds
   the age ("first seen 2d ago"). Two ember, two bold, for one fact reads as
   emphasis competing with itself, so the age drops to --muted and supports
   the pill instead of shouting alongside it (#1408). */
.ev-drop .delta{color:var(--drop)}
.ev-restock .delta{color:var(--restock)}
.ev-new .delta{color:var(--muted)}
.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; every control clears
   the 44px tap floor. */
@media(max-width:640px){
.find{flex-basis:100%;font-size:16px}
/* Readings stay one row at 390px. Wrapping them cost ~50px of fold and put
   the first card below it — the strip only earns its place if it is cheaper
   than the chip rail it replaced, so the type shrinks instead of stacking.
   The window labels stay: they are the honesty, not decoration. */
.readings{padding-top:var(--space-sm)}
.rlist{flex-wrap:nowrap}
.rread{flex:1 1 0;min-width:0;padding-right:var(--space-md)}
.rread+.rread{padding-left:var(--space-md)}
.rnum{font-size:1.5rem}
/* 10.5px, not 9.5: DESIGN.md pins micro-labels at 10.5px and that is the
   smallest step in the committed scale. Shrinking below it to buy fold space
   traded legibility for layout — the readings are the point of the strip. */
.rlab{font-size:10.5px;letter-spacing:.06em}
.rwin{font-size:10.5px}
.rlatest{margin-top:var(--space-md);padding-top:var(--space-xs);font-size:12px}
/* seeds and the filter disclosure share one row on phones — as two full-width
   rows they cost 44px of fold each and pushed the first card off screen. */
.tryrow{flex:1 1 auto;order:1}
.filttoggle{min-height:44px;flex:0 0 auto;order:2}
.sortsel{min-height:44px}
.beltmap,.beltcap .belthelp{display:none}
.railhelp .tip{max-width:min(240px,70vw);white-space:normal}
.bchip{min-height:44px}
/* Phone header = three legible bands, not six free-floating items.
   Row 1  wordmark ......................... EN·NL  ☆  cart
   Row 2  search (full width)
   Row 3  meter (full width, left-aligned ticker)

   Everything that is not the wordmark, the search box, or the meter is
   *utility*: language, watching, cart. Grouped tight at the end of row 1
   they read as one quiet cluster; scattered across their own rows (the
   previous arrangement) each one announced itself as a separate band, and
   "owner login" — bottom-aligned by header.site's align-items:flex-end —
   landed on the meter's first line at the opposite edge, which read as a
   broken layout rather than a link. align-items:center on the phone stack
   removes that whole class of stranding.

   Left alignment is the other half of the fix, and since the meter band
   ships left-aligned on every viewport it needs no phone override: the
   meter, the tagline, the readings and the cards below all start at the
   same left gutter. One grid line for the whole first screen. */
header.site{align-items:center;padding:var(--space-lg) var(--space-lg) var(--space-sm)}
.wordmark{flex:0 1 auto}
.hutils{order:1;gap:var(--space-xs)}
.watchchip,.cartchip,.cartlink{display:flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0 5px}
.watchlabel{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.cartchip,.cartlink{min-width:50px}
.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}
.tilemyc{width:34px;height:34px}
/* #1404: below the density line where .page-home strips the .card surface,
   there is nothing left for three absolutely-positioned circles to float
   over, so the cluster comes out of position:absolute and joins the flow —
   in source order that puts it between the thumb and .body, i.e. a row
   under the thumbnail. justify-content:flex-end right-aligns it, matching
   where the cluster used to hug the card's right edge. The backdrop/blur
   from the base rule is desktop-only chrome for reading over a photo the
   flow row no longer sits on top of, so it is switched off here. */
/* 44px tap targets (the 2026-07-05 mobile-first line) without growing the
   chips: three 44px circles would eat a 110px thumb, so the visual stays 34px
   and an invisible ::after carries the touch area. The gap goes 6px → 16px
   because 44px areas centred 40px apart overlap — at 16px the centres sit
   50px apart and adjacent targets clear each other by 6px.
   Scoped to `.tileacts>` on purpose: `.detail .cartadd` / `.foot .cartadd`
   reset to position:static and are never inside a .tileacts wrapper, so this
   can't drop a stray click-catcher on those buttons. */
.tileacts{position:static;justify-content:flex-end;gap:16px;padding:0 var(--space-sm);margin-top:var(--space-xs);background:none;backdrop-filter:none;border-radius:0}
.tileacts>.star::after,.tileacts>.cartadd::after,.tileacts>.tilemyc::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}
.profile{gap:var(--space-sm) var(--space-md);margin-top:var(--space-lg)}
.profile h1{flex:1 1 calc(100% - 76px);min-width:0}
.profile .domain{max-width:100%;overflow-wrap:anywhere}
.roasterabout{margin-top:var(--space-lg);padding:var(--space-md) 0}
.loc{align-items:flex-start}
.locmaps,.addressactions{width:100%;flex-wrap:wrap;gap:var(--space-xs)}
.page-coffee .detail .cartadd,.page-coffee .watchdetail,.page-coffee .detail .myc,
.page-roaster .locaddr-link,.page-roaster .locmaps a,.page-roaster .locaction{
  display:inline-flex;align-items:center;justify-content:center;min-height:44px
}
.page-roaster .locaddr-link,.page-roaster .locmaps a{justify-content:flex-start}
}

/* Pocket Sweep journey recut. These rules are deliberately page-scoped and
   phone-only: the real DOM, data attributes and delegated JS contracts stay
   untouched, while the home/watch/search/cart/scan journeys share the
   approved compact reading rhythm instead of switching visual dialects. */
@media(max-width:640px){
.page-home .taglinewrap{padding-top:var(--space-sm)}
.page-home section.feed,.page-search section.feed{margin-top:var(--space-xl)}
.page-home .feed h2,.page-search .feed h2{padding-bottom:var(--space-sm);border-bottom:1px solid var(--ink)}
.page-home .feed h2 .count,.page-search .feed h2 .count{color:var(--muted);background:none;border-radius:0;padding:0;font-size:10.5px}
.page-home .feed>.cards,.page-search .feed>.cards{margin-top:var(--space-md)}
.page-home .card{background:transparent;border:0;border-radius:0}
.page-home .card:hover{transform:none;border-color:transparent}
.page-home .card .thumb,.page-home .card .rbanner{border-radius:6px}
.page-home .card .body{padding:var(--space-sm) 0 0;border-top:0}
.page-home .card .bean{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;text-wrap:pretty}
.page-home #watching{padding-top:var(--space-xs);border-top:1px solid var(--line)}

.page-search .searchhead{margin-top:var(--space-xl);padding-bottom:var(--space-md);border-bottom:1px solid var(--ink)}
.page-search .shq{font:750 1.85rem/1 var(--fd);letter-spacing:-.035em;text-wrap:pretty}
.page-search #sr-coffees .cards{grid-template-columns:1fr;gap:0}
.page-search #sr-coffees .card{display:grid;grid-template-columns:88px minmax(0,1fr);align-items:start;background:transparent;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:var(--space-md) 0;min-width:0}
.page-search #sr-coffees .card:hover{transform:none;border-color:var(--line)}
.page-search #sr-coffees .thumb{grid-column:1;grid-row:1;width:88px;height:126px;border-radius:4px}
/* #1404: the 44px top reserve this padding used to carry for the absolute
   button cluster is gone now that .tileacts moves off the thumb entirely
   (below) — the roaster name goes back to starting level with the thumb's
   top edge instead of 44px under it. */
.page-search #sr-coffees .card .body{grid-column:2;grid-row:1;padding:0 0 0 var(--space-md);border-top:0;min-width:0}
/* Cap the width of price/norm so they cannot enter the action cluster's column.
   The cluster is three 34px buttons with two 16px clearing gaps AND the
   wrapper's own 8px side padding — 150px in all, not the 134px the buttons
   alone suggest — so the subtraction has to include that padding or the cap
   lands 8px inside the cluster. One more --space-sm keeps a visible gap rather
   than a touching edge. Capping width here is preferred over a bottom reserve
   because the cluster is bottom-anchored in the same grid cell, so a vertical
   reserve would add ~27% height to every row to protect one element on the
   rows that happen to render the per-250g line. */
.page-search #sr-coffees .price,.page-search #sr-coffees .norm{max-width:calc(100% - 3 * 34px - 2 * 16px - 2 * var(--space-sm) - var(--space-sm))}
/* #1404: the row is a fixed 88px-thumb | body grid — 88px is too narrow for
   three 34px buttons with the 16px clearing gaps the generic phone flow row
   above uses (134px needed), so the cluster can't just drop into flow under
   the thumbnail the way it does elsewhere. Anchoring it to the body
   column's bottom-right instead keeps it clear of the roaster name/title/
   tags that read top-down in that column, and off the thumbnail now that
   nothing needs the top clearance .tilechips used to reserve below. */
.page-search #sr-coffees .tileacts{position:static;grid-column:2;grid-row:1;justify-self:end;align-self:end;margin-top:var(--space-sm)}
/* The event/deal chip no longer has to clear the action cluster (it moved
   off the thumb entirely, above) — left only tightens the inset for the
   narrower 88px thumb, top reverts to the default .tilechips value. */
.page-search #sr-coffees .tilechips{left:var(--space-xs)}
.page-search .nomatch{padding:var(--space-xl) 0;border-block:1px solid var(--ink);background:transparent;border-radius:0}

.page-cart .cartpage{padding-top:var(--space-xl)}
.page-cart .carth1{font-size:1.9rem}
.page-cart .cartgroup{margin-top:var(--space-xl);padding-top:0;border-top:0}
.page-cart .cartgroup h2{padding-bottom:var(--space-sm);border-bottom:1px solid var(--ink)}
.page-cart .cartrow{padding:var(--space-md) 0}
.page-cart .qtybtn{width:44px;height:44px;line-height:42px}
.page-cart .cartremove,.page-cart .cartbuy{display:inline-flex;align-items:center;min-height:44px}
.page-cart .cartcheckout{min-height:44px}
.page-cart .cartpageempty{padding:var(--space-xl) 0;border-block:1px solid var(--ink);text-align:left;display:flex;flex-direction:column;align-items:flex-start}

.page-scan .scanapp{padding-top:var(--space-sm)}
.page-scan .scanstage{margin-inline:calc(-1 * var(--space-lg))}
.page-scan .scanstatus,.page-scan .scannudge,.page-scan .scancamerr{margin-inline:var(--space-lg)}
.page-scan .scanalt{padding-top:var(--space-sm);border-top:1px solid var(--ink)}
.page-scan .scanres{border-width:1px 0;border-color:var(--ink);border-radius:0;background:transparent;padding:var(--space-xl) 0}
}

.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}
/* #1405 pass 1: the Amsterdam finder CTA is one geo landing page, not worth
   outranking the reading and the whole feed with a filled button — demoted to
   the house text-link grammar (.pmeta a / .locmaps a: ember text, underline
   on hover). Overrides .fetchcta's button chrome on this one anchor instead
   of restyling .fetchcta itself, which still needs to read as a button at its
   other call sites (nomatch, fetch page, roaster claim, cart/myc empty state). */
.findercta{display:inline;font:600 13px/1.5 var(--fu);color:var(--ember)!important;background:none;border-radius:0;padding:0;text-decoration:none}
.findercta:hover{opacity:1;border-bottom:1.5px solid currentColor}

.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 10.5px/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}
/* #1208: roaster-level follow control. A functional CTA, so 6px radius per
   the radius rule above — the circular .star chips are a grandfathered pill
   exception, not a precedent for a new control. flex-basis:100% drops it
   onto its own row inside .profile's flex-wrap layout, below the name/
   domain/claim row (founder-approved mock). The email-upsell panel below it
   reuses .optin/.optinform/.optinlabel/.optininput/.optinbtn/.optinstatus
   verbatim (the per-bean star's own inline Watching-strip opt-in, #59) —
   same visual language, zero new CSS needed for that part. */
.rfollow{flex-basis:100%;display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-xs)}
.rfollowhead{display:inline-flex}
/* The bubble anchors to its own trigger inside the why-line, not to .rfollow
   (which also holds the panel and the count, so a bubble anchored there
   would float below all of them) and no longer to the button (from which it
   opened on top of the panel that same click revealed). Same
   relative-host/absolute-tip pairing .hastip uses. */
.rfollowtiphost{position:relative;display:inline-block;margin-left:.4em}
/* A text-weight trigger, not a second CTA: the primary action on this row is
   the watch button, and DESIGN.md's decoration budget has no room for a
   competing chip. Dotted underline is the site's existing "there is more
   here" cue on .hastip hosts. */
.rfollowtipbtn{background:none;border:0;padding:0;font:inherit;color:var(--muted);text-decoration:underline dotted;text-underline-offset:2px;cursor:help}
.rfollowtipbtn:hover{color:var(--ink)}
.rfollowtipbtn:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
/* The spoken-aloud line: an unwatch, or an email step this page can only
   half-finish. Muted because it reports, it does not ask. */
.rfollowsaid{display:block;font:600 12px/1.4 var(--fu);color:var(--muted)}
/* The panel's own one-line why, so the explanation sits with the action
   instead of floating over it. */
.optinwhy{font:500 12.5px/1.5 var(--fu);color:var(--muted);margin:0 0 var(--space-xs)}
/* border is 1px solid in BOTH states (transparent when unpressed, ember when
   pressed) so the box's rendered size never changes on toggle — a border:0
   -> border:1px swap here used to grow the button 2px per axis on follow and
   jitter back on unfollow. --surface/--ink on pressed reuses the exact same
   pairing .optin's own box already uses sitting directly on --paper (this
   button lives on the roaster page's --paper background too), so it inherits
   that pairing's already-vetted contrast: DESIGN.md's dark-mode paragraph
   requires the paper<->surface gap stay >=5% L (25%/30% today, exactly 5%)
   specifically so a --surface panel never melts into a --paper page, and
   --ink (94% L dark / 24% L light) against --surface (30% dark / 98.6%
   light) clears body-text contrast in both themes — no new token needed. */
.rfollowbtn{display:inline-flex;align-items:center;gap:.4em;font:700 13px/1 var(--fu);color:var(--paper);background:var(--ember);border:1px solid transparent;border-radius:6px;padding:.6rem 1rem;cursor:pointer;min-height:36px;transition:opacity .15s ease-out}
.rfollowbtn:hover{opacity:.85}
.rfollowbtn:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.rfollowbtn[aria-pressed="true"]{background:var(--surface);color:var(--ink);border-color:var(--ember)}
/* The two state glyphs live in the button together and trade places on
   aria-pressed, so the label span is the only thing JS rewrites. The ring
   (radar, watching-not-yet) is an inline SVG for the reason the template
   comment gives; the ON state is the site's OWN signature — the 7px .pulse
   dot from the header meter, recoloured to ember so it reads on the pressed
   --surface fill. That is the whole brand idea of the product in one
   control: "this thing watches this roaster for me". .pulse already stops
   animating under prefers-reduced-motion (see its rule above), and it stays
   the only motion here — no entrance animation per DESIGN.md's motion rule. */
.rfollowring{width:12px;height:12px;flex:none}
.rfollowpulse{display:none;background:var(--ember);margin-right:0}
.rfollowbtn[aria-pressed="true"] .rfollowring{display:none}
.rfollowbtn[aria-pressed="true"] .rfollowpulse{display:inline-block}
/* The why-line is the pre-click half of the explanation: a micro-label at
   the 10.5px uppercase step's sibling size, --muted, so it reads as an
   annotation on the control rather than a second piece of copy competing
   with it. */
.rfollowwhy{font:500 12px/1.4 var(--fu);color:var(--muted);margin:0}
/* Same bubble as .hastip .tip (#187) — repeated rather than shared because
   that rule is scoped to .hastip, whose click-to-toggle delegation this
   control deliberately does not use (see the template comment). left/top
   and the translateX default match it exactly so clampTip's flip and dx
   maths apply unchanged. */
.rfollowtip{display:none;position:absolute;left:50%;top:calc(100% + var(--space-xs));transform:translateX(-50%);z-index:20;width:min(18rem,calc(100vw - 2*var(--space-lg)));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}
.rfollowtip.tipup{top:auto;bottom:calc(100% + var(--space-xs))}
.rfollow.tipopen .rfollowtip{display:block}
.rfollowerr{font:600 12px/1.4 var(--fu);color:var(--ember)}
/* .rfollowcount carries .cicount's own intent (template.go's cafeClaimInfoHTML
   page-scoped <style>, ~line 4294: "display:block;margin-top:var(--space-xs);
   font:600 13px/1.5 var(--fu);color:var(--muted)") — that rule lives in a
   page-scoped <style> block that never reaches /roasters/<id>/, so a span
   borrowing the "cicount" class name here would ship with zero styling. This
   span no longer carries that class at all: it has its own copy keys now
   (the cafe-claim wording addressed a cafe owner, not a buyer), so there is
   nothing left to share and nothing to mistake for a shared rule.
   13px stays hardcoded rather than a --fs-* token: this codebase has no
   font-size custom properties (only --fu/--fd family tokens), and every
   sibling micro-line (.optinstatus, .watchstatus, etc.) hardcodes its px size
   the same way, so a token here would be a one-off, not a fit. */
.rfollowcount{display:block;margin-top:var(--space-xs);font:600 13px/1.5 var(--fu);color:var(--muted)}
.roasterabout{max-width:64ch;margin-top:var(--space-xl);padding:var(--space-lg) 0;border-top:1px solid var(--ink);border-bottom:1px solid var(--line)}
.profilelabel{display:block;font:700 10.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}
.pblurb{font:500 15px/1.6 var(--fu);color:var(--ink);max-width:62ch;margin-top:var(--space-sm)}
.roasterabout .bstory{margin-top:var(--space-sm)}
.watchstatus{flex:1 1 100%;font:600 12px/1.45 var(--fu);color:var(--muted)}
.watchstatus:has(.watchstatusmsg:empty){display:none}
.watchstatuslink{color:var(--ember)}
.cartstatus{flex:1 1 100%;font:600 12px/1.45 var(--fu);color:var(--ember)}
.cartstatus:empty{display:none}
.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}
/* Owner-managed locations (#29). Hairline rows, one per place — the ticker
   density DESIGN.md asks for, not a card per address. No side-stripe. */
.locs{margin-top:var(--space-md);border-top:1px solid var(--line)}
.loc{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-sm) var(--space-md);
  padding:.55rem 0;border-bottom:1px solid var(--line)}
.lockind{font:600 10.5px/1 var(--fu);text-transform:lowercase;letter-spacing:.06em;color:var(--faint)}
.locname{font:600 13.5px/1.3 var(--fu);color:var(--ink)}
.locaddr{font:400 13px/1.4 var(--fu);color:var(--muted);flex:1 1 12rem}
.locaddr-link{color:var(--ink);font-weight:700;text-decoration:none;overflow-wrap:anywhere}
.loc-primary{flex:1 1 100%;width:100%;align-items:center}
.locmaps{display:flex;gap:var(--space-md);font:600 12px/1 var(--fu)}
.locmaps a{color:var(--ember);text-decoration:none}
.locmaps a:hover{border-bottom:1.5px solid currentColor}
.addressactions{display:flex;gap:var(--space-xs);flex-wrap:wrap}
.locphone{font:600 12px/1 var(--fu);color:var(--ember);text-decoration:none}
.locphone:hover{border-bottom:1.5px solid currentColor}
.lochours{font:400 12px/1.4 var(--fu);color:var(--muted)}
.locnote{flex-basis:100%;font:400 12px/1.4 var(--fu);color:var(--faint)}
.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)}
/* No edge tab: the 2026-07-04 v2 re-cut retired 3px/4px event edges in favor
   of pill chips, and this rule kept a 4px left border the sweep missed. The
   1px ember hairline plus the 6% ember wash already read as emphasised. */
.promo{margin-top:var(--space-lg);border:1px 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)}
/* #1406 phase D: a roaster with no successful check yet gets this one quiet
   line in place of .stats' two bold zeros — same slot and vertical rhythm
   as .stats (occupies the same margin box), but --faint and unbolded so
   "never checked" reads as an honest non-answer, not as a headline number. */
.statsquiet{font:600 11px/1.4 var(--fu);text-transform:uppercase;letter-spacing:.08em;color:var(--faint);margin:var(--space-xl) 0 var(--space-sm)}

/* 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,.rfollowbtn{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{min-width:0;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);overflow-wrap:anywhere}
/* 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){
.fplate{margin-top:var(--space-2xl)}
.fplate .finner{padding-bottom:var(--space-xl)}
.plate{grid-template-columns:repeat(2,minmax(0,1fr))}
.pcell:last-child{grid-column:1/-1}
.pcell{padding:var(--space-md)}
.pcell a{font-size:11.5px}
.pcell li a{display:flex;align-items:center;min-height:44px;padding:4px 0}
.ftop{align-items:center}
}
/* 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)}

/* .itable: the intent-page comparison table (PL ChatGPT-search foundation
   work, /{slug}/, main.pi). Same hairline-rule + tabular-nums data
   discipline as the coffee page's .specplate (template.go), NOT a mono
   costume — DESIGN.md retired IBM Plex Mono as an AI-design tell, so this
   uses --fu with font-variant-numeric:tabular-nums (already set globally on
   body per DESIGN.md, restated here for clarity on the price column) —
   applied to a real <table> instead of a chip grid, since a ranked list of
   up to 40 rows is a different shape than a handful of spec tags. */
.itable{width:100%;border-collapse:collapse;margin-top:var(--space-lg)}
.itable th,.itable td{padding:.5rem .75rem;border-top:1px solid var(--line);text-align:left;font:500 13.5px/1.4 var(--fu)}
.itable th{font:700 11px/1 var(--fu);text-transform:uppercase;letter-spacing:.07em;color:var(--muted);border-top:none}
.itable td.itable-price{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--ink);font-weight:600}
.itable td.itable-out{color:var(--faint)}
/* The per-roaster freshness reading sits under the price, not beside it: the
   price cell is nowrap + 600 + tabular-nums, so an inline reading would run
   straight into the figure ("56,00 złniezweryfikowana 3 dni") and inherit
   the numeric weight. Same faint meter voice as the catalog tile's own
   .unverified div. */
.itable td.itable-price .unverified{display:block;margin-top:.2rem;font-weight:500;white-space:normal;font-variant-numeric:normal}

/* 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)}

/* Brand kit (#29) — owner-entered story, credentials and commerce facts. */
.bstory{font:400 14.5px/1.6 var(--fu);color:var(--muted);margin-top:var(--space-md);max-width:52ch}
.bvalues{display:flex;flex-wrap:wrap;gap:var(--space-sm);margin-top:var(--space-md)}
.bval{font:600 11px/1 var(--fu);color:var(--muted);background:var(--surface);
  border:1px solid var(--line);border-radius:99px;padding:.35rem .7rem}
.bfacts{display:flex;flex-wrap:wrap;gap:var(--space-lg);margin-top:var(--space-md);
  font:600 12.5px/1.5 var(--fu);color:var(--muted)}
.bfacts a{color:var(--ember);text-decoration:none}
.bfacts a:hover{border-bottom:1.5px solid currentColor}
.bsocial{display:flex;flex-wrap:wrap;gap:var(--space-lg);margin-top:var(--space-sm);
  font:600 12.5px/1.5 var(--fu)}
.bsocial a{color:var(--ember);text-decoration:none}
.bsocial a:hover{border-bottom:1.5px solid currentColor}

/* Buyer reviews (#29). Signals are a meter row — tabular figures, explicit n,
   because a 5.0 from one rater is not a 4.6 from forty. */
.orevs{margin-top:var(--space-lg);border-top:1px solid var(--line);padding-top:var(--space-md)}
.orevhead{font:600 12px/1 var(--fu);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.orevn{color:var(--faint);text-transform:none;letter-spacing:0}
.osignals{display:flex;flex-wrap:wrap;gap:var(--space-lg);margin-top:var(--space-md)}
.osignal{display:flex;align-items:baseline;gap:var(--space-sm)}
.oslabel{font:400 12.5px/1 var(--fu);color:var(--muted)}
.osval{font:700 15px/1 var(--fu);color:var(--ink);font-variant-numeric:tabular-nums}
.osn{font:400 11px/1 var(--fu);color:var(--faint);font-variant-numeric:tabular-nums}
.orev{margin:var(--space-md) 0 0;padding:0 0 0 var(--space-md);border:0}
.orev p{font:400 14px/1.6 var(--fu);color:var(--ink);margin:0;max-width:56ch}
.orev cite{display:block;margin-top:.3rem;font:600 11.5px/1 var(--fu);
  font-style:normal;color:var(--faint)}
/* #1278: the link into /review/. A quiet hairline affordance, not a primary
   CTA — asking for a rating must not outrank the roaster's own beans on their
   page. 44px min height for the phone tap target. */
.orevcta{display:inline-flex;align-items:center;min-height:44px;margin-top:var(--space-md);
  font:600 11.5px/1 var(--fu);text-transform:uppercase;letter-spacing:.08em;
  color:var(--muted);text-decoration:none;border-bottom:1px solid var(--line)}
.orevcta:hover{color:var(--ember);border-bottom-color:var(--ember)}
.orevcta:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
