/* Brew timeline — component + scene styles, ported from public/brew-timeline-mockups.html
   and scoped under .brew-timeline. Scene tokens map to the app theme where it defines
   them (--color-*), with the mock's warm-editorial hex as fallback. */
.brew-timeline{
  --bg:        var(--color-bg, #f5f1e8);
  --surface:   var(--color-surface, #ffffff);
  --surface2:  var(--color-surface-2, #efe9d7);
  --border:    var(--color-border, #e6dfcc);
  --ink:       var(--color-ink, #2f1f19);
  --ink-soft:  var(--color-ink-soft, #6e5d54);
  --accent:    var(--color-accent, #d97757);
  --accent-2:  var(--color-accent-2, #c25a38);
  /* Accent-tint steps this file uses under the local --accent convention
     (see application.css for the full step list + dark rgba values). The
     bt-cell/bt-spin rules further down already reference var(--color-*)
     directly, so those use --color-accent-tint-* inline instead of these
     locals — matching each section's own existing convention. */
  --accent-tint-12: var(--color-accent-tint-12, rgba(217,119,87,.12));
  --accent-tint-42: var(--color-accent-tint-42, rgba(217,119,87,.42));
  --accent-tint-50: var(--color-accent-tint-50, rgba(217,119,87,.50));
  /* Phase hues — one source for the blocks on the ruler, the chips under it
     and the legend above it, so the three can never drift into three colour
     schemes. Warm-editorial throughout: brew owns the accent because it is
     the part of a brew that matters, and the others step away from it in
     lightness rather than hue.

     Deliberately NOT a CVD-validated categorical palette. Running the four
     through the palette validator, every combination that clears the
     normal-vision separation floor needs a teal and a violet — hues this site
     does not have and a coffee timeline should not invent. The check exists
     for charts where colour IS the encoding; here it never is: every block is
     named by a numbered chip directly beneath it, and the legend spells out
     each phase in words with its duration. Colour groups; the text
     identifies. */
  --ph-prep:#8c7d6f; --ph-brew:#d97757; --ph-milk:#b98a4e; --ph-serve:#8a5a33;
  /* scene-only palette (no app equivalent) */
  --coffee:#8a5a33; --coffee-dark:#4e2f1c; --blond:#d9a05e; --water:#7fa8b8;
  --milk:#e3cba4; --foam:#f4ecdc; --chocolate:#3a2313;
  /* The scale's LCD: literal, not theme-aliased. --ink/--bg flip in .dark
     (see application.css), which would invert this into a white slab on a
     dark card. A real scale's display doesn't relight itself for the room —
     it's always a dark plate with warm-lit digits. */
  --lcd-bg:#20241f; --lcd-fg:#f2c879;
  --bt-shadow:0 1px 2px rgba(47,31,25,.05), 0 8px 24px rgba(47,31,25,.07);
  color:var(--ink);
}

/* Dark mode gets its own steps, not an automatic flip: #8a5a33 on a #1b1512
   card is nearly invisible, and #8c7d6f loses the little separation it has
   from the surface. Same hues, lighter and a shade more saturated.

   Palette round (2026-08-04): the first dark set above (pale pastels, OKLCH
   L 0.68-0.76) validated as FAILING — worst adjacent pair (milk↔brew) sat at
   ΔE 6.1, deep in "mush together" territory (floor is 15). Re-derived against
   the new charcoal --surface (#1b1a18) with
   dataviz/scripts/validate_palette.js --mode dark: all four pulled down into
   the dark OKLCH lightness band (0.48-0.67) and pushed further apart in hue
   (coral ~18°, rust ~38°, tan ~78°, gold ~103°) so adjacent pairs clear the
   normal-vision floor (worst: prep↔brew ΔE 17.0). CVD (deutan) separation on
   that same pair sits at 6.6 — a WARN, not a hard fail — matching this
   project's existing stance on the light set (see the comment above this
   block): a coffee timeline doesn't have a teal or a violet to spend, and
   every segment already carries a direct text label + numbered chip, so
   color groups rather than being the sole identifier. Full report + old→new
   table + validator output: palette-report.md. */
.dark .brew-timeline{
  --ph-prep:#ac7c26; --ph-brew:#cb364b; --ph-milk:#a19400; --ph-serve:#b64118;
}

/* ── Overview strip: every step's scene looping at a glance ── */
.brew-timeline .bt-overview{display:grid;grid-template-columns:repeat(var(--ov-cols,4),minmax(0,1fr));
  gap:16px 28px;margin-bottom:8px;list-style:none;padding:0}
.brew-timeline .bt-ov-tile{margin:0;min-width:0;position:relative}
.brew-timeline .bt-ov-btn{display:block;width:100%;text-align:left;background:none;border:0;padding:0;
  cursor:pointer;color:inherit;font:inherit}
/* .bt-ov-btn:hover .bt-ov-scene / .bt-ov-btn:hover .bt-ov-cap (Task 5):
   both stay KEEP, for two different reasons.

   .bt-ov-scene: one of the two elements the "dark paper-tile" pin targets
   (.dark .brew-timeline .stage-visual, .bt-ov-scene further down) — that
   pin overrides this file's LOCAL --accent alias only, not the global
   --color-accent/--accent root token a group-hover:border-accent utility
   would read, so converting the hover half would make hovering a scene
   tile in dark mode briefly show the dark theme's accent hue instead of
   the pinned light one.

   .bt-ov-cap: no pin risk on its own, but its BASE rule (below) has to
   stay CSS regardless — studio.html.erb's own inline overview strip
   (recipes/studio.html.erb, not this partial) reuses the same class name
   directly in its own markup, so deleting the base rule would leave that
   copy unstyled, and editing studio.html.erb is out of this task's file
   scope. That base rule being unlayered CSS is what actually blocks the
   hover conversion: tried `group`/`group-hover:text-accent` in
   _overview.html.erb first — per the CSS Cascade Layers spec, an
   unlayered rule beats ANY layered rule for a conflicting property
   regardless of specificity, INCLUDING one gated by a pseudo-class/
   variant that would otherwise "obviously" win when active. Confirmed
   empirically: `.bt-ov-btn:hover` matched, `--accent` resolved correctly,
   and the color still never changed — .bt-ov-cap's base `color:var(--ink)`
   (unlayered) silently out-prioritized `group-hover:text-accent` (in
   @layer utilities) even while actively hovered. Reverted. This is the
   same mechanism as .bt-icon-btn's trap above, in a new shape: it isn't
   only "deleting an unlayered rule can un-block another" — an unlayered
   rule you keep can ALSO permanently block any Tailwind utility, hover or
   not, from ever winning on that property. A partial KEEP-base/CONVERT-
   hover split is only safe when the two halves touch DIFFERENT
   properties; here both set `color`, so it isn't. */
.brew-timeline .bt-ov-btn:hover .bt-ov-scene{border-color:var(--accent)}
.brew-timeline .bt-ov-btn:hover .bt-ov-cap{color:var(--accent)}
/* the connector lives in the column gap; the last tile of a row has none */
.brew-timeline .bt-ov-tile:not(.is-row-end):not(:last-child)::after{
  content:"";position:absolute;right:-22px;top:calc(50% - 14px);width:16px;
  border-top:1px dashed var(--border)}
@media (max-width:640px){
  .brew-timeline .bt-overview{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px}
  .brew-timeline .bt-ov-tile::after{display:none}
}
.brew-timeline .bt-ov-scene{display:block;position:relative;aspect-ratio:440/300;background:var(--surface2);border:1px dashed var(--border);border-radius:6px;overflow:hidden}
/* The tile's EDGE, in light mode only. Its cream ground sits at 1.21 against a
   white page (measured) — enough for a 395px card, nothing at all for an 88px
   strip tile, where the dashed --border hairline dissolved into the page and
   the drawing read as floating. Dark mode is untouched: the pin below turns
   this tile into white paper on a dark page, which separates on its own.
   Scoped by :not(.dark *) rather than overridden under .dark, so the dark
   rules further down this file keep winning without a specificity contest. */
html:not(.dark) .brew-timeline .bt-ov-scene{border-color:#cdbf9e}
.brew-timeline .bt-ov-cap{display:flex;align-items:center;gap:7px;margin-top:7px;font:600 12.5px/1.2 "Inter";color:var(--ink)}
.brew-timeline .bt-ov-n{display:grid;place-items:center;width:18px;height:18px;border-radius:999px;background:var(--accent);color:#fff;font:700 10px/1 "JetBrains Mono",monospace;flex:none}

/* ── "Behind every step": animation + the why, one row per step ──
   Task 5: every static declaration here moved into _step_note_row.html.erb/
   _step_note_rows.html.erb's own class="…" (bare .brew-timeline scope, no
   studio.css overlap, no JS class-toggling beyond textContent — none of the
   dark-pin/JS-coupling traps the rest of this file has). Nothing left to
   keep. */

/* ── Card ──
   A plain surface, no graph-paper grid. The panel inside it is an instrument
   face now — bordered cells, steppers, an edited cell in accent — and a ruled
   background behind all of that was one texture too many. The page's own
   dot-grid still shows around the card, so it stays a sheet with an
   instrument set down on it. --surface, not literal white: this has to darken
   with the rest of the page in dark mode. */
.brew-timeline.card{background:var(--surface);border:1px dashed var(--border);border-radius:8px;
  box-shadow:var(--bt-shadow);padding:clamp(14px,2.5vw,24px)}
/* .card-head (Task 5): deleted — dead, no producer anywhere in the app
   (the player's own header is .bt-topbar/.bt-meta further down). */

/* Palette round 5 (2026-08-04): "the WHOLE Brew-with-us section should read
   as one light sheet in dark mode, not just the apparatus panel/tiles" —
   round 4 (0273d5f) pinned .stage-visual/.bt-ov-scene individually; this
   extends the identical paper-document idiom to .brew-timeline.card ITSELF
   (== .bt-player — _player.html.erb's <section class="brew-timeline
   bt-player bt-player--<chrome> card">, the actual player instance, on
   every chrome: :inline on /brew-methods/:slug and /drinks/:id, :full on
   /play, :embed inside the studio's own preview — same class, so all three
   inherit this rule with no fork). Every descendant inside the card — the
   grinder combobox, the figure cells, the timeline strip/chips, the step
   prose, the play controls — repaints off these SAME pinned tokens through
   the ordinary cascade, exactly like .stage-visual's own scene SVGs already
   did one section up: nothing here is a markup fork.

   Two families of consumer, both pinned:
   - This file's OWN local var(--ink)/var(--surface2)/var(--accent)/etc —
     .tl-chip, .tl-strip .tl-seg, .bt-btn-ghost, .bt-more-panel, the
     scene SVGs, and so on all read these directly, same as round 4.
   - Tailwind's global theme tokens (`@theme inline` in application.css
     aliases --color-X to `var(--X)`, declared once at :root) — the
     combobox partial (shared/ui/_combobox.html.erb) and the newer
     "instruments" section below (.bt-cell/.bt-spin/.bt-dirty/.bt-specline)
     style themselves with bg-surface/border-border/text-ink-soft utilities
     and bare var(--color-ink)/var(--color-border) etc, never this file's
     local aliases. A nested var() reference resolves against whatever
     value its target inherits AT THE ELEMENT USING IT, not where the alias
     was declared, so overriding the plain names above already reaches most
     of these for free — pinned again here anyway, directly, so this rule
     doesn't depend on that indirection working out for every path a future
     edit might add.

   --accent-tint-06/08/10/20: read straight off the global token by
   .bt-cell.is-edited and .bt-spin's hover/active states — never through
   this file's own local alias (which only covers -12/-42/-50, the three
   this file's pre-existing rules use) — so they need pinning by name too,
   same rgba steps as --color-accent-tint-NN below.

   --ph-prep/brew/milk/serve: the phase legend/track/chips read these local
   vars directly, and .dark .brew-timeline (above) already re-tunes them
   dark for the rest of the page — restored to light here, at higher
   specificity (3 classes vs. 2), so a pinned light card doesn't carry
   dark-mode-tuned phase hues.

   .stage-visual/.bt-ov-scene/.bt-scale-lcd's own dark-mode rules (below)
   still apply inside this pin — same light values now either way,
   redundant but harmless; the LCD border logic is untouched, it already
   keys off var(--border), which resolves to this same pinned value.

   Every value below is light mode's own value (:root / this file's own
   light .brew-timeline block above) — light mode is untouched, since none
   of this can ever match without .dark on an ancestor. */
.dark .brew-timeline.card{
  --bg:        #ffffff;
  --surface:   #ffffff;
  --surface2:  #efe9d7;
  --border:    #e6dfcc;
  --ink:       #2f1f19;
  --ink-soft:  #6e5d54;
  --accent:    #d97757;
  --accent-2:  #c25f42;
  --accent-tint-06: rgba(217,119,87,.06);
  --accent-tint-08: rgba(217,119,87,.08);
  --accent-tint-10: rgba(217,119,87,.10);
  --accent-tint-12: rgba(217,119,87,.12);
  --accent-tint-20: rgba(217,119,87,.20);
  --accent-tint-42: rgba(217,119,87,.42);
  --accent-tint-50: rgba(217,119,87,.50);
  --ph-prep:#8c7d6f; --ph-brew:#d97757; --ph-milk:#b98a4e; --ph-serve:#8a5a33;
  --color-bg:        #ffffff;
  --color-surface:   #ffffff;
  --color-surface-2: #efe9d7;
  --color-border:    #e6dfcc;
  --color-ink:       #2f1f19;
  --color-ink-soft:  #6e5d54;
  --color-accent:    #d97757;
  --color-accent-2:  #c25f42;
  --color-accent-tint-06: rgba(217,119,87,.06);
  --color-accent-tint-08: rgba(217,119,87,.08);
  --color-accent-tint-10: rgba(217,119,87,.10);
  --color-accent-tint-12: rgba(217,119,87,.12);
  --color-accent-tint-20: rgba(217,119,87,.20);
  --color-accent-tint-42: rgba(217,119,87,.42);
  --color-accent-tint-50: rgba(217,119,87,.50);
}

/* ── Timeline bar ──
   .tl-bar, .tl-seglbl and .tl-ticks (+ children) deleted (Task 5): confirmed
   dead — mockup-era selectors with no matching element anywhere in the app
   (the ruler's real DOM is #tlBar/.tl-strip below, already flagged by this
   file's own "Responsive" section comment before this cleanup). */
.brew-timeline .tl-seg{position:absolute;top:0;bottom:0;border-right:1px dashed rgba(47,31,25,.35);pointer-events:none;overflow:hidden}
.brew-timeline .tl-seg:last-of-type{border-right:0}
.brew-timeline .tl-hatch{position:absolute;inset:0;width:0%;background:repeating-linear-gradient(45deg,rgba(47,31,25,.22) 0 2px,transparent 2px 7px)}
.brew-timeline .tl-seg.now .tl-hatch{background:repeating-linear-gradient(45deg,var(--accent-tint-50) 0 2px,transparent 2px 7px)}
.brew-timeline .tl-cursor{position:absolute;top:-10px;bottom:-10px;left:0%;width:2px;background:var(--accent);pointer-events:none;z-index:3}
.brew-timeline .tl-cursor::after{content:"";position:absolute;top:-1px;left:-4px;width:10px;height:10px;border-radius:50%;background:var(--accent)}
.brew-timeline .tl-cursor .tag{position:absolute;top:-25px;left:50%;transform:translateX(-50%);background:var(--accent);color:#fff;
  font:700 10px/1 "JetBrains Mono",monospace;padding:3px 7px;border-radius:2px;white-space:nowrap;font-variant-numeric:tabular-nums}

/* ── Stage ── */
/* Two columns, two rows — the weighing surface over its readout on the left,
   the ruler and the step over the controls on the right. The rows are what
   the layout is actually for: Play sits level with the LCD because they share
   row 2, not because anything was nudged into place. .stage-surface keeps its
   wrapper (the strip has to match the surface's width, and a test pins the
   nesting) and adopts the parent's rows with subgrid, so its two children land
   on the same lines the right-hand column uses. Without subgrid support it
   degrades to its own two-row stack: still correct, just not aligned across.

   The apparatus is also capped: 1fr of a 1900px card drew a 740px-wide scale
   with a 500px-tall column of white space beside it. */
.brew-timeline .stage{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);
  grid-template-rows:auto auto;gap:clamp(14px,2.5vw,26px);row-gap:10px;
  margin-top:8px;padding-top:12px;border-top:1px dashed rgba(47,31,25,.12)}
/* No gap between the weighing surface and its readout: they are one
   instrument, and a 10px channel between them made the plate read as a
   separate widget parked underneath. The adjoining corners square off for the
   same reason — see .stage-visual and .bt-scale-lcd below. */
.brew-timeline .stage-surface{grid-column:1;grid-row:1 / 3;display:grid;
  grid-template-rows:subgrid;row-gap:0;min-width:0;max-width:560px}
/* Inside row 1: the ruler and the step travel together, centred against the
   weighing surface across the way. They are one block — the bar says where
   you are, the step says what you are doing — so pinning the bar to the top
   and floating the step below it split a single thought in two. */
.brew-timeline .stage-side{grid-column:2;grid-row:1;display:flex;flex-direction:column;
  justify-content:center;min-width:0}
.brew-timeline .controls{grid-column:2;grid-row:2;align-self:center}
.brew-timeline .stage-visual{position:relative;aspect-ratio:440/300;background:var(--surface2);
  border:1px dashed var(--border);border-radius:6px 6px 0 0;border-bottom:0;overflow:hidden}
.brew-timeline .scene{position:absolute;inset:0;width:100%;height:100%;opacity:0;visibility:hidden;transition:opacity .28s ease}
.brew-timeline .scene.is-active{opacity:1;visibility:visible}

/* Palette round 4 (2026-08-04, superseded for the player card by round 5's
   own .dark .brew-timeline.card pin above — kept here because .bt-ov-scene
   tiles live in the Overview/Behind-every-step sections, bare .brew-timeline
   with no .card, which round 5 never touches): the scene surfaces — this
   apparatus/animation panel (.stage-visual, above) and the two thumbnail
   idioms drawn from the same scene library (.bt-ov-scene, Overview strip
   section below:
   the Overview strip's own tiles, and — since _step_note_row.html.erb
   layers .bt-note-scene onto that very same element — the Behind-every-step
   row too) — are a printed-reference idiom, same as the grind chart panel
   (476f52e): scene art reads as ink on paper regardless of which theme the
   rest of the page is in, not a themed surface that inverts along with
   everything else around it. Pinning the LOCAL tokens on the panel elements
   themselves repaints every descendant for free through the ordinary
   custom-property cascade — every scene SVG (.scene .ln/.lbl-svg/.num-svg/
   .cap-svg above, plus each scenes/_*.html.erb partial's own shapes) draws
   exclusively off these, so no markup fork and the scene SVGs themselves
   stay untouched, matching the grind-chart-panel fix's own shape. The
   studio's embedded preview (recipes/studio.html.erb → brew_methods/player,
   chrome: :embed) renders .stage-visual through this same .brew-timeline
   wrapper, so it inherits the pin too — consistent, not a separate fix.

   Two tokens beyond the grind chart's own five: --bg (scenes/_scale.html.erb
   draws its own miniature LCD readout as fill:var(--bg) text on a
   fill:var(--ink) plate — left unpinned, pinning --ink light while --bg
   stayed dark would turn that pair into dark-on-dark, invisible digits) and
   --accent (several scenes stroke a flame/ring/drip-arrow in it; dark mode's
   --accent, #e8946f, is a distinct hex from light's #d97757 — unpinned, one
   decorative stroke would stay dark-mode-toned inside an otherwise
   pinned-light panel). Both pinned to their light-mode values alongside the
   rest so the panel matches light mode exactly, stroke for stroke.

   Placed here, after both real .stage-visual/.bt-ov-scene definitions
   (rather than up with this file's other .dark overrides): the selector
   text below contains the literal substring ".brew-timeline .bt-ov-scene{",
   and brew_player_test.rb's "flow map tile ... non-inline display" test
   greps this file for the FIRST such substring to check the real rule's
   `display` — placed earlier, this block's own (display-less) token
   overrides would shadow that match. */
.dark .brew-timeline .stage-visual,
.dark .brew-timeline .bt-ov-scene{
  --bg:       #ffffff;
  --surface:  #ffffff;
  --surface2: #efe9d7;
  --border:   #e6dfcc;
  --ink:      #2f1f19;
  --ink-soft: #6e5d54;
  --accent:   #d97757;
}
/* The scale's LCD (.bt-scale-lcd, in .stage-surface alongside .stage-visual)
   is seamed directly onto .stage-visual's own bottom edge — no gap, no
   border in light mode; see .bt-scale/.bt-scale-lcd's own comments further
   down this file ("the readout is the same instrument as the surface above
   it" / "reads as part of the same drawing"). 598ad1c's dark-only border
   exists purely to give the LCD's always-literal-dark plate an edge against
   .stage-visual's OWN dark --surface2 behind it (see that rule's own
   comment) — now that .stage-visual pins light above it, the seam is back to
   exactly light mode's own (a dark plate under a pale panel, no border at
   all), so the dark-only border has to stand down here. */
.dark .brew-timeline .stage-surface .bt-scale-lcd{border:0}

.brew-timeline .step-kicker{font:600 11px/1 "JetBrains Mono",monospace;letter-spacing:.22em;text-transform:uppercase;color:var(--accent)}
/* .clock/.clock small (Task 5): deleted — class="clock" renders in exactly
   one place (inside .bt-meta), so .bt-meta .clock's higher-specificity rule
   (below) always won for every property this base rule set; it was
   redundant, not merely convertible. Moved into _player.html.erb's own
   class="…" instead (.bt-meta .clock's declarations, the ones that actually
   rendered). The landscape media query's own `.clock{font-size:21px}`
   override further down this file (0,2,0, same specificity as .bt-meta
   .clock but declared earlier in the cascade) was ALREADY dead for the same
   reason before this edit — left untouched rather than "fixed", since
   changing it would be a visual-behavior change out of this task's scope. */
/* .clock-cap (Task 5): deleted — dead, no producer anywhere in the app. */
.brew-timeline .step-name{font-family:"Source Serif 4",serif;font-weight:700;font-size:clamp(20px,2.4vw,26px);margin:12px 0 4px}
/* 34ch broke "Weigh out the beans — fresh, not pre-ground." across two lines
   in a column with room for four times that. 56ch is still a measure — long
   prose does not run the full width of the card — but it stops the text
   wrapping mid-thought while the space beside it sits empty. */
.brew-timeline .step-text{color:var(--ink-soft);font-size:14.5px;line-height:1.55;max-width:56ch}
/* Task 4: this step's own param_notes, filled/cleared per step in
   brew_timeline_controller#activate — :empty hides the gap entirely on a
   step (or a whole recipe) that carries none. */
.brew-timeline .step-param-notes{margin-top:6px;display:flex;flex-direction:column;gap:2px;max-width:56ch}
.brew-timeline .step-param-notes p{margin:0;color:var(--ink-soft);font-size:12.5px;line-height:1.5}
.brew-timeline .step-param-notes:empty{display:none}
/* Task 1 of v2.1: global figure notes, under the figures line — same idiom
   as .step-param-notes just above (small, ink-soft, unobtrusive), a sibling
   of .bt-topbar rather than nested in it so it always spans full width
   under the grinder/facts/actions row regardless of how that row wraps.
   Fix wave (F2): used to rely on a `:empty{display:none}` rule that never
   actually matched — erubi emits the newline after this div's own opening
   tag as a text child, so the div was never truly :empty, and an 8px-margin
   empty box rendered on every player page site-wide. The container is now
   gated server-side instead (_player.html.erb omits the div outright when
   there's nothing to show); the studio's own :embed preview is the one
   exception (always gets the div, one <p hidden> per fact) and toggles
   `hidden` itself from the draft — see that partial's own comment. */
/* .bt-figure-notes(+p)/.bt-grinder-clicks(+label,+a) (Task 5): moved into
   _player.html.erb's own class="…" — bare .brew-timeline scope, zero
   studio.css overlap, no JS toggling beyond `hidden`. */
/* The reader's OWN grinder pick, when it matches one of the entries above —
   filled/toggled by brew_timeline_controller.js#updateGrinderClickHint,
   never server-rendered text (see _player.html.erb's own comment). */
.brew-timeline .bt-grinder-click-hint{font-size:11.5px;color:var(--accent-2)}
.brew-timeline .step-progress{height:4px;background:var(--surface2);border-radius:99px;margin-top:16px;overflow:hidden}
.brew-timeline .step-progress i{display:block;height:100%;width:0%;background:var(--accent);border-radius:99px}

/* ── The scale's readout, below its weighing surface ── */
/* No box of its own: the readout is the same instrument as the surface above
   it, so the plate runs to exactly the same edges. The dashed border and 8px
   padding it used to carry inset the LCD on both sides and framed it as a
   separate widget sitting underneath. */
.brew-timeline .bt-scale{align-self:start;border:0;padding:0;background:none}
/* Asleep, a real scale keeps its dark plate and lets the digits go out — it
   does not turn itself grey. Fading the whole element to 45% over a white card
   is exactly what made the plate read as a grey slab, so the opacity moves off
   the plate and onto what it displays. */
.brew-timeline .bt-scale-lcd{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;
  background:var(--lcd-bg);border-radius:0 0 6px 6px;padding:10px 8px 11px}
/* Palette round 2 (2026-08-04), dark only: --lcd-bg stays a literal always-
   dark plate on purpose (see the --lcd-bg comment above) even after the
   surrounding --surface-2 got lifted for the charcoal re-tune, so the two
   dark tones now sit only ~1.24x apart — legible digits, invisible plate
   edge, which read as "dark-on-dark" against the panel behind it. A border,
   not a paler fill: keeps the plate literally dark while giving it an edge
   to be seen by. No border-top — the plate is still one instrument with
   .stage-visual above it, seamed by colour in light mode and now by this
   line in dark. Light mode untouched. */
.dark .brew-timeline .bt-scale-lcd{border:1px dashed var(--border);border-top:0}
.brew-timeline .bt-scale .bt-scale-field,
.brew-timeline .bt-scale .bt-scale-ratio{transition:opacity .25s ease}
.brew-timeline .bt-scale.is-off .bt-scale-field,
.brew-timeline .bt-scale.is-off .bt-scale-ratio{opacity:.32}
.brew-timeline .bt-scale-field{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:0}
.brew-timeline .bt-scale-num{font:700 17px/1 "JetBrains Mono",monospace;font-variant-numeric:tabular-nums;
  color:var(--lcd-fg);white-space:nowrap}
.brew-timeline .bt-scale-lbl{font:600 8px/1 "JetBrains Mono",monospace;letter-spacing:.16em;
  text-transform:uppercase;color:var(--lcd-fg);opacity:.55}
/* spans the three fields as the plate's second line, lit like the rest of it */
.brew-timeline .bt-scale-ratio{grid-column:1 / -1;margin-top:7px;text-align:center;
  font:600 12px/1 "JetBrains Mono",monospace;font-variant-numeric:tabular-nums;
  color:var(--lcd-fg);opacity:.75}
@media (max-width:640px){
  .brew-timeline .bt-scale-num{font-size:15px}
}

/* ── Controls ── */
.brew-timeline .controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.brew-timeline .bt-btn{appearance:none;cursor:pointer;font:600 13.5px/1 "Inter";border-radius:999px;padding:11px 20px;transition:all .15s;border:1px dashed transparent;display:inline-flex;align-items:center;gap:7px}
.brew-timeline .bt-btn svg{width:14px;height:14px;flex:none}
.brew-timeline .bt-btn-primary{background:var(--ink);color:var(--bg)}
.brew-timeline .bt-btn-primary:hover{background:var(--accent-2)}
.brew-timeline .bt-btn-ghost{background:var(--surface);border-color:var(--border);color:var(--ink)}
.brew-timeline .bt-btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
/* .keys/.keys b (Task 5): deleted — dead, no producer anywhere in the app
   (the keyboard-shortcut legend the mockup this file was ported from once
   showed has no rendered equivalent here). */

/* ── SVG scene shared ── */
.brew-timeline .scene .ln{stroke:var(--ink);stroke-width:2.5;fill:none;stroke-linecap:round;stroke-linejoin:round}
.brew-timeline .scene .lbl-svg{font:600 10px/1 "JetBrains Mono",monospace;letter-spacing:.14em;text-transform:uppercase;fill:var(--ink-soft)}
.brew-timeline .scene .num-svg{font:700 26px/1 "JetBrains Mono",monospace;fill:var(--ink);font-variant-numeric:tabular-nums}
.brew-timeline .scene .cap-svg{font:500 11px/1 "Inter";fill:var(--ink-soft)}
.brew-timeline .scene:not(.is-active) .ambient{animation-play-state:paused}
.brew-timeline .spin{transform-box:fill-box;transform-origin:center;animation:bt-spin 1.1s linear infinite}
.brew-timeline .spin-slow{transform-box:fill-box;transform-origin:center;animation:bt-spin 5s linear infinite}
.brew-timeline .flow{stroke-dasharray:7 6;animation:bt-flowdash .45s linear infinite}
/* Circulation on a LIQUID SURFACE — a pour-over's spiral, a stirred cup, the
   whirlpool in a steaming pitcher. Each of those is drawn as a flat ellipse
   (rx≈35, ry≈8) because it is a circle seen from the viewer's angle, and each
   used .spin-slow: rotate(360deg).
   Rotating a circle-in-perspective does not spin it, it TIPS ITS PLANE. The
   silhouette sweeps through vertical, so a whirlpool lying flat in the milk
   read as a hoop tumbling end-over-end through the pitcher — caught mid-tumble
   it hangs diagonally in mid-air, attached to nothing.
   What actually moves in a whirlpool is the pattern ALONG the ring, not the
   ring. So the ellipse holds still in the surface plane and its dashes travel
   around it — the same idiom .flow above already uses for a moving stream.
   -22 is exactly two dash periods for the "5 6" these three carry, so the loop
   closes seamlessly instead of jumping at the restart. */
.brew-timeline .swirl{animation:bt-swirl 1.5s linear infinite}
/* A flame licks; it does not drift away. The moka pot's burner used .steam,
   which translates a wisp 16px UP and fades it — so the flames climbed off the
   hob and through the pot standing on it, reading as stray marks floating
   inside the boiler. This stretches from the base instead, so the flame stays
   rooted where the gas is and only its tip moves. */
.brew-timeline .flicker{transform-box:fill-box;transform-origin:bottom;
  animation:bt-flicker .5s ease-in-out infinite alternate}
.brew-timeline .grain{animation:bt-fall .75s linear infinite}
.brew-timeline .pulse{transform-box:fill-box;transform-origin:center;animation:bt-pulse 1.6s ease-out infinite}
.brew-timeline .settle{animation:bt-settle .5s ease-out}
.brew-timeline .steam{animation:bt-steam 2.2s ease-in-out infinite}
.brew-timeline .orbit{animation:bt-orbit 2.4s ease-in-out infinite}
/* Round 7 Task 2: add_soda's own rising bubbles — the same "rise and fade"
   shape as .steam above, just travelling further (a bubble climbs the whole
   glass, not just a wisp off a cup). */
.brew-timeline .rise{animation:bt-rise 1.6s ease-in infinite}
@keyframes bt-orbit{0%{transform:translate(0,-2px)}25%{transform:translate(8px,1px)}50%{transform:translate(0,4px)}75%{transform:translate(-8px,1px)}100%{transform:translate(0,-2px)}}
@keyframes bt-spin{to{transform:rotate(360deg)}}
@keyframes bt-swirl{to{stroke-dashoffset:-22}}
@keyframes bt-flicker{from{transform:scaleY(.8)}to{transform:scaleY(1.15)}}
@keyframes bt-flowdash{to{stroke-dashoffset:-24}}
@keyframes bt-fall{0%{transform:translateY(-4px);opacity:0}18%{opacity:1}88%{opacity:1}100%{transform:translateY(44px);opacity:0}}
@keyframes bt-pulse{0%{transform:scale(.92);opacity:.75}100%{transform:scale(1.18);opacity:0}}
@keyframes bt-settle{0%{transform:translateY(0)}35%{transform:translateY(1.6px)}70%{transform:translateY(-.6px)}100%{transform:translateY(0)}}
@keyframes bt-steam{0%{transform:translateY(5px);opacity:0}30%{opacity:.65}100%{transform:translateY(-16px);opacity:0}}
@keyframes bt-rise{0%{transform:translateY(6px);opacity:0}25%{opacity:.9}100%{transform:translateY(-30px);opacity:0}}

@media (prefers-reduced-motion:reduce){
  .brew-timeline .ambient,.brew-timeline .grain,.brew-timeline .flow,.brew-timeline .spin,
  .brew-timeline .spin-slow,.brew-timeline .orbit,.brew-timeline .pulse,.brew-timeline .settle,.brew-timeline .steam,
  .brew-timeline .rise,.brew-timeline .swirl,.brew-timeline .flicker{animation:none !important}
  .brew-timeline .scene{transition:none}
}

/* the ruler, legible again: each segment named and sized to its step's real
   duration, so you can see the shape of the brew and where you are in it. The
   8px version was tidier and told you nothing. Deliberately placed ahead of
   the Responsive section below: media queries add no specificity, so an
   equal-specificity override declared later in the file wins the cascade tie
   — above this point, this rule would always win instead, regardless of
   viewport, silently defeating every mobile override that touches it. */
/* The ruler, in two parts. The track is thin and exactly proportional — every
   step's real duration and nothing written on it — and the names sit below it
   as chips that wrap onto as many lines as they need. Keeping the two apart is
   what lets every name show in full: no clipping, no shortening, no sideways
   scrolling at any width. Both halves seek, and both show where you are. */
.brew-timeline .tl-strip{margin-bottom:12px}
/* The phase legend: what each part of the brew is and how long it runs. A
   swatch carries the phase's colour, matching its band on the track below. */
.brew-timeline .tl-phases{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 16px;margin-bottom:6px}
.brew-timeline .tl-phase{display:inline-flex;align-items:baseline;gap:5px;white-space:nowrap;
  font:600 9.5px/1.3 "JetBrains Mono",monospace;letter-spacing:.1em;text-transform:uppercase}
.brew-timeline .tl-phase::before{content:"";align-self:center;width:16px;height:6px;border-radius:2px;
  background:currentColor;opacity:.9}
.brew-timeline .tl-phase b{font-weight:600;opacity:.85}
.brew-timeline .tl-phase i{font-style:normal;font-variant-numeric:tabular-nums;opacity:.65}
.brew-timeline .tl-phase.g-prep{color:var(--ph-prep)}
.brew-timeline .tl-phase.g-brew{color:var(--ph-brew)}
.brew-timeline .tl-phase.g-milk{color:var(--ph-milk)}
.brew-timeline .tl-phase.g-serve{color:var(--ph-serve)}
/* The plot: each step is a block standing on the axis, exactly as wide as it
   lasts. Not a single filled bar — a bar says "this much of the brew is
   behind you", blocks say "these are the steps, and this is how long each of
   them takes", which is the thing a ruler is for. */
.brew-timeline .tl-track{position:relative;height:22px}
/* A transparent right border with background-clip:padding-box leaves a real
   gap between neighbouring blocks without moving either of them off the time
   it actually occupies — the alternative, insetting each block's own left and
   right, would make every block read a little shorter than it is. The fill
   below is positioned against the padding box, so it stops at the gap too. */
.brew-timeline .tl-strip .tl-seg{position:absolute;top:0;bottom:0;overflow:hidden;
  border:0;border-right:2px solid transparent;background-clip:padding-box;
  border-radius:2px;pointer-events:none;background-color:var(--surface2)}
/* one hue per phase, the same the legend and the chips use. The ring is what
   makes a block an object on the axis rather than a wash of colour. */
.brew-timeline .tl-strip .tl-seg.g-prep{background-color:color-mix(in oklab,var(--ph-prep) 26%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--ph-prep) 52%,transparent)}
.brew-timeline .tl-strip .tl-seg.g-brew{background-color:color-mix(in oklab,var(--ph-brew) 26%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--ph-brew) 55%,transparent)}
.brew-timeline .tl-strip .tl-seg.g-milk{background-color:color-mix(in oklab,var(--ph-milk) 26%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--ph-milk) 55%,transparent)}
.brew-timeline .tl-strip .tl-seg.g-serve{background-color:color-mix(in oklab,var(--ph-serve) 24%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--ph-serve) 50%,transparent)}
/* Quick minor (c): the missing fifth tone — an UNGROUPED step inside a
   recipe that HAS custom step groups (see the g-none .tl-hatch rule below
   for the full "why neutral, not a fifth hue" reasoning) had no fill/ring
   rule of its own at all, so it silently fell back to the bare .tl-seg
   rule above (a flat --surface2 wash, no ring) while its four siblings
   each got their own tinted fill AND ring — the one block on the axis
   that read as "nothing" rather than "an object", exactly backwards from
   what the ring's own comment says it's for. --ink-soft, not a --ph-*
   var: it's already themed correctly at every scope this file defines
   (light/dark/light-pin — see this file's own top comment), so this one
   rule inherits across all three for free, unlike the four hue rules
   above (which each need their own copy of --ph-* per scope). */
.brew-timeline .tl-strip .tl-seg.g-none{background-color:color-mix(in oklab,var(--ink-soft) 20%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--ink-soft) 44%,transparent)}
/* elapsed: the block fills in with its own hue at full strength */
.brew-timeline .tl-strip .tl-hatch{position:absolute;left:0;top:0;bottom:0;width:0;
  background:var(--ph-prep);opacity:.85}
.brew-timeline .tl-strip .tl-seg.g-brew .tl-hatch{background:var(--ph-brew);opacity:.95}
.brew-timeline .tl-strip .tl-seg.g-milk .tl-hatch{background:var(--ph-milk);opacity:.9}
.brew-timeline .tl-strip .tl-seg.g-serve .tl-hatch{background:var(--ph-serve);opacity:.9}
/* Round 4 Task 2: an UNGROUPED step inside a recipe that HAS custom step
   groups — brew_timeline_controller.js#segTone only ever emits "none" (never
   one of the four g-prep/g-brew/g-milk/g-serve tones) in that situation, so
   this rule is inert for any recipe with no custom groups. Neutral --ink-soft
   fill, not one of the four hues — "no phase", not a fifth undyed one; the
   numbered chip + name still identify it, same "colour groups, text
   identifies" idiom the file's own --ph-* comment already states. */
.brew-timeline .tl-strip .tl-seg.g-none .tl-hatch{background:var(--ink-soft);opacity:.7}
.brew-timeline .tl-strip .tl-seg.now .tl-hatch{background:var(--accent);opacity:.95}
.brew-timeline .tl-strip .tl-seg.g-prep.now,
.brew-timeline .tl-strip .tl-seg.g-brew.now,
.brew-timeline .tl-strip .tl-seg.g-milk.now,
.brew-timeline .tl-strip .tl-seg.g-serve.now,
.brew-timeline .tl-strip .tl-seg.g-none.now{box-shadow:inset 0 0 0 1.5px var(--accent)}
.brew-timeline .tl-strip .tl-cursor{top:-5px;bottom:-3px;width:2px;z-index:3}
/* the blocks are short, so the playhead needs a head to be findable */
.brew-timeline .tl-strip .tl-cursor::after{display:block;top:-5px;left:-3px;width:8px;height:8px}

/* The axis the blocks stand on: a hairline, ticked in real time. */
.brew-timeline .tl-axis{position:relative;height:20px;
  border-top:1px solid color-mix(in oklab,var(--ink-soft) 55%,transparent)}
.brew-timeline .tl-tick{position:absolute;top:0;width:1px;height:4px;
  background:color-mix(in oklab,var(--ink-soft) 40%,transparent)}
.brew-timeline .tl-tick.is-major{height:7px;background:var(--ink-soft)}
.brew-timeline .tl-tick i{position:absolute;top:9px;left:50%;transform:translateX(-50%);
  font:500 10px/1 "JetBrains Mono",monospace;font-variant-numeric:tabular-nums;
  color:var(--ink-soft);white-space:nowrap}
/* the ends would otherwise hang off their own edge of the ruler */
.brew-timeline .tl-tick[data-edge="start"] i{left:0;transform:none}
.brew-timeline .tl-tick[data-edge="end"] i{left:auto;right:0;transform:none}

/* Pointing at a name lights the slice of the brew it belongs to (see
   buildBar's `link`). The chips can't sit above their own segments — a
   5-second step owns 6% of the track — so the tie is shown on demand
   instead of implied by geometry that would clip every short name. */
.brew-timeline .tl-strip .tl-seg.is-linked,
.brew-timeline .tl-strip .tl-seg.g-prep.is-linked,
.brew-timeline .tl-strip .tl-seg.g-brew.is-linked,
.brew-timeline .tl-strip .tl-seg.g-milk.is-linked,
.brew-timeline .tl-strip .tl-seg.g-serve.is-linked{background-color:var(--accent-tint-42);
  box-shadow:inset 0 0 0 1px var(--accent)}

/* the names, wrapping — tight under the track they belong to */
.brew-timeline .tl-steps{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
/* Round 4 Task 2: an inline section label ahead of a custom step group's own
   chips, right inside the same wrapping row — flex-basis:100% forces it onto
   its own line without a second container. Plain text, matching "Behind
   every step"'s own .bt-notes-group header one section down; the ruler's
   ONE swatch-carrying legend already lives above this row (#tlPhases). Only
   ever appended when groupsValue is non-empty — see brew_timeline_
   controller.js#buildBar. */
.brew-timeline .tl-chip-group-hdr{flex-basis:100%;margin-top:4px;
  font:600 10px/1.4 "JetBrains Mono",monospace;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-soft)}
.brew-timeline .tl-chip-group-hdr:first-child{margin-top:0}
/* The expand switch, above the ruler on a drink. Flowbite's toggle structure —
   a visually-hidden checkbox driving a track and a knob through the sibling
   `peer` relationship — rebuilt on this project's own tokens: Flowbite ships
   bg-brand/bg-neutral-quaternary/text-heading, none of which exist here, and
   hardcoding its greys would strand the control in light mode while everything
   around it followed --surface/--ink.

   Deliberately NOT a chip and not a caret: a chip is a step you can seek to,
   and a caret is a section that opens and shuts. This is a persistent on/off
   the reader's browser remembers across drinks, so it takes the affordance
   people already read as exactly that. */
.brew-timeline .tl-base-switch{display:inline-flex;align-items:center;gap:9px;
  align-self:flex-end;margin:0 0 10px auto;cursor:pointer;user-select:none}
.brew-timeline .tl-base-switch[hidden]{display:none}
/* sr-only: off-screen but focusable, so the switch is reachable by keyboard and
   carries the checkbox's own semantics. display:none would take both away. */
.brew-timeline .tl-base-switch-in{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.brew-timeline .tl-base-switch-track{position:relative;flex:none;width:34px;height:20px;
  border-radius:999px;background:var(--surface2);border:1px solid var(--border);
  transition:background-color .18s ease,border-color .18s ease}
.brew-timeline .tl-base-switch-track::after{content:"";position:absolute;top:2px;left:2px;
  width:14px;height:14px;border-radius:50%;background:var(--bg);
  box-shadow:0 1px 2px rgb(0 0 0 / .25);transition:transform .18s ease}
.brew-timeline .tl-base-switch-in:checked + .tl-base-switch-track{background:var(--accent);border-color:var(--accent)}
.brew-timeline .tl-base-switch-in:checked + .tl-base-switch-track::after{transform:translateX(14px)}
.brew-timeline .tl-base-switch-in:focus-visible + .tl-base-switch-track{outline:2px solid var(--accent);outline-offset:2px}
.brew-timeline .tl-base-switch-lbl{font:500 12px/1 "Inter",sans-serif;color:var(--ink-soft);
  white-space:nowrap;transition:color .15s ease}
.brew-timeline .tl-base-switch:hover .tl-base-switch-lbl{color:var(--ink)}
.brew-timeline .tl-base-switch-in:checked ~ .tl-base-switch-lbl{color:var(--ink)}
@media (prefers-reduced-motion:reduce){
  .brew-timeline .tl-base-switch-track,
  .brew-timeline .tl-base-switch-track::after{transition:none}
}
/* Phone portrait: the label and the strip each get their own full row anyway,
   so keep the switch left-aligned with everything else rather than orphaned in
   a corner above a full-width bar. */
@media (max-width:640px){
  .brew-timeline .tl-base-switch{margin-left:0;align-self:flex-start}
}

.brew-timeline .tl-chip{display:inline-flex;align-items:center;gap:6px;min-height:26px;
  padding:4px 10px;border-radius:999px;border:1px dashed var(--border);background:var(--surface);
  color:var(--ink-soft);cursor:pointer;font:inherit;transition:border-color .15s ease,color .15s ease}
.brew-timeline .tl-chip:hover{border-color:var(--accent);color:var(--ink)}
.brew-timeline .tl-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.brew-timeline .tl-chip-n{font:600 9px/1 "JetBrains Mono",monospace;opacity:.6}
.brew-timeline .tl-chip-lbl{font:500 12px/1 "Inter",sans-serif;white-space:nowrap}
.brew-timeline .tl-chip.done{background:var(--surface2);color:var(--ink)}
.brew-timeline .tl-chip.done .tl-chip-n{color:var(--accent);opacity:1}
.brew-timeline .tl-chip.now,
.brew-timeline .tl-chip.g-prep.now,
.brew-timeline .tl-chip.g-brew.now,
.brew-timeline .tl-chip.g-milk.now,
.brew-timeline .tl-chip.g-serve.now{border:1.5px solid var(--accent);color:var(--accent-2);
  box-shadow:0 0 0 3px var(--accent-tint-12)}
.brew-timeline .tl-chip.now .tl-chip-lbl{font-weight:700}
.brew-timeline .tl-chip.now .tl-chip-n{color:var(--accent-2);opacity:1}

/* ── The step list reads like a carousel: one step lit, the rest receding ───
 *
 * The chips already marked the running step with an accent border, but every
 * other chip stayed at full strength, so the row read as nine equal things
 * with one outlined. Dimming the rest is what makes the current step findable
 * at a glance — the eye lands on the only thing at full contrast instead of
 * reading nine labels to find the ringed one.
 *
 * opacity AND scale, not colour: the chips are already tinted per phase
 * (g-prep/g-brew/g-serve), and dimming by colour would fight that encoding
 * and flatten the phases into one grey. Opacity keeps the hue and drops the
 * presence.
 *
 * `.done` stays readable at .62 rather than .38 — a step you have finished is
 * still something you may want to scrub back to, and it carries the accent
 * number that shows progress through the brew.
 */
.brew-timeline .tl-chip{opacity:.38;transform:scale(.96);transform-origin:center;
  transition:border-color .15s ease,color .15s ease,opacity .18s ease,transform .18s ease}
.brew-timeline .tl-chip.done{opacity:.62}
.brew-timeline .tl-chip.now{opacity:1;transform:scale(1.04)}
.brew-timeline .tl-chip:hover,
.brew-timeline .tl-chip:focus-visible{opacity:1}

/* A dimmed control still has to be operable and legible to anyone who needs
   contrast; forced-colors and reduced-transparency both mean "do not convey
   state with a wash". */
@media (prefers-reduced-transparency:reduce),(forced-colors:active){
  .brew-timeline .tl-chip,
  .brew-timeline .tl-chip.done{opacity:1}
}
@media (prefers-reduced-motion:reduce){
  .brew-timeline .tl-chip{transition:none;transform:none}
  .brew-timeline .tl-chip.now{transform:none}
}

/* Phases. A brew reads as setup → the brew itself → serving, and the steps
   inside one phase belong together: a V60's bloom and its pours are a single
   drip, which is what the recipe's 1:16 actually describes. One hue per phase,
   on both the track and the names, so that grouping is visible at a glance
   rather than inferred from reading eleven labels. */
/* A chip wears its phase's own colour — the same tint as that phase's band on
   the track above, so the two read as one thing rather than two colour schemes.
   Done deepens the same hue; it never changes it. */
.brew-timeline .tl-chip .tl-chip-n{opacity:1}
.brew-timeline .tl-chip.g-prep{background:color-mix(in oklab,var(--ph-prep) 11%,transparent);
  border:1px solid color-mix(in oklab,var(--ph-prep) 38%,transparent);color:var(--ink)}
.brew-timeline .tl-chip.g-prep .tl-chip-n{color:var(--ph-prep)}
.brew-timeline .tl-chip.g-prep.done{background:color-mix(in oklab,var(--ph-prep) 26%,transparent)}
.brew-timeline .tl-chip.g-brew{background:color-mix(in oklab,var(--ph-brew) 11%,transparent);
  border:1px solid color-mix(in oklab,var(--ph-brew) 40%,transparent);color:var(--ink)}
.brew-timeline .tl-chip.g-brew .tl-chip-n{color:var(--ph-brew)}
.brew-timeline .tl-chip.g-brew.done{background:color-mix(in oklab,var(--ph-brew) 26%,transparent)}
.brew-timeline .tl-chip.g-milk{background:color-mix(in oklab,var(--ph-milk) 11%,transparent);
  border:1px solid color-mix(in oklab,var(--ph-milk) 40%,transparent);color:var(--ink)}
.brew-timeline .tl-chip.g-milk .tl-chip-n{color:var(--ph-milk)}
.brew-timeline .tl-chip.g-milk.done{background:color-mix(in oklab,var(--ph-milk) 26%,transparent)}
.brew-timeline .tl-chip.g-serve{background:color-mix(in oklab,var(--ph-serve) 10%,transparent);
  border:1px solid color-mix(in oklab,var(--ph-serve) 38%,transparent);color:var(--ink)}
.brew-timeline .tl-chip.g-serve .tl-chip-n{color:var(--ph-serve)}
.brew-timeline .tl-chip.g-serve.done{background:color-mix(in oklab,var(--ph-serve) 24%,transparent)}
/* The blocks' own phase colours live with the plot they belong to (see
   .tl-strip .tl-seg above). A second copy here — same specificity, later in
   the file, and using the `background` shorthand — silently reset the
   background-clip the gap between blocks depends on. */
/* the blocks stand apart on the axis now, so a phase hand-over needs no rule
   of its own — the legend above and the colour change carry it */

/* ── Responsive ── */
@media (max-width:760px){
  /* One column, apparatus first: the animation is the point of the card, and
     with the controls first it landed a full screen below the fold. */
  /* One column: the explicit two-row placement the desktop layout uses has to
     be handed back, or every item would be assigned to the same two cells. */
  .brew-timeline .stage{grid-template-columns:1fr;grid-template-rows:none}
  /* .stage-visual used to be the grid's first child directly; now it's wrapped
     in .stage-surface (so the strip travels with it), which is the actual
     grid child that needs to lead on phones. */
  .brew-timeline .stage-surface{order:-1;grid-column:auto;grid-row:auto;
    display:block;max-width:none}
  .brew-timeline .stage-side{grid-column:auto;grid-row:auto}
  .brew-timeline .controls{grid-column:auto;grid-row:auto;margin-top:14px}
  /* .stage-surface is a plain block here, so the row gap that separated the
     readout from the surface on desktop no longer applies. */
  .brew-timeline .bt-scale{margin-top:0}
  /* The bar is ~330px wide here and eleven names need far more than that, so
     the strip scrolls rather than hiding or shortening any of them. Segments
     tighten up a little; the 44px height keeps each one past the 24px tap
     minimum (WCAG 2.5.8) on its own. */
  .brew-timeline .tl-steps{gap:5px;margin-top:9px}
  .brew-timeline .tl-chip{padding:4px 8px;gap:5px}
  .brew-timeline .tl-chip-lbl{font-size:11.5px}
}

/* Phone portrait. The bar is ~330px wide here, so a 5-second step is a 20px
   segment — anything drawn inside one is unreadable, and the chrome around it
   has to stop competing for the same row. */
@media (max-width:640px){
  .brew-timeline .card-head{flex-direction:column;align-items:flex-start;gap:2px;margin-bottom:10px}
  .brew-timeline .card-head h2{font-size:16px}
  .brew-timeline .card-head .hint{font-size:10.5px;line-height:1.45}
  .brew-timeline .tl-bar{height:44px;margin-top:18px}
  .brew-timeline .tl-ticks span{font-size:9.5px}
  /* The cursor's time chip is centred on the playhead, so at 0:00 half of it
     sits outside the card — and the same figure is already in the clock right
     below. Drop it and keep the playhead line. */
  .brew-timeline .tl-cursor .tag{display:none}
  /* The grinder picker gets the full row — in a half-width tile the
     placeholder is cut mid-word. */
  .brew-timeline .bt-tiles{grid-template-columns:repeat(auto-fit,minmax(108px,1fr))}
  .brew-timeline .bt-tiles .tile:has(> [data-controller="combobox"]),
  .brew-timeline .bt-tiles .tile:last-child:nth-child(odd){grid-column:1/-1}
  .brew-timeline .grind-sel{max-width:none}
  .brew-timeline .stage{gap:14px}
  .brew-timeline .controls{gap:8px}
  .brew-timeline .bt-btn{padding:9px 16px;font-size:12.5px}
}

/* Compact landscape (horizontal phone): shrink everything so the whole
   step-by-step card fits in one short viewport. */
@media (max-height:540px) and (orientation:landscape){
  .brew-timeline.card{padding:8px 14px 10px}
  .brew-timeline .bt-tiles{display:flex;gap:6px;margin-bottom:6px;overflow-x:auto}
  .brew-timeline .tile{padding:5px 9px;flex:none;min-width:0}
  .brew-timeline .tile .lbl{font-size:8px}
  .brew-timeline .tile .val{font-size:13px;margin-top:2px;white-space:nowrap}
  .brew-timeline .tile .clk{display:none}
  .brew-timeline .grind-sel{margin-top:2px;padding:3px 6px;font-size:11px}
  .brew-timeline .card-head{margin-bottom:2px}
  .brew-timeline .card-head h2{font-size:13px}
  .brew-timeline .card-head .hint{display:none}
  /* The ruler is .tl-strip (#tlBar in the DOM) — .tl-bar and .tl-ticks are
     mockup-era selectors with no matching element in the app, so these two
     rules never fired; retargeted onto the real track + its dropped
     margin-bottom (there is no separate ticks row to compact — the strip's
     own labels carry that job, sized down just below). */
  .brew-timeline .tl-strip{margin-bottom:6px}
  /* this query has no width bound, and this is one of the pair the review
     flagged — landscape phones commonly exceed 760px wide (e.g. 844×390),
     so the ≤760px query below doesn't fire and this one still has to win
     the shrink-and-reposition on its own. Scoped to match the base
     .tl-strip rules' specificity so it does. */
  /* A 390px-tall landscape phone has no vertical room for a second row of
     names, and pushing the Play controls below the fold is worse than a
     sideways nudge — so here alone the names stay on one line and scroll.
     Still every name in full: scrolled is not clipped, and the running step
     is scrolled into view for you (see keepChipInView). */
  .brew-timeline .tl-track{height:6px}
  .brew-timeline .tl-phases{gap:2px 10px;margin-bottom:3px}
  .brew-timeline .tl-phase{font-size:8.5px;gap:4px}
  .brew-timeline .tl-phase::before{width:10px;height:4px}
  .brew-timeline .tl-strip{margin-bottom:4px}
  .brew-timeline .tl-steps{flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;
    gap:4px;margin-top:5px;scrollbar-width:none}
  .brew-timeline .tl-steps::-webkit-scrollbar{display:none}
  .brew-timeline .tl-chip{flex:0 0 auto;min-height:24px;padding:3px 7px;gap:4px}
  .brew-timeline .tl-chip-lbl{font-size:11px}
  /* Same as portrait: the chip lands on the heading once the bar is this
     close to it, and the clock below already reads the time. */
  .brew-timeline .tl-cursor .tag{display:none}
  .brew-timeline .stage{margin-top:4px;padding-top:4px;gap:16px;grid-template-columns:minmax(150px,.8fr) 2fr}
  .brew-timeline .stage-side{order:0}
  .brew-timeline .stage-visual{max-height:150px}
  /* The scale strip stacks under stage-visual in the same narrow column, so
     its full size pushes the row (and everything below it) taller than the
     540px this query targets — the Play controls landed below the fold at
     844×390 because of it. */
  .brew-timeline .bt-scale{margin-top:4px}
  .brew-timeline .bt-scale-lcd{padding:4px 5px;gap:1px}
  .brew-timeline .bt-scale-num{font-size:12px}
  .brew-timeline .bt-scale-lbl{font-size:7px}
  .brew-timeline .bt-scale-ratio{margin-top:3px;font-size:10px}
  .brew-timeline .step-kicker{font-size:9px}
  /* .clock{font-size:21px;margin:2px 0 0} (Task 5's own comment above,
     "already dead"): true only while .bt-meta .clock still existed as a
     real CSS rule of equal (0,2,0) specificity declared later in this
     file — it always won, so this rule never rendered. Task 6 moved
     .bt-meta .clock's declarations (font:700 15px/1 …, margin:0) into
     _player.html.erb's own utilities instead (text-[15px]/[1] … m-0) —
     and this sheet is unlayered, so an unlayered rule now beats that
     utility outright regardless of source order. This rule quietly came
     back to life at exactly the landscape breakpoint it was written for,
     rendering the clock 21px/margin-top:2px instead of the 15px/margin:0
     every other breakpoint (and this one, before Task 6) has always
     shown. Deleted rather than fixed to 15px/0 — a no-op rule that
     duplicates the markup's own utilities for no reason is worse than no
     rule at all (see the file-level gotcha on deleting vs. fighting an
     unlayered rule). */
  .brew-timeline .step-name{font-size:16px;margin:4px 0 1px}
  .brew-timeline .step-text{font-size:11.5px;max-width:none}
  .brew-timeline .step-progress{margin-top:6px}
  .brew-timeline .controls{margin-top:4px;gap:6px}
  .brew-timeline .bt-btn{padding:6px 12px;font-size:11px;gap:5px}
  .brew-timeline .bt-btn svg{width:12px;height:12px}
}

/* ── Player chrome ──────────────────────────────────────────────────────────
   The panel reads as one instrument deck: the grinder and what it resolves to,
   the figures you turn, the ones that follow, then the window actions. Its
   height is fixed whether or not anything is edited — see .bt-dirty. */
.brew-timeline .bt-topbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:2px}
.brew-timeline .bt-grinder{display:flex;align-items:center;gap:10px;flex:1 1 320px;min-width:0}
.brew-timeline .bt-grinder-box{flex:1 1 auto;min-width:0;max-width:19rem}
/* On a phone the picker takes the row to itself: its dropdown is only as wide
   as the input, and "Saint Anthony Industries Millwright" needs every pixel. */
@media (max-width:640px){
  .brew-timeline .bt-grinder{flex:1 1 100%;order:1}
  .brew-timeline .bt-grinder-box{max-width:none}
}
/* .bt-topbar-lbl(+400px hide)/.bt-actions(+640px reorder)/.bt-meta(+.clock,
   see above)/.bt-more/.bt-more-panel(+button,+hover) (Task 5): moved into
   _player.html.erb's own class="…". Zero studio.css overlap (unlike
   .bt-grinder/.bt-grinder-box just above, which studio.css DOES reference
   for its own header row — left as CSS, not touched here).

   .bt-icon-btn stays KEEP, despite zero studio.css overlap: this file is
   entirely unlayered (no @layer wrapper), and per the CSS Cascade Layers
   spec, an unlayered rule ALWAYS beats a Tailwind utility class (which
   lives in @layer utilities) for a conflicting property, regardless of
   specificity or source order. The 640px "Phone portrait" media query
   above (`.bt-btn{padding:9px 16px;font-size:12.5px}`, further up this
   file) targets EVERY .bt-btn, including the three buttons that are also
   .bt-icon-btn — before this task, .bt-icon-btn's own unconditional
   `padding:9px 11px` (same specificity, later in source) was the tie-
   breaker keeping that mobile override from ever applying to icon buttons.
   Converting .bt-icon-btn to a utility class removes that tie-breaker: the
   mobile .bt-btn rule, still unlayered, then wins unconditionally at
   <=640px regardless of what utility class replaces it (touch-target size
   regression, caught by styles_snapshot.mjs: .bt-icon-btn/.bt-btn-ghost
   width 38px -> 48px at 390x844). Fixing this correctly means touching
   .bt-btn's own mobile rule too, which reaches buttons beyond .bt-icon-btn
   — left as CSS rather than grow this slice's blast radius. Flagged for
   Task 6: studio.css is equally unlayered and far more tangled: the same
   "deleting rule A silently un-blocks rule B, both unlayered, no @layer
   involved at all" failure mode is a live risk there, not just the
   already-known .font-display-is-unlayered trap from Task 4. */
.brew-timeline .bt-icon-btn{padding:9px 11px}

/* ── The instruments ────────────────────────────────────────────────────────
   A cell is a thing you turn: bordered, with a stepper and an accent it takes
   on once you have moved it. A readout is a thing that follows: no border, no
   stepper, so the difference is legible without a second column of chrome. */
.bt-facts{display:flex;flex-wrap:wrap;align-items:stretch;gap:8px;margin:0;padding:0}
/* The chevron the mobile grinder picker wears. Inline data URI so the page
   still makes zero external requests; drawn in black and recoloured by the
   mask that uses it, never by a fill in the file. */
.brew-timeline{--bt-chevron-down:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5 12 15.5 18 9.5'/%3E%3C/svg%3E")}
.bt-fact-label{font:600 9px/1 "JetBrains Mono",monospace;letter-spacing:.16em;
  text-transform:uppercase;color:var(--color-ink-soft)}
.brew-timeline .bt-cell{position:relative;display:flex;flex-direction:column;gap:3px;
  padding:6px 9px 7px;border:1px dashed var(--color-border);border-radius:7px;
  background:var(--color-surface);min-width:100px;
  transition:border-color .12s ease, background-color .12s ease}
.brew-timeline .bt-cell-field{display:flex;align-items:baseline;gap:3px;padding-right:26px}
.brew-timeline .bt-cell-pre{font:600 13px/1 "JetBrains Mono",monospace;color:var(--color-ink-soft)}
.brew-timeline .bt-cell-input{width:4.5ch;padding:0;border:0;background:none;color:var(--color-ink);
  font:600 20px/1.15 "JetBrains Mono",monospace;font-variant-numeric:tabular-nums;
  -moz-appearance:textfield}
/* A 21px-tall cell is a fine MOUSE target and half a thumb. Only on coarse
   pointers, because the desktop grid's proportions are deliberate and this
   is a touch problem, not a layout one. 2rem clears WCAG 2.2's 24px with
   room, without the row growing the way a full 44px would force. */
@media (pointer: coarse) {
  .brew-timeline .bt-cell-input { min-height: 2rem }
}

.brew-timeline .bt-cell-input::-webkit-outer-spin-button,
.brew-timeline .bt-cell-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.brew-timeline .bt-cell-input:focus{outline:none}
.brew-timeline .bt-cell:focus-within{border-style:solid;border-color:var(--color-accent)}
.brew-timeline .bt-fact-unit{font:600 11px/1 "JetBrains Mono",monospace;color:var(--color-ink-soft)}

/* Two 24x24 targets (WCAG 2.5.8) stacked at the cell's edge — the whole point
   of the redesign: a figure you can move without typing. */
.brew-timeline .bt-spin{position:absolute;right:4px;top:50%;transform:translateY(-40%);
  display:flex;flex-direction:column}
.brew-timeline .bt-spin button{width:24px;height:24px;display:grid;place-items:center;border:0;
  background:none;color:var(--color-ink-soft);cursor:pointer;padding:0;border-radius:4px;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.brew-timeline .bt-spin button:hover{color:var(--color-accent);background:var(--color-accent-tint-10)}
.brew-timeline .bt-spin button:active{background:var(--color-accent-tint-20)}
.brew-timeline .bt-spin svg{width:11px;height:11px;stroke-width:3}

/* Edited: the cell itself says so. The chip that used to say it in words is
   gone — it named the fact of an edit without naming which figure moved, and
   it pushed the save row onto a new line as it appeared. */
.brew-timeline .bt-cell.is-edited{border-style:solid;border-color:var(--color-accent);
  background:var(--color-accent-tint-06)}
.brew-timeline .bt-cell.is-edited .bt-fact-label{color:var(--color-accent)}
.brew-timeline .bt-cell.is-edited .bt-cell-input,
.brew-timeline .bt-cell.is-edited .bt-cell-pre,
.brew-timeline .bt-cell.is-edited .bt-fact-unit{color:var(--color-accent-2)}

.brew-timeline .bt-read{display:flex;flex-direction:column;gap:3px;padding:6px 2px 7px;
  justify-content:center;min-width:0}
.brew-timeline .bt-read-val{font:600 13px/1.25 "JetBrains Mono",monospace;
  font-variant-numeric:tabular-nums;color:var(--color-ink)}
.brew-timeline .bt-read-sub{font:500 11px/1.2 "JetBrains Mono",monospace;color:var(--color-ink-soft)}
.brew-timeline .bt-read-sub:empty{display:none}

/* Task 4: the bean card reuses .bt-facts/.bt-read (see _bean_card.html.erb)
   but its values are prose — a bean name, a roaster — not the tabular
   figures the mono type was built for; a plain readable face and a wrap
   suit them better than the player's own chips. */
.brew-timeline .bean-facts .bt-read{padding:8px 12px;border:1px dashed var(--color-border);
  border-radius:7px;background:var(--color-surface)}
.brew-timeline .bean-facts .bt-read-val{font-family:"Inter",sans-serif;font-variant-numeric:normal;
  white-space:normal}

@media (max-width:640px){
  .bt-facts{display:grid;grid-template-columns:1fr 1fr;order:3;width:100%;gap:6px}
  .brew-timeline .bt-cell{min-width:0}
  .brew-timeline .bt-cell-input{font-size:18px}

  /* ── The grinder picker reads as a DROP-DOWN on a phone ──────────────────
     It is a combobox either way, but the affordance was wrong: a bordered
     box with a magnifier promises a search you are about to start, and on
     this card the field is already filled in with the grinder you are on.
     A name with a dashed underline and a chevron says the opposite — this
     is set, and you may change it — and dashed-means-editable is the same
     language the figure cells beside it already speak.
     Styling only, scoped to this one mount: shared/ui/_combobox is used by
     the grinder guides and the studio too, and those ARE searches. */
  .brew-timeline .bt-grinder-box > div{position:relative}
  .brew-timeline .bt-grinder-box svg{display:none}
  .brew-timeline .bt-grinder-box input[type="search"]{
    appearance:none;-webkit-appearance:none;border:0;border-radius:0;
    border-bottom:1px dashed var(--color-border);background:none;box-shadow:none;
    padding:3px 20px 3px 0;font-weight:600}
  .brew-timeline .bt-grinder-box input[type="search"]:focus{
    outline:none;border-bottom-color:var(--color-accent)}
  /* A mask, not a background image: the chevron takes its colour from the
     rule, so dark mode repaints it with no second asset and no JS — the
     same reason the atlas draws its map through CSS tokens. */
  .brew-timeline .bt-grinder-box > div::after{
    content:"";position:absolute;right:2px;top:50%;transform:translateY(-50%);
    width:13px;height:13px;pointer-events:none;background:var(--color-ink-soft);
    -webkit-mask:var(--bt-chevron-down) center/contain no-repeat;
    mask:var(--bt-chevron-down) center/contain no-repeat}

  /* ── The picker and what it resolves to are ONE control ──────────────────
     "GRIND Fine · 200–300 µm" was its own row under the box. It is not a
     figure of its own — it is what the thing above it resolves to, which is
     why the caption had to say GRIND at all. Inside the box the caption is
     redundant and the relationship is the layout: grinder on the first
     line, what it grinds to on the second, one dashed underline around
     both.
     The underline moves from the input to the group for exactly that
     reason: a rule between the two lines would separate the pair the move
     exists to join. */
  .brew-timeline .bt-grinder{border-bottom:1px dashed var(--color-border);
    padding-bottom:4px;gap:1px}
  .brew-timeline .bt-grinder:focus-within{border-bottom-color:var(--color-accent)}
  .brew-timeline .bt-grinder-box input[type="search"]{border-bottom:0;padding-bottom:0}
  /* Chrome and Safari draw their own clear "✕" inside a type=search once it
     has text, and it lands right on top of the chevron — two controls in one
     corner, one of which empties a box the reader is meant to read. This is
     dressed as a menu, so the menu's own affordance is the only one that
     should be there. */
  .brew-timeline .bt-grinder-box input[type="search"]::-webkit-search-cancel-button{
    -webkit-appearance:none;appearance:none;display:none}
  .brew-timeline .bt-grinder > .bt-read{justify-content:flex-start;padding:0}
  /* The caption goes; the line's own place under the grinder says what it
     is. The values step down a size so the grinder stays the headline. */
  .brew-timeline .bt-grinder > .bt-read .bt-fact-label{display:none}
  .brew-timeline .bt-grinder > .bt-read .bt-read-val{font-size:12px;font-weight:600}
  .brew-timeline .bt-grinder > .bt-read .bt-read-sub{font-size:11px}

  /* ── Two things a phone does not need ────────────────────────────────────
     THE NUDGE BUTTONS. Two 24px targets per figure is 48px of chrome in a
     ~79px cell, and the thing they compete with is a number input that
     opens a keypad on tap. On a desktop they are the fast way to walk a
     dose; on a phone typing is, and the arrows were costing a whole line
     per cell. Hidden, not shrunk — a 20px target is a WCAG 2.5.8 failure,
     which is the trade I made once already and had to undo.
     The cell reserves a 26px lane for them at the base size; with the
     buttons gone that lane is just dead space on the right of every value. */
  .brew-timeline .bt-spin{display:none}
  .brew-timeline .bt-cell-field{padding-right:0}

  /* THE RESET / SAVE ROW. Its height is reserved whether or not anything is
     edited (see .bt-dirty's own rule) so an edit never shifts the page — a
     fixed 30px of blank on a screen that has none to spare. display:none,
     not visibility, precisely so the space goes too. The figures stay
     editable; what a phone loses is the way back and the save, both of
     which are still there on a wider screen. */
  .brew-timeline .bt-dirty{display:none}

  /* THE TOTAL-TIME ROW, whole. The number is on screen twice — the step
     line below reads "0:00 / 1:20" while it runs, and the timeline axis is
     drawn to the same total. The window actions were the row's other half;
     they move down to that step line instead (.bt-actions-alt, rendered
     from the same content_for in _player.html.erb) rather than disappear
     with it. */
  .brew-timeline .bt-facts > .bt-read[data-fact="time"]{display:none}
  .brew-timeline .bt-topbar > .bt-actions{display:none}

  /* And the step line goes last, so the card ends on the two things you
     touch while brewing: where you are, and the way into fullscreen. */
  .brew-timeline .bt-meta{order:9;margin-bottom:0}
}

/* ── The way back, and the way to keep it ───────────────────────────────────
   Reserved height at rest, contents revealed once the draft differs from ours,
   so an edit never moves anything else on the page. */
/* Task 5: .bt-dirty's own box (flex/gap/min-height/margin) and every child's
   own look (.bt-dirty-reset/-save/-saved/-hint, incl. their :hover states
   and the reset icon/saved-link descendants) moved into _player.html.erb's
   class="…". KEEP: the state-driven visibility rule just below — a bare
   `[data-state="rest"] > *` selector reaching every current AND future
   child generically is deliberate specificity a Tailwind group-data variant
   would have to replicate per child with no CSS reduction, for a JS-toggled
   attribute (recipe-tuner#reset/#save flip data-state) — not worth the risk
   for this task. */
.brew-timeline .bt-dirty[data-state="rest"] > *{visibility:hidden}

/* ── The method's own spec, above the panel ─────────────────────────────────
   One line of what we published, so an edited panel always has something to
   disagree with, and the handoff into it. */
/* Directly under the Overview heading, above the map: it reads as the
   method's own spec caption — this is the brew you are about to look at —
   rather than a footnote discovered after the steps. */
.bt-specline{display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px;
  border-bottom:1px dashed var(--color-border);padding-bottom:12px;margin:0 0 18px}
.bt-specline-tag{font:600 9px/1 "JetBrains Mono",monospace;letter-spacing:.18em;text-transform:uppercase;
  color:var(--color-ink-soft);border:1px dashed var(--color-border);border-radius:4px;padding:4px 7px}
.bt-specline-figs{font:500 13px/1.5 "JetBrains Mono",monospace;color:var(--color-ink);
  font-variant-numeric:tabular-nums;flex:1 1 auto;min-width:0}
.bt-specline-figs i{font-style:normal;color:var(--color-border);padding:0 6px}
.bt-specline-cta{font:600 12.5px/1 "Inter",sans-serif;color:var(--color-ink);background:none;
  border:1px dashed var(--color-ink);border-radius:6px;padding:9px 14px;cursor:pointer;
  display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
.bt-specline-cta:hover{border-style:solid;border-color:var(--color-accent);color:var(--color-accent)}
.bt-specline-cta svg{width:14px;height:14px}
@media (max-width:640px){
  .bt-specline-cta{width:100%;justify-content:center}
}
/* ── Embed widget: fills the host's 16:9 frame, never resizes it ── */
.bt-embed{position:relative;height:100vh;display:flex;flex-direction:column;justify-content:center;
  padding:14px 16px;overflow:hidden}
.bt-embed .brew-timeline.card{margin:0;border:0;box-shadow:none;background-image:none;padding:0}
.bt-embed .stage-visual{max-height:52vh}
.bt-embed .step-text{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.bt-embed-brand{position:absolute;right:12px;bottom:10px;font:600 10px/1 "JetBrains Mono",monospace;
  letter-spacing:.14em;text-transform:uppercase;color:var(--color-ink-soft);text-decoration:none}
.bt-embed-brand:hover{color:var(--color-accent)}

.brew-timeline .bt-share{position:relative}
.brew-timeline .bt-share-panel{position:absolute;right:0;top:calc(100% + 6px);z-index:20;min-width:190px;
  display:flex;flex-direction:column;gap:2px;background:var(--surface);border:1px dashed var(--border);
  border-radius:6px;box-shadow:var(--bt-shadow);padding:4px}
.brew-timeline .bt-share-panel button{appearance:none;background:none;border:0;text-align:left;cursor:pointer;
  padding:8px 10px;border-radius:4px;font:500 13px "Inter";color:var(--ink)}
.brew-timeline .bt-share-panel button:hover{background:var(--surface2)}
.brew-timeline .bt-share-fallback{width:calc(100% - 20px);margin:2px 10px;padding:6px 8px;
  font:500 12px "JetBrains Mono",monospace;color:var(--ink);background:var(--surface2);
  border:1px dashed var(--border);border-radius:4px}
.brew-timeline .bt-share-feedback{padding:2px 10px 6px;font:600 11px "JetBrains Mono",monospace;color:var(--accent);min-height:14px}
/* the player owns the whole screen in fullscreen, so give it room */
.brew-timeline:fullscreen{padding:4vh 6vw;display:flex;flex-direction:column;justify-content:center;background:var(--bg)}
.brew-timeline:fullscreen .stage-visual{max-height:62vh}

/* Which brew is this? On the page the heading above the card answers that;
   in fullscreen the card IS the page, so it has to answer for itself. Two
   rules, not one selector list: :-webkit-full-screen is unknown to Firefox
   and would invalidate the whole rule for it. */
.brew-timeline .bt-fs-title{display:none;margin:0}
.brew-timeline:fullscreen .bt-fs-title{display:block;
  font:600 clamp(17px,1.9vw,24px)/1.2 "Source Serif 4",serif;color:var(--ink);
  letter-spacing:-.01em;margin:0 0 14px}
.brew-timeline:-webkit-full-screen .bt-fs-title{display:block;
  font:600 clamp(17px,1.9vw,24px)/1.2 "Source Serif 4",serif;color:var(--ink);
  letter-spacing:-.01em;margin:0 0 14px}

/* ── Fullscreen where there IS no Fullscreen API ─────────────────────────────
   iOS Safari implements requestFullscreen on <video> and nothing else, so on
   an iPhone `this.element.requestFullscreen?.()` was an optional-chain into
   undefined: the button did nothing at all, silently, which is the whole
   reported bug. .is-fs is the same treatment done by hand — fixed to the
   viewport, over everything, scrolling inside itself — toggled by
   brew_timeline_controller#toggleFullscreen when the real API is missing.
   Its own rules, not a selector list with :fullscreen: same reason the two
   title rules above are written out twice. */
.brew-timeline.is-fs{position:fixed;inset:0;z-index:80;overflow-y:auto;
  padding:4vh 6vw;display:flex;flex-direction:column;justify-content:center;
  background:var(--bg);border-radius:0;margin:0;max-width:none}
.brew-timeline.is-fs .bt-fs-title{display:block;
  font:600 clamp(17px,1.9vw,24px)/1.2 "Source Serif 4",serif;color:var(--ink);
  letter-spacing:-.01em;margin:0 0 14px}
/* The page behind must not scroll under it — on iOS a fixed overlay does not
   stop the document, it just floats over one that is still moving. */
html.bt-fs-lock, html.bt-fs-lock body{overflow:hidden}

/* Guide me is a peer of Play, not a lesser action — same weight,
   different fill so the pair reads as a choice. */
.brew-timeline .bt-btn-alt{background:var(--surface);color:var(--ink);border:1px solid var(--ink)}
.brew-timeline .bt-btn-alt:hover{background:var(--surface2)}

/* .bt-note-secs (Task 5): moved into _step_note_row.html.erb's class="…". */


/* ── The homepage sample: one frame, not two ─────────────────────────────
   home/index.html.erb shows the flow map and the player together as a single
   block — "this is what a guide gives you" — and two framed cards stacked
   inside one container read as two separate offers rather than one.

   A named class rather than a Tailwind arbitrary variant: this has to
   out-specify `.brew-timeline.card` above, and a descendant selector says so
   plainly where an escaped `[&_.brew-timeline.card]:border-0` said it in a way
   nobody could grep for.

   THE CONTAINER TAKES OVER THE PAPER PIN, and that is the load-bearing half.
   `.dark .brew-timeline.card` (above) pins this palette to white paper and
   espresso ink as a PAIR — flattening the inner card's background without
   moving the pin outward left dark ink painted straight onto the dark page,
   and half the player went unreadable in dark mode. Measured, not reasoned
   about: the first version of this rule shipped that way in a screenshot.
   The sheet moves out to the container, the card keeps the ink, and the two
   stay together. */
.home-sample{background:var(--surface);border-color:var(--border)}
/* The pin has to land ON each .brew-timeline, not on the container around
   them. `.brew-timeline` (top of this file) re-derives every one of these
   tokens from the global --color-* set, so it overwrites whatever an ancestor
   pinned — which is why the existing dark pin is written as
   `.dark .brew-timeline.card` rather than on anything outside it. Pinning the
   container alone left the flow map's captions resolving --ink to the dark
   theme's near-white and printing it on the white sheet; measured with
   getComputedStyle rather than guessed at, after the same rule looked right
   in light mode twice.

   Two selectors, because the block holds two different things: the container
   is not a .brew-timeline and needs its own sheet, and the strip and the
   player both are. */
.dark .home-sample,
.dark .home-sample .brew-timeline{
  --bg:        #ffffff;
  --surface:   #ffffff;
  --surface2:  #efe9d7;
  --border:    #e6dfcc;
  --ink:       #2f1f19;
  --ink-soft:  #6e5d54;
}
.home-sample .brew-timeline.card{
  border:0;border-radius:0;padding:0;box-shadow:none;background:transparent}

/* ── Phone portrait: the whole player, in one screen ────────────────────────
 *
 * There is already a landscape block above (max-height:540px) that shrinks
 * everything so the card fits a short viewport. Portrait had no equivalent,
 * and it is the orientation people actually brew in: the card measured 1032px
 * against an 844px viewport, and iOS spends ~150px of that on its own chrome.
 * So roughly 700px of room for 1032px of card — the timer and the controls
 * were always below the fold, which is the half you need while brewing.
 *
 * Nothing here changes what the card CONTAINS. It trims the padding, the
 * label sizes and the wrapping, in that order, because a step you have to
 * scroll to reach is worse than a step drawn slightly smaller.
 */
@media (max-width:640px) and (orientation:portrait){
  /* ── The figures, on one line ─────────────────────────────────────────
     Four cells across a 390px card leaves each about 79px. Label over
     value, and nothing else in the box: the nudge buttons are hidden at
     this width (see the ≤640 block above), which is what makes four fit —
     they wanted 48 of those 79, and "0.63 oz" at the 16px an input must
     carry so iOS does not zoom on focus already wants 50. */
  .brew-timeline .bt-cell{flex-direction:column;align-items:flex-start;
    gap:1px;padding:4px 5px;min-width:0;min-height:0}
  /* .bt-fact-label / .bt-fact-unit, not .bt-cell-lbl / .bt-cell-unit — I
     guessed the names first and trimmed 12px instead of 80. */
  .brew-timeline .bt-fact-label{font-size:8px;letter-spacing:.02em;
    white-space:nowrap;max-width:none;flex:none;line-height:1.1}
  /* 4.2ch, not 3.2: US units make dose "0.63" and yield "1.27", and 3.2ch
     clipped the fourth character while the unit beside it kept its place —
     the "0,6ˌoz" in the report. Four digits is the real worst case. */
  .brew-timeline .bt-cell-input{font-size:16px;width:4.2ch}
  .brew-timeline .bt-fact-unit{font-size:9px}
  .brew-timeline .bt-cell-pre{font-size:13px}
  .brew-timeline .bt-cell-field{flex:none;min-width:0;gap:2px}

  /* The two reading blocks — GRIND above, TOTAL TIME beside the figures —
     were set at desktop leading and each ran to three lines. */
  .brew-timeline .bt-read{gap:0;line-height:1.25}
  /* "TOTAL / TIME" on two lines just looks broken, and the line has room. */
  .brew-timeline .bt-read-lbl{font-size:8.5px;white-space:nowrap}
  .brew-timeline .bt-read-val{font-size:15px}
  .brew-timeline .bt-read-sub{font-size:10px}

  /* ── One thing per line, in the order a brew is set up ─────────────────
       grinder picker
       grind reading
       ratio · dose · yield · water temp
       total time · fullscreen · share
     .bt-facts holds the four cells and the total-time reading; the window
     actions are its SIBLING, so nothing inside .bt-facts can be placed
     beside them. display:contents dissolves that box — it carries no
     border, background or padding of its own (see its base rule), so
     there is nothing to lose — and lifts the cells, the reading and the
     actions into ONE 12-column grid whose last row splits 7/5. */
  .brew-timeline .bt-topbar{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
    gap:5px;align-items:center}
  .brew-timeline .bt-facts{display:contents}
  /* The ≤640px rule above gives .bt-grinder order:1, to drop it below the
     figures in the flex row this used to be. In a grid that same order
     puts it AFTER the cells; DOM order is already right, so clear it. */
  .brew-timeline .bt-grinder{grid-column:1/-1;order:0;flex-direction:column;
    align-items:stretch;gap:5px}
  .brew-timeline .bt-grinder > .bt-read{flex-direction:row;align-items:baseline;
    gap:6px;flex-wrap:wrap}
  .brew-timeline .bt-cell{grid-column:span 3}
  .brew-timeline .bt-facts > .bt-read{grid-column:span 7;flex-direction:row;
    align-items:baseline;gap:7px}
  .brew-timeline .bt-actions{grid-column:span 5;justify-self:end}

  /* THE STEP LIST BECOMES THE CAROUSEL IT LOOKS LIKE. Nine chips wrapping
     across three rows is ~90px of chrome; one scrolling row is 30, and the
     controller already scrolls the running step into view (keepChipInView).
     The dimming above is what makes a single row readable — you are meant to
     look at one chip, not read all nine. */
  /* padding-inline, and it is not cosmetic: the running chip is drawn at
     scale(1.04) with its own border, so at scroll position 0 its left edge
     sits OUTSIDE the scroll box and the browser clips it — the border came
     out missing down one side. The negative margin puts the row's content
     back on the card's own left edge, so nothing appears indented. */
  .brew-timeline .tl-steps{flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;
    scrollbar-width:none;margin-top:7px;padding:3px;margin-inline:-3px}
  .brew-timeline .tl-steps::-webkit-scrollbar{display:none}
  .brew-timeline .tl-chip{flex:0 0 auto}
  /* The group headers are flex-basis:100% line breaks; in a single row they
     would each force a wrap and defeat it. */
  .brew-timeline .tl-chip-group-hdr{display:none}

  /* The scene is the tallest single thing on the card, and it is the thing
     you are actually watching — so it gets every pixel the rest of this
     block gave back. 132px was set when the card ran 300px over the fold;
     it now runs under, and the cap goes up to match.
     CENTRED, and that is not optional: .stage-visual carries
     aspect-ratio 440/300, so a height cap shrinks its WIDTH to match, and a
     narrower box in a full-width column sits against the left edge with
     white beside it — it read as the animation having come loose. The cap
     centres nothing; margin does. The scale strip beneath has to follow it
     or the two stop being one object; its own height is set by its LCD type,
     not an aspect ratio, so widening it costs no vertical space at all. */
  .brew-timeline .stage-visual{max-height:186px;margin-inline:auto}
  .brew-timeline .bt-scale{max-width:min(100%,273px);margin-inline:auto}
  .brew-timeline .stage{gap:8px;margin-top:6px;padding-top:6px}
  .brew-timeline .stage-side{gap:4px}
  /* The track and the chips grow with it — the strip is the same instrument
     as the scene, read at a glance from arm's length. */
  .brew-timeline .tl-track{height:26px}
  .brew-timeline .tl-chip{font-size:13px}

  /* The step block sat in desktop air: 12px above the name, 16 above the
     progress bar, on a card where the same 28px is most of a figure cell. */
  .brew-timeline .step-name{margin-top:4px;margin-bottom:2px}
  .brew-timeline .step-progress{margin-top:7px}
  .brew-timeline .tl-strip{margin-bottom:6px}

  /* Five controls wrapping onto two rows; one scrolling row instead. */
  .brew-timeline .controls{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;gap:6px}
  .brew-timeline .controls::-webkit-scrollbar{display:none}
  .brew-timeline .bt-btn{flex:0 0 auto;padding:8px 13px;font-size:12px}

  .brew-timeline .step-text{font-size:12.5px;line-height:1.4}
  .brew-timeline.card{padding-left:12px;padding-right:12px}
}
