/* ============================================================
   Cold Brew — styles
   Palette: espresso dark, copper (coffee) + ice (water) accents
   ============================================================ */

@font-face{
  font-family:'Hanken Grotesk';
  font-style:normal;
  font-weight:300 800;
  font-display:swap;
  src:url('fonts/hanken-grotesk.woff2') format('woff2');
}
@font-face{
  font-family:'Hanken Grotesk';
  font-style:italic;
  font-weight:300 800;
  font-display:swap;
  src:url('fonts/hanken-grotesk-italic.woff2') format('woff2');
}

/* :root carries the STRUCTURAL system (type, space, radius, motion, elevation)
   plus a full color set identical to midnight, so a missing/unknown data-theme
   lands in a real designed theme rather than a third orphan palette. The two
   theme blocks below own every color decision. */
:root{
  /* ---- color: midnight (see the theme blocks for the real definitions) ---- */
  --bg: #0b1014;
  --panel: #18242c;
  --panel-raised: #22303a;
  --line: #516470;       /* control boundary — carries the 3:1 that 1.4.11 wants */
  --line-soft: #263239;  /* decorative divider — never the only thing marking a control */
  --cream: #e2eaee;
  --cream-dim: #8a99a5;
  /* -strong / -quiet name EMPHASIS, not luminance: the prominent copper is dark
     on crema and light on midnight, so -bright/-dim inverted between themes and
     every rule built on those names was wrong in exactly one of them. */
  --copper: #d98d55;
  --copper-strong: #e8a06a;
  --copper-quiet: #8a5c38;
  --ice: #7fd4cf;
  --ice-quiet: #3d6b67;
  --warn: #e07856;
  --focus-ring: #e2eaee;
  --scrim: rgba(3,6,9,0.62);
  --shadow-color: 205 60% 3%;

  --sans: 'Hanken Grotesk', -apple-system, system-ui, sans-serif;
  /* The label tier is ~60% of the visible text in this app and was specified ten
     ways across TWO faces, at five sizes inside a 1.6px window. It is one face
     now, and that face is the humanist one — a geometric grotesque set in caps
     at 10px is exactly the "designed by a dashboard" read the product avoids.
     To hand the tier back to Space Grotesk instead, restore its @font-face and
     point --label-font at it; nothing else has to change. */
  --label-font: var(--sans);
  color-scheme: dark;

  --display-font: var(--sans);
  --data-font: var(--sans);
  --display-weight: 750;
  --cta-bg: linear-gradient(180deg, #e8a06a, #c97e46);
  --cta-fg: #0b1014;
  --cta-glow: rgba(232,160,106,0.22);
  --cta-bevel: rgba(255,255,255,0.3);
  --nav-bg: #10171d;
  --dial-tick: #93a3ad;
  --dial-tick-minor: #39464f;
  --dial-brand: rgba(237,226,208,0.55);
  --dial-brand-shadow: rgba(0,0,0,0.45);
  --ring-track: rgba(255,255,255,0.12);
  --band-lo:#e6d7b8; --band-mid:#c17a3f; --band-hi:#8c5125;
  --nav-bevel: rgba(255,255,255,0.07);

  /* ---- colors that used to be decided inside a rule. A color chosen anywhere
     but these three blocks is invisible to scripts/gen-tokens.mjs, so it has to
     be hand-copied into Swift — which is exactly how the two third-palette bugs
     (welcome.html, DialTokens.swift) happened. Naming it is the fix. ---- */
  --atmos-copper: rgba(217,141,85,0.07);   /* body::before coffee wash, top left */
  --atmos-ice:    rgba(127,212,207,0.09);  /* body::before water wash, top right */
  --chip-rest:    rgba(226,234,238,0.07);  /* resting .chip fill */
  --on-copper: #0b1014;   /* ink on a solid --copper fill: .chip.active, .tab.active */
  --on-ice:    #0b1014;   /* ink on a solid --ice fill — see crema, where the two differ */
  --badge-fg:  #17110d;   /* the .badge count coin, which sits on --ice */
  --cmp-hit:   rgba(193,122,63,0.14); /* .cmp-chip.hit — a tasting note the bag promised */
  --takeover-inner: #131a20; /* badge-screen ::backdrop, centre of the vignette */
  --takeover-outer: #05080a; /* ...and its outer edge */

  /* ---- radius: one family derived from the theme's control radius, so the
     midnight/crema character split (tight instrument vs soft bakery) actually
     reaches the popovers and sheets instead of stopping at the inputs ---- */
  --ctl-radius: 8px;
  --ctl-radius-lg: 10px;
  --r-1: calc(var(--ctl-radius) * 0.6);
  --r-2: var(--ctl-radius);
  --r-3: var(--ctl-radius-lg);
  --r-4: calc(var(--ctl-radius-lg) * 1.6);
  --r-pill: 99px;

  /* ---- space: every value on the Brew screen used to be its own number
     (25 of them, every integer 8→16 in play). Eight steps cover all of it. ---- */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 72px;
  --hint-inset: 14px; /* one inset for every hint line under a control */

  /* ---- type: eight steps, each carrying a line-height. The Brew screen alone
     rendered 29 distinct tiers, 23 of which never set one — so the app had no
     vertical rhythm, only font metrics. Nothing sits below 11px any more. ---- */
  --t-1: 0.6875rem; --lh-1: 1.35;  /* 11.0px — the label tier */
  --t-2: 0.75rem;   --lh-2: 1.5;   /* 12.0px — captions, hints */
  --t-3: 0.85rem;   --lh-3: 1.5;   /* 13.6px — chips, small controls */
  --t-4: 0.95rem;   --lh-4: 1.55;  /* 15.2px — body, inputs */
  --t-5: 1.05rem;   --lh-5: 1.45;
  --t-6: 1.3rem;    --lh-6: 1.3;
  --t-7: 1.7rem;    --lh-7: 1.2;
  --t-8: clamp(3rem, 7vw, 4.4rem); --lh-8: 1;  /* hero numerals */

  /* ---- accent washes: derived, not hand-mixed. The literals these replace
     were blended from the deleted espresso palette, so a "teal" tint and the
     theme's own --ice sat in the same pill as two different teals. ---- */
  --tint-copper: color-mix(in srgb, var(--copper) 15%, transparent);
  --tint-ice:    color-mix(in srgb, var(--ice) 13%, transparent);
  --tint-warn:   color-mix(in srgb, var(--warn) 13%, transparent);
  --edge-copper: color-mix(in srgb, var(--copper) 52%, transparent);
  --edge-ice:    color-mix(in srgb, var(--ice) 48%, transparent);

  /* ---- motion ---- */
  --dur-1: 120ms; --dur-2: 200ms; --dur-3: 320ms; --dur-4: 480ms;
  --ease-out: cubic-bezier(.2,.9,.3,1);
  --ease-spring: cubic-bezier(.3,1.3,.4,1);

  /* ---- elevation: one shadow family tinted by the theme, so crema stops
     smearing neutral grey under warm cream surfaces ---- */
  --e1: 0 1px 3px hsl(var(--shadow-color) / 0.18);
  --e2: 0 2px 8px -2px hsl(var(--shadow-color) / 0.22);
  --e3: 0 12px 30px -10px hsl(var(--shadow-color) / 0.34);
  --e4: 0 22px 54px -18px hsl(var(--shadow-color) / 0.46);

  /* safe-area indirection: real device insets normally; ?sim=iphone overrides
     them so the floating nav island can be verified off-device */
  --sa-top: env(safe-area-inset-top, 0px);
  --sa-bottom: env(safe-area-inset-bottom, 0px);
}

*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ margin:0; padding:0; }
/* contain, not none: both block pull-to-refresh/chaining, but none also kills
   the iOS rubber-band at the scroll edges — the bounce is half of what makes
   scrolling feel native */
html{ overscroll-behavior-y:contain; }

