: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)}
::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:600 12.5px/1.55 var(--fu);color:var(--muted);text-align:right;letter-spacing:.01em}
.meter b{color:var(--ink);font-weight:700}
.meter a{color:inherit}
.lsw{font:600 11.5px/1 var(--fu);color:var(--muted);margin-right:var(--space-md)}
.lsw a{color:var(--muted);text-decoration:none}
.lsw a:hover{color:var(--ink)}
.lsw b{color:var(--ink)}
/* #716 part 2: persistent header-level login entry point for roaster owners
   / team members (footer's link, reworded by #708, was still undiscoverable
   — ~26 links deep). Quiet utility text, same weight as .lsw, so it reads as
   header chrome not a nav item. Mobile: drops to its own full-width row
   (order:3, after .hsearch's order:2) so it never competes with or shifts
   the search box or the cart chip. */
.hdrlogin{font:600 11.5px/1 var(--fu);color:var(--muted);text-decoration:none}
.hdrlogin:hover{color:var(--ink)}
/* Shares one row with .meter below (order:3, both auto-basis) instead of its
   own full-width line — see the mobile block near .meter for why. */
@media(max-width:640px){.hdrlogin{order:3;flex:0 0 auto;align-self:flex-start;text-align:right;margin-top:var(--space-xs)}}
.pulse{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--drop);margin-right:.45em;animation:pulse 2.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.2}}
@media (prefers-reduced-motion:reduce){.pulse{animation:none}}

/* Header search (docs/plans/coffee-search.md "UX spec"): instrument styling,
   no thumbnails, no entrance motion — same hairline/pill grammar as .find /
   .chip. Sits between the wordmark and the meter block, per DESIGN.md's
   "wordmark left; meter block right" — search is a meter function. */
.hsearch{position:relative;flex:1 1 16rem;max-width:22rem;margin:0 var(--space-lg)}
.hq{width:100%;font:500 13.5px/1 var(--fu);color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:.55rem 4.6rem .55rem .75rem;min-height:38px;transition:border-color .15s ease-out}
.hq::placeholder{color:var(--faint)}
.hq:focus{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.hkey{position:absolute;right:.6rem;top:50%;transform:translateY(-50%);font:600 10.5px/1 var(--fu);color:var(--faint);border:1px solid var(--line);border-radius:4px;padding:.2rem .4rem;pointer-events:none;background:var(--paper)}
.hq:focus~.hkey{display:none}
/* .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}
}

.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 and the newest arrival, 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. */
.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 gauges,
   not a fourth stat crowding the row. */
.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: one honest status line, promoted to the same bordered-surface
   "status card" grammar the empty event sections already use (DESIGN.md
   "quiet sections collapse into one compact status card") — a considered
   empty state instead of a bare sentence floating in the fold. */
.rquiet{font:500 13px/1.5 var(--fu);color:var(--ink);margin:0;background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:var(--space-md) var(--space-lg)}
.rsweep{color:var(--faint)}
.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. */
.tryrow{flex:1 1 100%;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::placeholder{color:var(--faint)}
.find:focus{outline:2px solid var(--ember);outline-offset:-1px;border-color:transparent}
.chip{font:600 12px/1 var(--fu);border:1px solid var(--line);background:var(--surface);color:var(--muted);border-radius:99px;padding:.45rem .75rem;cursor:pointer;transition:all .15s ease-out}
.chip:hover{border-color:var(--muted);color:var(--ink)}
.chip:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
.chip[aria-pressed="true"]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
/* recent searches (F6): a compact chip row under the find bar, shown only on
   focus of an empty search box — never competes with live filter results.
   Base visibility is the inline style="display:none" the template renders
   (same convention as .suggest/.hidebar) — JS toggles it to 'flex' to show,
   so an empty/collapsed panel never eats layout space it shouldn't. */
.recent{flex-basis:100%;flex-wrap:wrap;align-items:center;gap:var(--space-xs);margin-top:var(--space-xs)}
.recentlabel{font:600 11.5px/1 var(--fu);color:var(--muted)}
.recent .chip{padding:.35rem .65rem;font-size:11px}
.obtn{border-color:var(--ember);border-width:2px;color:var(--ink);background:var(--ember-soft);font-weight:700}
.obtn b{font-weight:700}
.obtn:hover{border-color:var(--ember);background:var(--ember-soft);color:var(--ember)}
.obtn:focus-visible{outline:2px solid var(--ember);outline-offset:1px;border-color:var(--ember)}
.belt{max-width:76rem;margin:0 auto;padding:var(--space-md) var(--space-lg) var(--space-lg);border-bottom:1px solid var(--line)}
.beltmap{width:100%;display:block;color:var(--muted)}
.beltcap{display:flex;align-items:baseline;gap:var(--space-sm);font:500 12px/1.5 var(--fu);color:var(--muted);margin-bottom:var(--space-sm)}
.beltcap b{font:700 13px/1.3 var(--fd);color:var(--ink)}
.belthelp{position:relative;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;border:1px solid var(--faint);color:var(--faint);font:700 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}}

section.feed{margin-top:var(--space-2xl)}
.feed h2{font:700 clamp(1.25rem,2.2vw,1.55rem)/1.1 var(--fd);letter-spacing:-.015em;display:flex;align-items:baseline;gap:.55rem}
.feed h2 .count{font:700 12px/1 var(--fu);color:var(--paper);background:var(--muted);border-radius:99px;padding:.25rem .5rem;letter-spacing:.02em;align-self:center}
.feed .sub,main .sub{font:500 12.5px/1.5 var(--fu);color:var(--muted);margin-top:var(--space-xs)}
.taglinewrap{max-width:76rem;margin:0 auto;padding:0 var(--space-lg)}
h1.tagline{font:500 13px/1.5 var(--fu);color:var(--muted);margin:var(--space-sm) 0 0;max-width:52ch}
.buytrust{font:400 12px/1.6 var(--fu);color:var(--faint);margin-top:var(--space-sm);max-width:52ch}
.buytrust a{color:inherit;text-decoration:underline}
.feed>.cards{margin-top:var(--space-lg)}
/* Updates (news-feed MVP slice 1, #146): smaller variant of the image-card
   tile grammar — same .card surface/radius, no click-to-inspect (not a
   product), so cursor/hover lift are dropped. Source pill reuses the solid
   "count" badge pill visual (muted bg, paper text, 99px radius) rather than
   the event trio's colors — this is provenance, not a radar event. Owner
   posts render no pill (first-person voice). No new color or font. */
#updates .cards{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.upcard{cursor:default}
.upcard:hover{transform:none;border-color:var(--line)}
.upcard .thumb{height:110px}
.upcard .body{gap:var(--space-sm)}
.srcpill{align-self:flex-start;font:700 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}}
/* watch star: quiet on the thumb, ember when armed */
.star{position:absolute;top:var(--space-sm);right:var(--space-sm);width:26px;height:26px;border-radius:99px;border:1px solid var(--line);background:var(--surface);color:var(--muted);font:600 13px/1 var(--fu);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease-out}
.star:hover{color:var(--ember);border-color:var(--ember)}
.star:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.star[aria-pressed="true"]{color:var(--paper);background:var(--ember);border-color:var(--ember)}
/* cart add: sits beside the star, same chip language, on the .card thumb */
.cartadd{position:absolute;top:var(--space-sm);right:calc(26px + var(--space-sm) + 6px);width:26px;height:26px;border-radius:99px;border:1px solid var(--line);background:var(--surface);color:var(--muted);font:700 14px/1 var(--fu);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s ease-out}
.cartadd:hover{color:var(--ember);border-color:var(--ember)}
.cartadd:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
.cartadd.added{color:var(--paper);background:var(--ember);border-color:var(--ember)}
/* 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:absolute;top:var(--space-sm);right:calc(52px + var(--space-sm) + 12px);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);margin-right:var(--space-md);background:none;border:0;padding:0;cursor:pointer;display:inline-flex;align-items:center;gap:.3em}
.cartchip:hover{color:var(--ember)}
.cartchip:focus-visible{outline:2px solid var(--ember);outline-offset:2px;border-radius:2px}
.cartchip .ccount{font-variant-numeric:tabular-nums}

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

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

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

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

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

.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:var(--space-md)}
.card{position:relative;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);border-radius:6px;cursor:pointer;transition:border-color .15s ease-out,transform .15s cubic-bezier(.22,1,.36,1)}
.card:hover{border-color:var(--muted);transform:translateY(-2px);z-index:2}
.card[hidden]{display:none}
.card:focus-visible{outline:2px solid var(--ember);outline-offset:2px}
/* 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)}
.ev-drop .delta{color:var(--drop)}
.ev-restock .delta{color:var(--restock)}
.ev-new .delta{color:var(--ember)}
.norm{font:500 11.5px/1.5 var(--fu);color:var(--muted)}
.listed{font:500 11px/1.5 var(--fu);color:var(--faint)}
/* #315 stale-scrape freshness reading — reuses the .listed faint meter voice, no new color/chip */
.unverified{font:500 11px/1.5 var(--fu);color:var(--faint)}
.buy{font:600 12.5px/1 var(--fu);color:var(--ember);text-decoration:none;border-bottom:1.5px solid currentColor;padding-bottom:1px;transition:opacity .15s ease-out}
.buy:hover{opacity:.75}
/* prominent secondary CTA: ember-outline, hard 6px edge, so the inspector's
   link to the richer full coffee page reads as an obvious affordance instead
   of the buried plain-text link it replaces (founder UX feedback, 2026-07-08) */
.fullpage{display:inline-flex;align-items:center;gap:.3rem;font:700 12.5px/1 var(--fu);color:var(--ember);text-decoration:none;border:1.5px solid var(--ember);border-radius:6px;padding:.55rem .85rem;margin-bottom:var(--space-sm);transition:background .15s ease-out,color .15s ease-out;min-height:44px}
.fullpage:hover{background:var(--ember);color:var(--paper)}

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

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

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

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

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

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

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

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