body{
  position: relative; /* containing block for the .oscover surface extensions */
  background: var(--bg);
  /* gradients live on a fixed, composited pseudo-layer (::before) so fast
     scrolling never repaints them — background-attachment:fixed did */
  color: var(--cream);
  font-family: var(--sans);
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

/* native scroll-range clamp (the definitive fix): the document carries a REAL
   100vh painted extension at each end, and the shell pulls the resting scroll
   range back to the page with negative contentInset — the rubber-band then
   stretches into fully rendered page content, so WebKit's flat overhang fill
   (the seam every overhang-painting attempt eventually exposed, because page
   tiles are only rasterized a limited margin past the document) can never
   appear. Web PWA keeps the lighter overhang approach below. */
html.native body{ padding-top:100vh; padding-bottom:100vh; }
html.native .oscover{ top:0; -webkit-mask-image:none; mask-image:none; }

/* the page surface: document-anchored (paints into WebKit's overscroll tiles
   and travels with the content), extending a full viewport ABOVE the document
   so the top rubber-band stretches into more of the same surface. */
.oscover{
  position:absolute; left:0; right:0;
  top:-100vh; bottom:0; /* top overhang only — bottom overflow would extend the scrollable area */
  background-color:var(--bg);
  /* grain baked at 16%-equivalent alpha, blended WITHIN this element
     (background-blend-mode): backdrop-independent, layerization-stable —
     mix-blend-mode across elements is what produced the seam (WebKit tiled
     vs non-tiled blend inconsistency). One element paints page surface and
     overhang, so the rubber-band boundary is the same paint. */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
  background-blend-mode:soft-light;
  opacity:1;
  /* dissolve the grain approaching the document bottom: the bottom void is
     WebKit's flat underPageBackgroundColor fill, so the page's last rows
     must be flat --bg for the bottom bounce to be seamless too */
  -webkit-mask-image:linear-gradient(to bottom, #000 calc(100% - 120px), transparent 100%);
  mask-image:linear-gradient(to bottom, #000 calc(100% - 120px), transparent 100%);
  pointer-events:none;
  z-index:-1;
}

/* atmosphere gradients — own GPU layer, never repainted on scroll */
body::before{
  content:"";
  position:fixed; inset:0;
  pointer-events:none;
  z-index:-1;
  transform:translateZ(0);
  background:
    radial-gradient(ellipse 1000px 600px at 12% -10%, var(--atmos-copper) 0%, transparent 60%),
    radial-gradient(ellipse 800px 500px at 100% -5%, var(--atmos-ice) 0%, transparent 55%);
  /* fade the atmosphere in below the very top edge: the native overscroll
     void is flat --bg, and a tinted first pixel row seams against it during
     the rubber-band — this makes page top and void the same color exactly */
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 90px);
  mask-image:linear-gradient(to bottom, transparent 0, #000 90px);
}

/* film grain lives on the .oscover surface element (document-anchored, so it
   extends into the top overhang and the bounce boundary is invisible); the old
   viewport-fixed z90 overlay rode the rubber-band and stopped dead at the
   document edge — the seam Tyler could see on a 17 Pro. */

/* ============ header ============ */

h1{
  font-size: clamp(1.5rem, 5vw, 1.9rem);
  font-weight:600;
  margin:0;
  letter-spacing:-0.01em;
}
h1 span{ color:var(--ice); }

/* Screen-reader-only: in the a11y tree, out of the visual one. Carries the
   recipe live region — the hero numbers count up by patching text nodes
   directly, so there is nothing for a reader to notice without this. */
.sr-only{
  position:absolute;
  width:1px; height:1px;
  margin:-1px; padding:0;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
  border:0;
}

/* ============ fields ============ */

/* THE label tier. Every uppercase micro-label in the app resolves here — this
   replaces ten separate recipes that rendered at 10.56 / 10.24 / 9.92 / 9.60 /
   8.96px with five different letter-spacings, across two typefaces. */
.label{
  font-family:var(--label-font);
  font-size:var(--t-1);
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.12em;
  text-transform:uppercase;
  color:var(--cream-dim);
  display:block;
}
.label--sm{ font-size:0.625rem; letter-spacing:0.1em; } /* the ONE step down */
.label--gap{ margin-bottom:var(--s3); }
.label--gap-sm{ margin-bottom:var(--s1); }

/* the sheet-side alias, kept so existing markup keeps working */
.field-label{
  font-family:var(--label-font);
  font-size:var(--t-1);
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.12em;
  text-transform:uppercase;
  color:var(--cream-dim);
  margin-bottom:9px;
  display:block;
}

/* ONE input skin, and the .ctl class agrees with it.
   This selector is specificity (0,1,1); `.ctl` is (0,1,0) — so the v4 control
   tokens on .ctl lost to this rule and every text input in the app silently
   rendered at 7px radius / --panel-raised / 41px tall. `select.ctl` is also
   (0,1,1) and declared later, which is exactly why the selects looked right and
   the inputs didn't. That one mismatch is what put three radii and two fills in
   the Brew column, buried the combobox ellipsis under its own chevron, and made
   the auth screens read as a different product. */
input[type=number], input[type=text], input[type=email], input[type=password],
input[type=tel], select, textarea{
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r-2);
  color:var(--cream);
  font-family:var(--sans);
  font-size:0.95rem;
  padding:12px 14px;
  min-height:44px; /* HIG floor — the old skin came in at 41px */
  outline:none;
  transition:border-color var(--dur-1) ease;
}
input:focus-visible, select:focus-visible, textarea:focus-visible{
  border-color:var(--copper-strong);
  outline:2px solid var(--focus-ring);
  outline-offset:2px;
}
input[type=number]{ width:92px; }
input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button{ -webkit-appearance:none; margin:0; }
input[type=text], input[type=email], input[type=password], textarea{ width:100%; }
textarea{ resize:vertical; line-height:1.5; min-height:0; padding:12px 14px; }
/* native selects wear the same chevron, same inset, as the combobox */
/* compact unit selects (oz/lb, mL/fl oz): tighter chevron, real clearance */
select.ctl.ctl-unit{
  background-position:right 9px center;
  background-size:11px 11px;
  padding-right:30px !important;
}
select.ctl{
  -webkit-appearance:none;
  appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239b8d7d' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 17px center;
  background-size:14px 14px;
  padding-right:44px;
}

/* strength heatmap: coffee color IS the scale — pale tea (left, 1:16) deepens
   to syrup (right, 1:3), same direction as the chip ladder above. Notches
   under the track mark where each preset lands; the dot is the live brew. */
.sband{
  position:relative;
  overflow:visible;
  height:44px; /* ≥44px hit area (HIG); the visual track/dot sit inset within it */
  margin-top:14px;
  cursor:grab;
  touch-action:none;
  outline:none;
}
.sband:active{ cursor:grabbing; }
/* while dragging, drop the spring transition so the dot tracks the finger 1:1 */
.sband.dragging .sband-dot, .sband.dragging .sband-val{ transition:none; }
/* two-tone: a page-colored spacer, then the ring — so the dot reads as focused
   over the track's own gradient rather than dissolving into it */
.sband:focus-visible .sband-dot{ box-shadow:0 0 0 2px var(--bg), 0 0 0 4px var(--focus-ring), var(--e1); }
.sband-val{
  position:absolute; top:0;
  transform:translateX(-50%);
  /* the readout follows the dot all the way to either end; these keep the
     glyphs on screen without lying about where the value sits */
  min-width:max-content;
  font-family:var(--data-font);
  font-size:0.64rem;
  font-weight:600;
  letter-spacing:0.04em;
  color:var(--cream-dim);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
  transition:left .45s cubic-bezier(.3,1.2,.4,1);
}
.sband-track{
  position:absolute; top:19px; left:0; right:0;
  height:6px; border-radius:99px;
  background:linear-gradient(90deg, var(--band-lo), var(--band-mid) 55%, var(--band-hi));
}
.sband-pip{
  position:absolute; top:25px;
  width:2px; height:5px;
  border-radius:2px;
  background:var(--cream-dim);
  opacity:0.55;
  transform:translateX(-50%);
}
.sband-ref{
  position:absolute; top:24px;
  width:6px; height:6px;
  background:var(--ice);
  transform:translateX(-50%) rotate(45deg);
  border-radius:1.5px;
  opacity:0.9;
}
.sband-dot{
  position:absolute; top:15px;
  width:14px; height:14px;
  border-radius:99px;
  background:var(--copper-strong);
  border:2.5px solid var(--bg);
  box-shadow:var(--e1);
  transform:translateX(-50%);
  transition:left .45s cubic-bezier(.3,1.2,.4,1);
}
/* batch-size variant: water-themed track, coffee-colored bag detents */
.vband .sband-track{
  background:linear-gradient(90deg, color-mix(in srgb, var(--ice) 26%, var(--bg)), var(--ice) 72%, var(--ice-quiet) 100%);
}
.vband .sband-dot{ background:var(--ice); }
.vband-pip{
  position:absolute; top:24px;
  width:6px; height:6px;
  background:var(--copper-strong);
  transform:translateX(-50%) rotate(45deg);
  border-radius:1.5px;
  opacity:0.9;
}
/* sentence case, not caps: this row carries UNITS as often as words, and
   uppercasing turned "250 mL" into "250 ML". */
.sband-ends{
  display:flex;
  justify-content:space-between;
  margin-top:var(--s1);
  font-family:var(--label-font);
  font-size:var(--t-2);
  line-height:var(--lh-2);
  font-weight:500;
  letter-spacing:0.04em;
  color:var(--cream-dim);
}

/* The ring lives OUTSIDE the control (positive offset) so it only ever needs to
   hold against the page, not against an accent fill. At -2px it was painted
   INSIDE the fill and measured 1.23–1.30:1 on every selected chip, every active
   tab and the CTA — keyboard focus was invisible exactly where it mattered most.
   A single ring color cannot clear 3:1 on both copper and ice fills; moving it
   off the control makes that constraint go away entirely. */
button:focus-visible,
[role="button"]:focus-visible,
summary:focus-visible{
  outline:2px solid var(--focus-ring);
  outline-offset:2px;
}

/* ============ hero ============ */

.dial-svg{ width:min(232px, 62vw); height:auto; margin:0 auto; display:block; }
#needle{
  transform-origin:120px 116px;
  transition: transform .55s cubic-bezier(.3,1.4,.4,1);
}
.dial-brand{
  font-family:var(--label-font);
  font-size:8px;
  font-weight:600;
  letter-spacing:0.3em;
  fill:var(--dial-brand);
  paint-order:stroke;
  stroke:var(--dial-brand-shadow);
  stroke-width:0.5px;
}

/* ============ CTA ============ */
.cta{
  width:100%;
  margin-top:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:9px;
  background:var(--cta-bg);
  color:var(--cta-fg);
  border:none;
  border-radius:var(--r-3);
  padding:15px 20px;
  font-family:var(--sans);
  font-size:1rem;
  font-weight:600;
  letter-spacing:0.01em;
  cursor:pointer;
  box-shadow: 0 6px 20px var(--cta-glow), inset 0 1px 0 var(--cta-bevel);
  transition: transform .1s ease, box-shadow .15s ease, filter .15s ease;
  touch-action:manipulation;
}
.cta svg{ width:19px; height:19px; }
.cta:hover{ filter:brightness(1.05); }
.cta:active{ transform:scale(0.985); box-shadow:0 3px 10px var(--cta-glow); }

.method{
  margin-top:18px;
  border-top:1px solid var(--line-soft);
  padding-top:14px;
}
.method summary::-webkit-details-marker{ display:none; }
.method summary{ list-style:none; }
.method summary::before{
  content:"";
  display:inline-block;
  width:0; height:0;
  margin-right:8px;
  width:7px; height:7px;
  border-right:1.8px solid currentColor;
  border-bottom:1.8px solid currentColor;
  transform:translateY(-2px) rotate(-45deg);
  transition:transform var(--dur-1) ease;
}
.method[open] summary::before{ transform:translateY(-4px) rotate(45deg); }
.method summary{
  font-family:var(--label-font);
  font-size:var(--t-1);
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.12em;
  text-transform:uppercase;
  color:var(--cream-dim);
  cursor:pointer;
  list-style-position:outside;
}
.method p{
  font-size:0.78rem;
  line-height:1.7;
  color:var(--cream-dim);
  margin:10px 0 0;
}
/* ============ badge ============ */

.badge{
  min-width:17px;
  height:17px;
  padding:0 4px;
  border-radius:99px;
  background:var(--ice);
  color:var(--badge-fg);
  font-family:var(--label-font);
  font-size:0.62rem;
  font-weight:700;
  font-variant-numeric:tabular-nums;
  display:flex;
  align-items:center;
  justify-content:center;
}

/* ============ journal brew-card chevron ============ */
/* <details> with list-style:none gives no expand hint — the chevron is it */
.brew-chev{
  width:14px; height:14px; color:var(--cream-dim);
  align-self:center;
  transition:transform 0.22s ease;
}
details[open] .brew-chev{ transform:rotate(90deg); }

/* ============ tune hint (dial-it-in receipt) ============ */

.tune-hint{
  font-family:var(--data-font);
  font-size:0.68rem;
  letter-spacing:0.05em;
  color:var(--ice);
  line-height:1.6;
  margin-top:8px;
}
.tune-reset{
  background:none; border:none; padding:0; cursor:pointer;
  font:inherit; letter-spacing:inherit;
  color:var(--cream-dim); text-decoration:underline; text-underline-offset:2px;
}
.tune-reset:hover{ color:var(--cream); }

/* sheets park focus on themselves when they open (no ring on rating bean #1) —
   the sheet itself never needs a visible ring */
dialog.sheet:focus{ outline:none; }

/* ============ status-bar veil ============ */
/* Progressive-blur scrim under the clock/Dynamic Island: scrolling content
   melts away instead of colliding with the status bar. Height collapses to a
   sliver when there's no top inset (desktop web). */
/* the native shell draws this scrim itself (AppDelegate CAGradientLayer) so
   it stays screen-glued during the bounce instead of riding the page */
html.native .top-veil{ display:none; }
.top-veil{
  position:fixed;
  top:0; left:0; right:0;
  height:calc(var(--sa-top) + 12px);
  z-index:45;
  pointer-events:none;
  background:linear-gradient(to bottom, var(--bg) 25%, transparent 90%);
  -webkit-backdrop-filter:blur(14px);
  backdrop-filter:blur(14px);
  -webkit-mask-image:linear-gradient(to bottom, black 50%, transparent);
  mask-image:linear-gradient(to bottom, black 50%, transparent);
}

/* ============ mini readout ============ */
/* Docked just above the floating nav island: a top-fixed bar fights the iOS
   status bar / Dynamic Island and Safari's collapsing chrome. Down here it
   joins the nav cluster, stays in the thumb zone, and never gets cut off. */
.mini-readout{
  position:fixed;
  left:50%;
  bottom:calc(88px + var(--sa-bottom)); /* clear the 44px-tall nav pill */
  z-index:30;
  display:flex;
  align-items:center;
  gap:9px;
  padding:9px 18px;
  border-radius:99px;
  background:var(--nav-bg);
  -webkit-backdrop-filter:blur(18px);
  backdrop-filter:blur(18px);
  border:1px solid var(--line);
  box-shadow:var(--e3);
  font-family:var(--display-font);
  font-size:0.84rem;
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
  max-width:calc(100vw - 32px);
  overflow:hidden;
  opacity:0;
  pointer-events:none;
  transform:translate(-50%, 14px);
  transition:transform .28s ease, opacity .28s ease;
}
.mini-readout.show{ transform:translate(-50%, 0); opacity:1; }
.mr-item{
  display:inline-flex;
  align-items:baseline;
  font-weight:var(--display-weight, 600);
  letter-spacing:-0.01em;
}
.mr-item.copper{ color:var(--copper-strong); }
.mr-item.ice{ color:var(--ice); }
.mr-unit{
  font-size:0.68em;
  font-weight:500;
  color:var(--cream-dim);
  margin-left:2px;
  letter-spacing:0;
}
.mr-tag{
  font-family:var(--label-font);
  font-size:var(--t-1);
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.12em;
  text-transform:uppercase;
  color:var(--cream-dim);
  margin-right:5px;
  align-self:center;
  max-width:9ch;
  overflow:hidden;
  text-overflow:ellipsis;
}
.mr-sep{ color:var(--line); }
/* content fades out under the floating chrome rather than being sliced by it */
.nav-fade{
  position:fixed; left:0; right:0; bottom:0;
  height:calc(168px + var(--sa-bottom));
  background:linear-gradient(to top, var(--bg) 58%, transparent);
  pointer-events:none;
  z-index:29; /* UNDER the mini-readout (30) and the island (40), not over them */
}
@media (min-width:900px){ .mini-readout{ display:none; } .nav-fade{ display:none; } }

/* ============ desktop chrome (>=900px) ============ */
/* The dial column is sticky and the page barely scrolls it — so on desktop
   the brand + gear pin to a fixed glass top bar and the Brew/Steep/Journal
   pill docks into it, instead of floating over content at the bottom. */
@media (min-width:900px){
  .app-header{
    position:fixed;
    top:0; left:0; right:0;
    z-index:46;
    margin:0;
    padding:14px max(26px, calc((100vw - 1080px) / 2 + 26px));
    background:var(--bg);
    box-shadow:var(--e1);
    -webkit-backdrop-filter:blur(18px);
    backdrop-filter:blur(18px);
    border-bottom:1px solid var(--line-soft);
  }
  .tabbar-nav{
    top:10px;
    bottom:auto !important;
    z-index:47 !important;
    /* docked into the glass header — drop its own glass so it isn't glass-on-glass;
       the active-tab chip alone carries state */
    background:transparent !important;
    -webkit-backdrop-filter:none !important;
    backdrop-filter:none !important;
    border-color:transparent !important;
    box-shadow:none !important;
  }
  /* the docked Steep tab already carries the count badge — the header chip is
     redundant (and was overlapping the pill). Needs the .app-header qualifier:
     the base .steep-chip{display:flex} sits LATER in this file and media
     queries add no specificity, so a bare .steep-chip{display:none} loses */
  .app-header .steep-chip{ display:none; }
  .page{
    padding-top:104px !important;
  }
}
/* ============ sheet-open transition ============ */

@keyframes viewIn{
  from{ opacity:0; transform:translateY(5px); }
  to{ opacity:1; transform:none; }
}
/* ============ log ============ */

/* (removed: no element has ever carried id="activeSection") */

.tag{
  font-family:var(--label-font);
  font-size:0.625rem;
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.1em;
  text-transform:uppercase;
  padding:4px 9px;
  border-radius:99px;
  white-space:nowrap;
  flex-shrink:0;
}
.tag.steeping{ background:var(--tint-ice); color:var(--ice); }
.tag.window{ background:var(--tint-copper); color:var(--copper-strong); }
.tag.over{ background:var(--tint-warn); color:var(--warn); }

/* --ring-track, not a hardcoded black: this is the only black-derived fill left
   over from the dark-only original, and on a warm cream page it read as a
   foreign component. The token is already correct in both themes. */
.progress{
  margin-top:12px;
  height:5px;
  border-radius:var(--r-pill);
  background:var(--ring-track);
  border:1px solid var(--line-soft);
  overflow:hidden;
}
.progress-fill{
  height:100%;
  border-radius:99px;
  background:linear-gradient(90deg, var(--ice-quiet), var(--ice));
  transition:width .4s ease;
}
.progress-fill.full{ background:linear-gradient(90deg, var(--copper-quiet), var(--copper-strong)); }

.btn{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-height:44px; /* ≥44px touch target (HIG) */
  border:none;
  border-radius:var(--r-2);
  font-family:var(--sans);
  font-size:0.86rem;
  font-weight:600;
  padding:10px 16px;
  cursor:pointer;
  transition: filter .15s ease, transform .1s ease, background .15s ease, color .15s ease;
  touch-action:manipulation;
}
.btn:active{ transform:scale(0.97); }
.btn.primary{ background:var(--cta-bg); color:var(--cta-fg); }
.btn.primary:hover{ filter:brightness(1.08); }
.btn.ghost{
  background:transparent;
  border:1px solid var(--line);
  color:var(--cream-dim);
}
/* Destructive intent has to be visible AT REST. Living only in :hover meant it
   never existed on the device this ships to — "Delete account" and "Keep my
   account" were pixel-identical ghost pills under a thumb. */
.btn.ghost.danger{
  color:var(--warn);
  border-color:color-mix(in srgb, var(--warn) 34%, transparent);
}

/* Hover is a pointer affordance. Ungated, WebKit leaves the :hover fill stuck
   after a tap — which is how a DISABLED stepper button ended up looking louder
   than its enabled neighbour. */
@media (hover: hover){
  .btn.ghost:hover{ color:var(--cream); background:var(--panel-raised); }
  .btn.ghost.danger:hover{
    color:var(--warn);
    border-color:var(--warn);
    background:color-mix(in srgb, var(--warn) 10%, transparent);
  }
}

/* There were ZERO :disabled rules in this file, so a disabled control rendered
   identically to a live one — Save, Sign in mid-request, Scan while busy, and
   Delete before you'd typed DELETE. */
.btn:disabled, .chip:disabled, .cta:disabled, button:disabled{
  opacity:0.42;
  cursor:default;
  filter:saturate(0.6);
}
.btn:disabled:active, .chip:disabled:active, .cta:disabled:active, button:disabled:active{
  transform:none;
}
.btn.ghost.armed{ color:var(--warn); border-color:var(--warn); background:var(--tint-warn); }
.btn.small{ font-size:0.8rem; padding:8px 13px; }

.beans{ display:flex; gap:2px; flex-shrink:0; }
.bean{ width:15px; height:15px; }
.bean ellipse{ fill:var(--cream-dim); fill-opacity:0.38; }
.bean.filled ellipse{ fill:var(--copper-strong); fill-opacity:1; }
.bean path{ stroke:var(--bg); stroke-width:1.8; fill:none; }
/* the glyph sits on a panel inside sheets, on the page in the journal */
.sheet-body .bean path, dialog .bean path{ stroke:var(--panel); }

/* rating input */
.rating{ display:flex; gap:6px; margin:4px 0 18px; }
.rating button{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:44px;
  min-height:44px; /* ≥44px touch target (HIG) */
  background:transparent;
  border:none;
  padding:4px;
  cursor:pointer;
  touch-action:manipulation;
}
.rating .bean{ width:27px; height:27px; }
.rating button .bean ellipse{ transition: fill .12s ease; }

/* empty state */
.empty{
  text-align:center;
  padding:56px 20px 40px;
  color:var(--cream-dim);
}
.empty svg{ width:72px; height:72px; opacity:0.45; margin-bottom:16px; }
.empty p{ margin:4px 0; font-size:0.95rem; }
.empty .empty-sub{ font-size:0.8rem; line-height:1.6; max-width:290px; margin:6px auto 0; }
.empty em{ color:var(--copper-strong); font-style:normal; }

/* ============ dialogs / sheets ============ */
dialog.sheet{
  border:1px solid var(--line);
  background:var(--panel);
  color:var(--cream);
  border-radius:var(--r-4) var(--r-4) 0 0;
  padding:14px 22px calc(22px + var(--sa-bottom));
  width:100%;
  max-width:560px;
  /* a tall sheet (setups + sync + community + about) must scroll, not overflow
     the screen with its lower sections unreachable on a phone */
  max-height:90vh;
  max-height:90dvh;
  overflow-y:auto;
  overscroll-behavior:none; /* no chain-scroll, no bounce — the swipe-dismiss gesture owns overscroll */
  margin: auto auto 0;
  /* native keyboard (Keyboard.resize "none" overlays the page): --kb is set
     from keyboardWillShow so the sheet rides above it instead of typing blind */
  margin-bottom: var(--kb, 0px);
  max-height: min(90dvh, calc(100dvh - var(--kb, 0px) - 24px));
  transition: margin-bottom 0.25s ease;
  box-shadow: var(--e4);
}
/* themed scrim: the shared near-black took crema's page from L=0.844 to 0.158 —
   a warm cream surface turned muddy olive the instant any sheet opened. The
   badge takeover already had a crema override; the sheet (which opens far more
   often) never got one. */
dialog.sheet::backdrop{
  background:var(--scrim);
  -webkit-backdrop-filter:blur(3px);
  backdrop-filter:blur(3px);
}
/* The panel must be OPAQUE the instant it exists — animating its opacity meant
   you saw the dial, the numbers and the nav island THROUGH the sheet while the
   backdrop was already fully dark. And 40px of travel on a 594px panel is a
   card fade-in, not a sheet. The fade belongs to ::backdrop alone. */
dialog.sheet[open]{ animation: sheetUp var(--dur-3) var(--ease-out); }
dialog.sheet[open]::backdrop{ animation: scrimIn var(--dur-3) ease; }
@keyframes sheetUp{ from{ transform:translateY(100%); } }
@keyframes scrimIn{ from{ opacity:0; } }
/* there was no exit animation anywhere in this file: closing a sheet — or
   committing a swipe-dismiss — made it blink out from wherever the finger left it */
dialog.sheet.closing{ animation: sheetDown var(--dur-2) var(--ease-out) forwards; }
dialog.sheet.closing::backdrop{ animation: scrimOut var(--dur-2) ease forwards; }
@keyframes sheetDown{ to{ transform:translateY(100%); } }
@keyframes scrimOut{ to{ opacity:0; } }
@media (min-width:700px){
  dialog.sheet{
    border-radius:var(--r-4);
    margin:auto;
    margin-bottom:auto; /* wide viewports center the sheet; no keyboard lift */
    padding-bottom:22px;
  }
  dialog.sheet[open]{ animation: viewIn var(--dur-2) var(--ease-out); }
  dialog.sheet.closing{ animation: viewOut var(--dur-1) ease forwards; }
  @keyframes viewOut{ to{ opacity:0; transform:scale(.97); } }
  .sheet-handle{ display:none; }
}
.sheet-handle{
  width:40px;
  height:4px;
  border-radius:99px;
  background:var(--line);
  margin:0 auto 16px;
  /* a drag starting on the grabber belongs to the swipe-dismiss gesture, not
     the scroller (WebKit otherwise claims it and fires touchcancel) — and a
     4px bar is no touch target, so grow the hit area without moving pixels */
  touch-action:none;
  box-sizing:content-box;
  padding:14px 60px;
  margin:-14px auto 2px;
  background-clip:content-box;
}
.sheet-title{
  margin:4px 0 8px;
  font-size:1.15rem;
  font-weight:600;
  letter-spacing:-0.01em;
}
/* This is the body copy of every sheet — onboarding pitch, "Signed in as…",
   the Community paragraph, the delete warning. It was set in the LABEL face at
   11.84px, i.e. a caption-size data face carrying multi-sentence prose. */
.sheet-summary{
  font-family:var(--sans);
  font-size:0.86rem;
  color:var(--cream-dim);
  line-height:1.6;
  overflow-wrap:anywhere;
  margin:0 0 18px;
}
.sheet-summary strong{ color:var(--copper-strong); font-weight:600; }
.sheet-summary .w{ color:var(--ice); font-weight:600; }
/* Pinned. The Finish sheet put 684px of content in a 592px box, so Save landed
   at y685 in a 660px viewport: not visible, not reachable, inside a dialog that
   marks itself as scrollable nowhere. Settings was worse — 964px of content and
   the only Close at the bottom of it. */
.sheet-actions{
  display:flex;
  justify-content:flex-end;
  gap:10px;
  margin-top:20px;
}
/* Only the sheet's own final row is the pinned footer. Applying this to every
   .sheet-actions gave Settings a second full-width footer, divider and all,
   floating mid-sheet above the real one. */
dialog.sheet > .sheet-actions:last-child{
  position:sticky;
  bottom:calc(-1 * (22px + var(--sa-bottom)));
  z-index:2;
  margin:20px -22px calc(-1 * var(--sa-bottom));
  /* solid, not a 60% wash: content used to ghost through the top of the bar and
     get sliced by its own border */
  padding:14px 22px calc(14px + var(--sa-bottom));
  background:var(--panel);
  box-shadow:0 -10px 14px -12px hsl(var(--shadow-color) / 0.5);
  border-top:1px solid var(--line-soft);
}
/* one action shouldn't huddle at the far right of an 800px row */
.sheet-actions:has(.btn:only-child){ justify-content:stretch; }
.sheet-actions .btn:only-child{ width:100%; }

/* NB: no separate desktop close button. The sticky actions row above keeps the
   sheet's own Close permanently on screen at any scroll position and any width,
   which is what CHR-06 actually needed — a second control would just re-create
   the duplicate-Close problem the a11y batch removed. */

/* named setups (Settings sheet) */
/* stacked title/subtitle: at phone width a side-by-side split clips both
   the name and the summary at once */
.setup-row{
  flex:1;
  min-width:0;
  min-height:44px;
  display:flex;
  flex-direction:column;
  align-items:stretch;
  gap:3px;
  padding:10px 14px;
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r-2);
  color:var(--cream);
  cursor:pointer;
  text-align:left;
  transition:border-color .18s ease, background .18s ease;
  touch-action:manipulation;
}
@media (hover: hover){ .setup-row:hover{ border-color:var(--copper-strong); background:var(--panel-raised); } }
.setup-name{
  font-family:var(--sans);
  font-size:0.92rem;
  font-weight:600;
  letter-spacing:-0.01em;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.setup-sum{
  font-family:var(--label-font);
  font-size:0.64rem;
  color:var(--cream-dim);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

/* ============ misc ============ */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:0.01ms !important;
    transition-duration:0.01ms !important;
    /* Durations alone were not enough. These animations use fill-mode:both, so
       an un-zeroed delay held the element at opacity:0 and then popped it —
       worst case the badge takeover's dismiss button, invisible for 450ms for
       exactly the users who asked for less motion. */
    animation-delay:0s !important;
    transition-delay:0s !important;
  }
}

/* ============================================================
   v4 additions — themes, chips, tabs
   Ported from Cold Brew v4.dc.html (applyTheme/chipStyle/tabStyle)
   ============================================================ */

@keyframes cardIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes ringPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
/* arrival should glow UP; the old pulse faded the ring toward transparent,
   which reads as the window closing rather than opening */
@keyframes ringReady { 0%, 100% { opacity: 1; filter: none; } 50% { opacity: 0.9; filter: brightness(1.14); } }
@keyframes chipBlink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ============ themes ============ */
/* Crema is the DEFAULT theme (index.html ships data-theme='crema' and resolves
   'auto' to it on light devices), so its accents carry AA on their own ground:
   copper-strong 4.57:1 as text, copper 4.54:1 under --cta-fg, warn 4.54:1,
   and both CTA gradient stops ≥4.5:1. The pre-AA values were 4.48 / 3.96 /
   4.37 / 3.20 — the primary button was the worst offender. */
html[data-theme='crema']{
  --bg: #f4ecdd; --panel: #fdf8ee; --panel-raised: #ffffff;
  --line: #94826c; --line-soft: #d8cbb4;
  --cream: #32261a; --cream-dim: #6e5d4b;
  --copper: #a2622b; --copper-strong: #9b5b24; --copper-quiet: #c99e75;
  --ice: #22766f; --ice-quiet: #7fb5b0; --warn: #ac502e;
  --focus-ring: #32261a;
  --scrim: rgba(88,64,40,0.34); /* warm veil — the old near-black crushed cream to mud */
  --shadow-color: 28 40% 18%;
  --nav-bg: #fbf6ea;
  --cta-bg: linear-gradient(180deg, #9e6434, #8a5423); --cta-fg: #fff6e8; --cta-glow: rgba(158,100,52,0.26);
  --ctl-radius: 12px; --ctl-radius-lg: 14px;
  --dial-tick: #6b5a46; --dial-tick-minor: #c4b49c;
  --ring-track: rgba(52,38,26,0.14);
  --band-lo:#968565; --band-mid:#a2622b; --band-hi:#40230e;
  --nav-bevel: rgba(255,255,255,0.85);
  --cta-bevel: rgba(255,255,255,0.3);
  --dial-brand: rgba(237,226,208,0.55); --dial-brand-shadow: rgba(0,0,0,0.45);
  --atmos-copper: rgba(193,122,63,0.16); --atmos-ice: rgba(34,118,111,0.1);
  --chip-rest: rgba(255,255,255,0.45);
  /* the two on-accent inks are NOT one value: warm cream reads as part of the
     coffee, cool near-white as part of the water. copper = coffee, ice =
     water/time — the same rule that keeps the accents themselves apart. */
  --on-copper: #fff6e8; --on-ice: #f4f9f8;
  --badge-fg: #17110d;
  --cmp-hit: rgba(193,122,63,0.14);
  --takeover-inner: #fdf8ee; --takeover-outer: #ecdfc8;
  color-scheme: light;
}
html[data-theme='crema'] body{ background:var(--bg); }
/* Midnight gets its character from surface and depth, NOT from reassigning the
   semantic accents. design-system/README.md: "copper = coffee, ice = water/time.
   Never swap them." The CTA and the active tab used to go teal here, which put
   two different brand accents on the same job in one screen. */
html[data-theme='midnight']{
  --bg: #0b1014; --panel: #18242c; --panel-raised: #22303a;
  --line: #516470; --line-soft: #263239;
  --cream: #e2eaee; --cream-dim: #8a99a5;
  --copper: #d98d55; --copper-strong: #e8a06a; --copper-quiet: #8a5c38;
  --ice: #7fd4cf; --ice-quiet: #3d6b67; --warn: #e07856;
  --focus-ring: #e2eaee;
  --scrim: rgba(3,6,9,0.62);
  --shadow-color: 205 60% 3%;
  --nav-bg: #10171d;
  --cta-bg: linear-gradient(180deg, #e8a06a, #c97e46); --cta-fg: #0b1014; --cta-glow: rgba(232,160,106,0.22);
  --ctl-radius: 8px; --ctl-radius-lg: 10px;
  --dial-tick: #93a3ad; --dial-tick-minor: #39464f;
  --ring-track: rgba(255,255,255,0.12);
  --band-lo:#e6d7b8; --band-mid:#c17a3f; --band-hi:#8c5125;
  --nav-bevel: rgba(255,255,255,0.07);
  --cta-bevel: rgba(255,255,255,0.3);
  --dial-brand: rgba(237,226,208,0.55); --dial-brand-shadow: rgba(0,0,0,0.45);
  --atmos-copper: rgba(217,141,85,0.07); --atmos-ice: rgba(127,212,207,0.09);
  --chip-rest: rgba(226,234,238,0.07);
  /* both inks collapse to the page ground here — midnight's accents are light
     enough that one dark ink serves both. They stay two tokens because in
     crema they are not the same value. */
  --on-copper: #0b1014; --on-ice: #0b1014;
  --badge-fg: #17110d;
  --cmp-hit: rgba(193,122,63,0.14);
  --takeover-inner: #131a20; --takeover-outer: #05080a;
  color-scheme: dark;
}
html[data-theme='midnight'] body{ background:var(--bg); }

/* ============ chips (segmented choices) ============ */
.chip{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-height:44px; /* ≥44px touch target (HIG) — most-tapped controls in the app */
  padding:10px 18px;
  border-radius:var(--ctl-radius);
  border:1px solid var(--line);
  background:transparent;
  color:var(--cream-dim);
  font-family:var(--sans);
  font-size:0.85rem;
  cursor:pointer;
  transition:background 0.25s ease, border-color 0.25s ease, color 0.25s ease;
  touch-action:manipulation;
}
/* A border-color nudge alone was invisible next to a resting sibling — you
   could not tell which chip you were on. Fill carries the hover now. */
@media (hover: hover){
  .chip:hover{ border-color:var(--cream-dim); background:var(--panel-raised); }
  .chip.tone-ice:hover{ border-color:var(--ice); }
  .chip.active:hover{ filter:brightness(1.07); }
}
.chip.active{
  background:var(--tint-copper);
  border-color:var(--edge-copper);
  color:var(--copper-strong);
}
.chip.active.tone-ice{
  background:var(--tint-ice);
  border-color:var(--edge-ice);
  color:var(--ice);
}
/* both themes get the same three rules, but they stay theme-scoped:
   `html[data-theme=…] .chip` is (0,2,1) and outranks the (0,2,0) `.chip.active`
   above it, so the active fills have to be restated at the same weight. */
html[data-theme='crema'] .chip{ background:var(--chip-rest); }
html[data-theme='crema'] .chip.active{
  background:var(--copper); border-color:transparent; color:var(--on-copper); font-weight:600;
}
html[data-theme='crema'] .chip.active.tone-ice{ background:var(--ice); color:var(--on-ice); }
html[data-theme='midnight'] .chip{ background:var(--chip-rest); }
html[data-theme='midnight'] .chip.active{
  background:var(--copper); border-color:transparent; color:var(--on-copper); font-weight:600;
}
html[data-theme='midnight'] .chip.active.tone-ice{ background:var(--ice); color:var(--on-ice); }
/* vessel rows are chips stretched full-width */

/* ============ v4 nav tabs ============ */
.tab{
  display:flex; align-items:center; gap:7px;
  padding:13px 20px; min-height:44px; border-radius:99px; border:none; /* ≥44px touch target (HIG) */
  background:transparent; color:var(--cream-dim);
  font-family:var(--sans); font-size:0.85rem; font-weight:600;
  cursor:pointer; transition:background 0.25s ease, color 0.25s ease;
  touch-action:manipulation;
}
.tab.active{ background:var(--tint-copper); color:var(--copper-strong); }
html[data-theme='crema'] .tab.active{ background:var(--copper); color:var(--on-copper); }
html[data-theme='midnight'] .tab.active{ background:var(--copper); color:var(--on-copper); }

/* ============ header controls ============ */
.steep-chip{
  display:flex; align-items:center; gap:7px;
  background:var(--tint-ice);
  border:1px solid var(--edge-ice);
  border-radius:var(--ctl-radius);
  padding:7px 15px; cursor:pointer;
  font-family:var(--label-font); font-size:var(--t-1); line-height:var(--lh-1);
  font-weight:600; letter-spacing:0.12em; color:var(--ice); text-transform:uppercase;
  white-space:nowrap;
  transition:background 0.25s ease;
}
@media (hover: hover){ .steep-chip:hover{ background:color-mix(in srgb, var(--ice) 20%, transparent); } }
/* ready is a different message from steeping — give it the coffee accent, not
   the water one, so a glance at the header tells you which state you're in */
.steep-chip.ready{ color:var(--copper-strong); background:var(--tint-copper); border-color:var(--edge-copper); }
.icon-btn{
  display:flex; align-items:center; justify-content:center;
  width:44px; height:44px; flex-shrink:0; /* ≥44px touch target (HIG) */
  border-radius:99px; border:1px solid var(--line);
  background:var(--panel); color:var(--cream-dim);
  cursor:pointer; transition:color 0.25s ease, border-color 0.25s ease;
}
.icon-btn:hover{ color:var(--cream); border-color:var(--cream-dim); }

/* iOS Safari zooms the whole viewport when a focused field's font-size is
   under 16px — force 16px on touch devices (beats inline font-size styles) */
@media (pointer: coarse){
  input.ctl, select.ctl, textarea.ctl, .sheet input, .sheet textarea{ font-size:16px !important; }
}

/* v4 rounded form controls.
   Type selectors are carried here deliberately: at a bare `.ctl` this block is
   specificity (0,1,0) and loses every property to the base input rule above.
   Matching (0,1,1) and declaring later is what makes the class actually apply. */
input.ctl, select.ctl, textarea.ctl, .ctl{
  width:100%;
  font-family:var(--sans);
  font-size:0.95rem;
  padding:12px 14px;
  min-height:44px;
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--ctl-radius);
  color:var(--cream);
}

/* ============ combobox (typeahead pickers) ============ */
.combo{ position:relative; }
/* type selector for the same specificity reason as .ctl — at `.combo-input` the
   reserved chevron gutter lost to the base rule's 14px and the value's ellipsis
   rendered on top of the chevron */
input.combo-input{ padding-right:46px; text-overflow:ellipsis; }
.combo-chev{
  position:absolute; right:17px; top:50%;
  width:14px; height:14px;
  transform:translateY(-50%);
  color:var(--cream-dim);
  pointer-events:none;
  transition:transform 0.2s ease;
}
.combo.open .combo-chev{ transform:translateY(-50%) rotate(180deg); }
.combo-pop{
  position:absolute; top:calc(100% + 8px); left:0; right:0; z-index:70;
  background:var(--panel-raised);
  border:1px solid var(--line);
  border-radius:var(--r-4);
  box-shadow:var(--e4);
  /* third bound: never extend under the native keyboard (--kb overlays the
     page, Keyboard.resize "none") — assumes the input was pinned near the
     top by the keyboardWillShow scroll */
  max-height:min(320px, 44vh, calc(100dvh - var(--kb, 0px) - 156px)); overflow-y:auto;
  padding:12px 8px 8px; /* extra top clearance so the first group header clears the rounded corner */
  -webkit-overflow-scrolling:touch;
}
.combo-input{ scroll-margin-top:84px; } /* keyboard scroll pins it below the status veil */
.combo-group{
  font-family:var(--label-font);
  font-size:0.625rem; line-height:var(--lh-1); font-weight:600;
  letter-spacing:0.1em; text-transform:uppercase;
  color:var(--cream-dim);
  padding:11px 14px 5px;
}
.combo-opt{
  min-height:52px;
  display:flex; flex-direction:column; align-items:flex-start; justify-content:center; gap:2px;
  width:100%; text-align:left;
  padding:11px 14px;
  border:0; background:transparent;
  color:var(--cream);
  font-family:var(--sans); font-size:0.92rem;
  border-radius:var(--r-3);
  cursor:pointer;
  touch-action:manipulation;
}
.combo-opt .combo-label{ overflow-wrap:anywhere; }
.combo-opt .sub{
  font-family:var(--data-font); font-size:var(--t-2); line-height:var(--lh-2);
  color:var(--cream-dim); overflow-wrap:anywhere;
}
.combo-opt.hi{ background:var(--tint-copper); color:var(--copper-strong); font-weight:600; }
.combo-opt.sel .combo-label{ color:var(--copper-strong); }
.combo-opt.free, .combo-opt.more{ color:var(--ice); }
.combo-opt.free.hi, .combo-opt.more.hi{ background:var(--tint-ice); color:var(--ice); }
.combo-empty{
  padding:13px 14px;
  font-family:var(--sans); font-size:0.85rem; font-style:italic;
  color:var(--cream-dim);
}

/* combobox inside a bottom sheet: open upward so the list stays on screen */
/* only while a combobox is open may its drop-up escape the sheet's box —
   permanently visible overflow made the sheet a fake scroller (no native
   momentum, no rubber-band; felt like it "flew" with zero friction) */
dialog.sheet:has(.combo.open){ overflow:visible; }
/* direction is measured in combobox.js now — this rule used to force EVERY
   in-sheet list upward, which put the pop over the sheet's own title and, at
   full height, floated it right off the sheet onto the dimmed page. */
.combo-pop.up{ top:auto; bottom:calc(100% + 8px); }
dialog.sheet .combo-pop{ max-height:min(300px, 42vh); }
.scan-row{ display:flex; align-items:center; flex-wrap:wrap; gap:10px; margin-top:12px; }
/* a tinted container instead of bare red text that shoved the actions down */
.scan-msg{
  font-family:var(--sans);
  font-size:var(--t-3);
  line-height:var(--lh-3);
  margin:10px 0 0;
  padding:9px 12px;
  border-radius:var(--r-2);
  color:var(--ice);
  background:var(--tint-ice);
  border:1px solid var(--edge-ice);
}
.scan-msg.bad{ color:var(--warn); background:var(--tint-warn); border-color:color-mix(in srgb, var(--warn) 40%, transparent); }
.link-row{ display:flex; gap:var(--s2); align-items:center; margin-top:var(--s3); }
.link-row input{ flex:1 1 auto; min-width:0; }
.link-row .btn{ flex:0 0 auto; }
.bean-hint{
  font-family:var(--data-font); font-size:0.68rem; letter-spacing:0.05em;
  color:var(--ice); line-height:1.6; margin-top:9px; padding-left:14px;
}

/* ============ brew hero stickiness ============ */
/* Desktop two-column layout: keep the dial in view while the controls column
   scrolls. MUST stay media-gated: iOS Safari constrains sticky grid items to
   the whole grid instead of their row, so in the single-column phone layout a
   sticky hero slides over the controls (the mini-readout bar is the mobile
   answer instead). Grid goes two-column at >=804px viewport (2x340 + 72 gap
   + 52 page padding). */
@media (min-width: 900px){
  .brew-hero{ position:sticky; top:20px; }
}
/* with the fixed top bar the dial pins at its resting offset, so it reads as
   fixed — no initial slide before the sticky engages */
@media (min-width: 900px){
  .brew-hero{ top:148px; }
}

/* ---- desktop: it stopped responding above 1080px (identical 478px columns at
   1280 AND 1440), the dial measured 232px at every width from 390 to 1440, and
   the hero numeral was the only type on the page that scaled. ---- */
.dial-svg{ width:clamp(232px, 26vw, 320px) !important; }
@media (min-width: 900px){
  h1{ font-size:clamp(1.45rem, 1.6vw, 1.9rem) !important; }
  /* prose and hints stop stretching to 95 characters at a single column */
  .sheet-summary, .empty-sub, .notes{ max-width:62ch; }
}
@media (min-width: 1200px){
  .page{ max-width:1240px !important; }
}

/* ============ device sim (?sim=iphone) ============
   Desktop browsers report safe-area insets as 0px, so the floating nav island
   and docked bars render lower than on a real phone — the hardest geometry to
   verify off-device. Adding ?sim=iphone to the URL reproduces an iPhone
   14/15 Pro standalone-PWA viewport: status-bar/dynamic-island inset up top,
   home-indicator inset below, plus visual stand-ins for both. Testing only;
   never activates without the query param. */
html.sim-iphone{
  --sa-top: 59px;
  --sa-bottom: 34px;
}
html.sim-iphone::before{
  /* dynamic island */
  content:"";
  position:fixed;
  top:11px; left:50%;
  transform:translateX(-50%);
  width:125px; height:37px;
  border-radius:var(--r-4);
  background:#000;
  box-shadow:0 0 0 1px rgba(255,255,255,0.05);
  z-index:99;
  pointer-events:none;
}
html.sim-iphone::after{
  /* home indicator */
  content:"";
  position:fixed;
  bottom:9px; left:50%;
  transform:translateX(-50%);
  width:140px; height:5px;
  border-radius:3px;
  background:currentColor;
  opacity:.4;
  z-index:99;
  pointer-events:none;
}

/* promised-vs-tasted scorecard (journal) */
.cmp{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:6px;
  margin-top:12px;
}
/* journal notes read in the same quoted-italic voice in both views (the
   class previously had no rule at all — placeholder dimming was dead) */
.notes{
  font-family:var(--sans);
  font-size:0.9rem; font-style:italic; line-height:1.55;
  color:var(--cream); margin:0; overflow-wrap:anywhere;
}
.notes.placeholder{ color:var(--cream-dim); font-size:0.85rem; }
.cmp-title{
  font-family:var(--label-font);
  font-size:0.625rem;
  line-height:var(--lh-1);
  font-weight:600;
  letter-spacing:0.1em;
  text-transform:uppercase;
  color:var(--cream-dim);
}
.cmp-chip{
  font-family:var(--sans);
  font-size:0.72rem;
  padding:3px 10px;
  border-radius:99px;
  border:1px solid var(--line);
  color:var(--cream-dim);
}
.cmp-chip.hit{
  border-color:var(--copper-quiet);
  background:var(--cmp-hit);
  color:var(--copper-strong);
  font-weight:600;
}
.cmp-score{
  font-family:var(--label-font);
  font-size:0.62rem;
  letter-spacing:0.04em;
  color:var(--ice);
  margin-left:2px;
  white-space:nowrap;
}

/* Start sheet: a chosen bean is confirmed, not re-picked (protects a
   scanned/typed bean from an accidental catalog tap) */
.chosen-bean{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:12px;
  padding:12px 16px;
  background:var(--panel-raised);
  border:1px solid var(--line);
  border-radius:var(--r-3);
}
.chosen-bean-name{
  font-family:var(--sans);
  font-size:0.98rem;
  font-weight:600;
  letter-spacing:-0.01em;
  color:var(--cream);
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.chosen-bean-roaster{
  font-family:var(--label-font);
  font-size:0.68rem;
  color:var(--cream-dim);
  margin-top:3px;
}
.chosen-bean-change{
  flex-shrink:0;
  background:none;
  border:none;
  color:var(--ice);
  font-family:var(--sans);
  font-size:0.82rem;
  font-weight:600;
  cursor:pointer;
  padding:6px 4px;
  min-height:44px;
}

/* ============ shader flourishes (Paper shaders) ============ */
/* the steep ring's breathing glow — a square host centered on the ring;
   the library's canvas fills it (absolute, z:-1, inherits the 50% radius) */
.steep-glow{
  position:absolute;
  inset:-46px;
  z-index:-1;
  border-radius:50%;
  animation:glowIn 1.4s ease both;
}
@keyframes glowIn{ from{ opacity:0; } }

/* the dialed-in badge screen: not a toast — it holds until dismissed and
   reads like an award: the goo medallion, the claim, the recipe that earned
   it. A native <dialog>, so Escape, backdrop tap and focus all behave. */
dialog.badge-screen{
  background:transparent;
  border:none;
  padding:24px;
  max-width:min(92vw, 400px);
  color:var(--cream);
  text-align:center;
}
/* WebKit does not apply backdrop-filter to ::backdrop, so the declared blur
   never ran and all that held the takeover together was a 60–72% wash — the
   Steep empty state printed straight through it and "Dial in a brew" landed on
   top of the bean name. The celebration is opaque now; no filter to depend on.
   crema's ink is dark, so its takeover goes cream where midnight's goes
   near-black — one rule for both, now that the two ends of the vignette are
   tokens rather than a hand-copied pair of hexes per theme. */
dialog.badge-screen::backdrop{
  background:radial-gradient(120% 90% at 50% 32%, var(--takeover-inner) 0%, var(--takeover-outer) 100%);
}
dialog.badge-screen:focus{ outline:none; }
.badge-stack{
  display:grid;
  justify-items:center;
  gap:6px;
}
.badge-stack > *{ animation:badgeUp 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
.badge-coin{
  position:relative;
  width:176px; height:176px;
  border-radius:50%;
  overflow:hidden;
  margin-bottom:16px;
  box-shadow:0 0 0 1.5px var(--copper-quiet), var(--e4);
  animation:coinPop 0.55s cubic-bezier(0.2, 1.35, 0.35, 1) both;
}
.dialed-host{ position:absolute; inset:0; }
.badge-title{
  margin:0;
  font-family:var(--display-font);
  font-weight:var(--display-weight, 600);
  font-size:1.9rem;
  letter-spacing:-0.02em;
  animation-delay:0.1s;
}
.badge-bean{
  font-family:var(--sans);
  font-size:1rem;
  font-weight:600;
  overflow-wrap:anywhere;
  animation-delay:0.18s;
}
.badge-line{
  font-family:var(--data-font);
  font-size:0.72rem;
  letter-spacing:0.06em;
  color:var(--cream-dim);
  animation-delay:0.24s;
}
.badge-note{
  margin:12px 0 20px;
  font-family:var(--sans);
  font-size:0.88rem;
  line-height:1.55;
  color:var(--cream-dim);
  max-width:30ch;
  animation-delay:0.32s;
}
.badge-stack .btn{ animation-delay:0.45s; }
@keyframes badgeUp{ from{ opacity:0; transform:translateY(10px); } }
@keyframes coinPop{ from{ opacity:0; transform:scale(0.8); } }


/* ---- combobox: the escape hatch has to be reachable ---- */
.combo-opt.more{
  position:sticky; bottom:-8px;
  background:var(--panel-raised);
  box-shadow:0 -8px 12px -8px hsl(var(--shadow-color) / 0.5);
  border-top:1px solid var(--line-soft);
  border-radius:0 0 var(--r-4) var(--r-4);
  min-height:44px;
  font-weight:600;
}
.combo-opt.more.hi{ background:var(--tint-ice); }

/* A picker's list is z-70 and the nav is z-40, but the Brew form column carries
   a cardIn entrance animation, which makes it a stacking context and traps the
   list inside it — so the floating chrome sliced the last options in half.
   Lift the open column, and get the bars out of the way while choosing. */
main section > div:has(.combo.open){ position:relative; z-index:60; }
body:has(.combo.open) .tabbar-nav,
body:has(.combo.open) .mini-readout,
body:has(.combo.open) .nav-fade{ opacity:0; pointer-events:none; }
.tabbar-nav, .mini-readout, .nav-fade{ transition:opacity var(--dur-2) ease; }


/* The Steep tab's actions must clear the floating island at ANY content length.
   This lives on .page because nothing in the app carries id="activeSection" —
   an earlier version of this rule targeted that id and silently did nothing. */
.page{ padding-bottom:calc(150px + var(--sa-bottom)) !important; }


/* ---- journal ---- */
/* JRN-05: `padding: 22px 0` sat on the <details>, not inside <summary>, so the
   padding band was OUTSIDE the hit area — 40px-tall rows separated by 45px of
   dead space, and the one interactive element in the tab under the 44px floor
   that .btn/.chip/.tab all respect. */
details[data-t="brew"]{ padding:0 !important; }
details[data-t="brew"] > summary{ padding:18px 0; min-height:44px; }
details[data-t="brew"][open] > summary{ padding-bottom:10px; }
details[data-t="brew"][open]{ padding-bottom:18px !important; }

/* JRN-06: the note is the one human thing in the record and it was hidden
   behind every disclosure while the row spent its only line on metadata the
   expanded body then repeated */
/* teaser on the face, full note in the body — never both at once */
details[open] > summary .note-peek{ display:none; }
.note-peek{
  display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical;
  overflow:hidden;
  font-family:var(--sans); font-style:italic;
  font-size:var(--t-3); line-height:var(--lh-3);
  color:var(--cream-dim);
  margin-top:5px;
}

/* JRN-07/JRN-08: title, chips and verdict were peer flex items, so the score
   sentence landed wherever the wrap left room and read as a run-on */
.cmp{ display:grid; gap:7px; margin-top:12px; }
.cmp-chips{ display:flex; flex-wrap:wrap; gap:6px; }
.cmp-score{ margin-left:0 !important; white-space:normal; font-size:var(--t-2); line-height:var(--lh-2); }

/* JRN-10: the copper "landed" pill was the payoff of the whole feature and the
   least legible text in the card (3.93:1) — the tint lifted the ground and ate
   the ratio. The check and the border carry the meaning instead. */
.cmp-chip.hit{
  background:transparent;
  border:1px solid var(--copper-strong);
  color:var(--copper-strong);
  font-weight:600;
}

/* JRN-09: long bean names ran to four lines with the rating stranded top-right */
/* the markup carries .bean-card-title and inlines its own clamp; this selector
   never matched anything. Brew rows are the ones that actually need clamping. */
details[data-t="brew"] > summary h3{
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
details[data-t="brew"] > summary{ align-items:flex-start; }

/* JRN-14: a 7,000px undifferentiated scroll at 50 brews */
.month-rule{
  position:sticky; top:0; z-index:1;
  font-family:var(--label-font); font-size:0.625rem; font-weight:600;
  letter-spacing:0.1em; text-transform:uppercase;
  color:var(--cream-dim);
  background:var(--bg);
  padding:10px 0 6px;
  border-bottom:1px solid var(--line-soft);
}

/* JRN-12: two free-standing pills read as two independent buttons; this is one
   choice. And in midnight the selected chip was copper while the primary button
   was mint — two brand accents doing the same job in one cluster. */
.seg{
  display:inline-flex; gap:0; padding:3px;
  border-radius:var(--r-pill);
  border:1px solid var(--line);
  background:var(--panel);
}
/* the theme blocks reach these with html[data-theme='…'] .chip.active — (0,3,1).
   A bare .seg .chip.active is (0,3,0) and silently loses, which left the
   selected segment rendering as the solid pill this control replaced. */
html .seg .chip{
  border:none; background:transparent; min-height:38px;
  border-radius:var(--r-pill); /* a 12px chip inside a 99px track crossed its stroke */
}
html .seg .chip.active,
html[data-theme='crema'] .seg .chip.active,
html[data-theme='midnight'] .seg .chip.active{
  background:var(--panel-raised);
  color:var(--cream);
  border-color:transparent;
  font-weight:600;
  box-shadow:var(--e1);
}

/* the tasting note is the point of the journal — let it read like one */
.notes{
  padding-left:12px;
  border-left:2px solid var(--copper-quiet);
}
.notes.placeholder{ border-left-color:var(--line-soft); }

/* a labelled, contained block at the end of the sheet — not an unlabelled red
   link floating between two hairlines in the middle of Settings */
.danger-zone{
  margin-top:22px;
  padding:14px 16px;
  border:1px solid color-mix(in srgb, var(--warn) 34%, transparent);
  border-radius:var(--r-3);
  background:var(--tint-warn);
}

/* the moment you commit to an 18-hour wait deserves to say when it ends */
.start-promise{
  font-family:var(--sans);
  font-size:var(--t-4);
  line-height:var(--lh-4);
  color:var(--ice);
  margin:-8px 0 18px;
}
/* the highest-stakes field in the product should not look like a name input */
input.code-input{
  font-size:1.4rem;
  letter-spacing:0.4em;
  text-align:center;
  font-variant-numeric:tabular-nums;
  padding-right:calc(14px - 0.4em);
}
.hint-line{
  font-family:var(--data-font);
  font-size:var(--t-2);
  color:var(--cream-dim);
  margin-top:6px;
  text-align:center;
}

/* the disclosure affordance the vessel nuance never had — same copper micro-link
   the app already uses, but NOT .tune-reset (that one means "undo the tuning") */
.more-less{
  margin-left:6px;
  color:var(--copper-strong);
  font-weight:600;
  text-decoration:underline;
  text-underline-offset:2px;
  white-space:nowrap;
}

/* the steep stepper read as two floating glyphs — every sibling control carries
   the panel fill, and its minus was a hyphen next to a full-width plus */
.btn.small.ghost[aria-label^="Steep an hour"]{
  background:var(--panel);
  min-width:44px;
  font-size:1.05rem;
  line-height:1;
}

/* every scrollable tab needs the floating island's clearance, not just Steep */
main section{ padding-bottom:calc(24px + var(--sa-bottom)); }

/* ---- Journal opening: the memory, then the numbers ---- */
.best-cup{
  border-left:2px solid var(--copper-quiet);
  padding:2px 0 2px 14px;
}
.best-cup h3{
  font-family:var(--display-font);
  font-size:var(--t-6); line-height:var(--lh-6);
  font-weight:500; color:var(--cream);
  margin:4px 0 0;
  overflow-wrap:anywhere;
}
.best-roaster{ font-family:var(--sans); font-size:var(--t-2); color:var(--cream-dim); }
.best-note{
  font-family:var(--sans); font-style:italic;
  font-size:var(--t-3); line-height:var(--lh-3);
  color:var(--cream-dim);
  margin:8px 0 0;
}
.best-meta{
  font-family:var(--data-font); font-size:var(--t-2);
  color:var(--cream-dim); margin-top:6px;
}
.j-receipt{
  font-family:var(--data-font); font-size:var(--t-2); line-height:var(--lh-2);
  letter-spacing:0.03em; color:var(--cream-dim);
  margin-top:14px;
}
