/* The whole sheet sits in the `components` cascade layer, so a Tailwind
   utility written in the markup WINS over any rule in here.

   Why this matters: Tailwind v4 puts its utilities in `@layer utilities`, and
   per the cascade-layers spec UNLAYERED css beats layered css outright,
   whatever the specificity. Left unlayered, every rule below silently
   defeated every utility — which is exactly how three real bugs happened
   (the method, grinder and country pickers each painting their search glass
   on top of their own placeholder, because `.st-rail-field input`'s padding
   beat the combobox's own `pl-8`). Now a utility in the ERB simply wins, and
   the rule it replaces can be deleted afterwards rather than in the same
   breath.

   Verified a no-op when introduced: styles_snapshot 27/27 identical,
   studio.mjs 427/427, studio_sweep 550/550, dark 52/52, units 60/60.
   NOTE this is safe for THIS sheet only — layering brew_timeline.css and
   home.css the same way produced 1191 computed-style differences across 16
   element families, because their markup already carries utilities that were
   losing on purpose. Do not layer those without adjudicating each one. */
/* The recipe studio — hand-written, like brew_timeline.css, and scoped under
   .st-page so nothing here leaks onto the rest of the app (or gets pruned by
   Tailwind's content scan, which never looks at this file). One breakpoint:
   lg (1024px). Below it every control is a touch target (>=44px); above it
   the list and the preview sit side by side and the preview never hides. */
.st-page{
  --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);
  --bad:      var(--color-bad, #8c3b32);
  /* Round 5 Task 3: the same phase-tone palette the public timeline uses
     (brew_timeline.css), duplicated here so the studio's own group bands
     and overview-strip tiles can reference it without depending on that
     file being loaded (it is, for the embedded player, but this keeps the
     studio sheet self-contained). */
  --ph-prep:  #8c7d6f;
  --ph-brew:  #d97757;
  --ph-milk:  #b98a4e;
  --ph-serve: #8a5a33;
  /* Studio-alpha round: the ALPHA chip's own tone (.st-alpha-chip below) —
     same var(--color-x, fallback) idiom as every other token in this
     block, aliasing the site-wide --gold custom property (application.css/
     tailwind's own :root/.dark) rather than hardcoding it twice. */
  --gold:     var(--color-gold, #8a6b4f);
  /* Accent-tint steps this file actually uses (see application.css for the
     full step list + dark-mode rgba values) — precomputed, no color-mix. */
  --accent-tint-06: var(--color-accent-tint-06, rgba(217,119,87,.06));
  --accent-tint-20: var(--color-accent-tint-20, rgba(217,119,87,.20));
  color:var(--ink);background:var(--bg);
  max-width:1100px;margin:0 auto;padding:16px;
}
/* Round 5 Task 3: dark-mode phase-tone values mirror brew_timeline.css's
   own .dark .brew-timeline palette. */
.dark .st-page{
  --ph-prep: #ac7c26;
  --ph-brew: #cb364b;
  --ph-milk: #a19400;
  --ph-serve: #b64118;
}

/* ── Top bar: back link, title, Save ── */
/* Task 3b: 4px, not the original 12px — the below-lg bar now carries one
   more icon control (.st-preview-btn) than the sweep's own 390px width budget
   (scripts/verify/studio_sweep.mjs's own "no horizontal page scroll" check)
   had room for at 12px; tightening every gap by 8px buys back exactly the
   room one more 44px control costs. Cosmetic-only at lg+, where the bar
   spans (at minimum) a 1024px shell and was never anywhere near this edge.

   Lang-topbar round: 3px, not that 4px — .st-lang-trigger (studio.html.
   erb's own .st-lang-widget comment) is a SECOND new icon control landing
   on this exact row, the same 44x44-plus-a-gap cost .st-preview-btn's own
   arrival cost the last time this comment was written, and there's no
   spare gap left to give: measured directly (scripts/verify/studio_
   sweep.mjs's own "no horizontal page scroll" check at 390 started
   FAILING the moment the trigger was added — scrollWidth 422 against a
   390 budget, ~32px over). This 1px-per-gap trim plus .st-save/.st-units'
   own tightened padding (below) together buy back ~31px — a matching
   floor-respecting trim, not a touch-target shrink: every control on this
   row still measures >=44x44 (assertTouchTargets), only their SURROUNDING
   whitespace shrank. */
.st-page .st-topbar{display:flex;align-items:center;gap:3px;margin-bottom:16px}
/* Task 6 (chrome slice): .st-back/.st-undo/.st-redo/.st-save/.st-preview-btn/
   .st-units all moved their box/colour/hover/disabled styling into markup
   (.st-back also gained `lg:hidden` directly, replacing the separate lg+
   override further down this file). Each icon button's own child `svg`
   sizing rule stays here — the icon comes from brew_tl_icon(), which takes
   no class argument, so there's nowhere in the ERB to put a matching
   utility without changing that helper's signature. */
.st-page .st-back svg{width:18px;height:18px}
.st-page .st-title{flex:1;min-width:0;margin:0;font-family:"Source Serif 4",serif;font-weight:700;
  font-size:19px;line-height:1.25;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.st-page .st-undo svg,.st-page .st-redo svg{width:17px;height:17px}
.st-page .st-preview-btn svg{width:18px;height:18px}

/* Studio-alpha round: the ALPHA status chip (studio.html.erb/studio_home.
   html.erb's own comment on why it sits in this exact DOM slot). A STATIC
   span, never a button — no touch-target floor to clear, unlike every
   other control on this row. Gold (--color-gold), not accent/bad: neither
   "primary action" nor "broken" fits a status marker — see application.css
   (tailwind)'s own .alpha-tag comment, the exact same reasoning, for the
   entry-point cousin of this chip outside the studio.

   Below lg (default, this block): icon-only — .st-alpha-label/-note stay
   display:none below the lg+ override further down this file. Not a
   compromise made lightly: the below-lg topbar row already spends nearly
   every spare pixel on back/units/undo/redo/preview/save (.st-topbar's own
   "4px, not 12px" comment above), and studio_sweep.mjs's own
   assertStepListInFirstViewport is ALREADY failing by ~46px on this row's
   single tightest combination (vi/us) with no chip here at all — measured
   directly against a live server. A dummy element swept through that same
   row confirmed the real ceiling by hand: ~16px added here is still safe
   (no wrap, no horizontal scroll), 20px already isn't. 14px (this icon,
   with zero padding of its own) leaves a couple of px of margin under that
   ceiling rather than sitting flush against it. The full warning still
   reaches every visitor at every width regardless — title + aria-label on
   the OUTER span always carry recipes.studio.alpha.warning in full (see
   the view's own comment on why role="img" + aria-hidden children, not a
   sr-only span, carries it) — a below-lg visitor only ever loses the extra
   on-screen word a lg+ visitor gets for free below. */
/* Accent orange, not gold — this is a caution about unfinished software,
   and the gold tone read as decoration rather than a warning. */
.st-page .st-alpha-chip{flex:none;display:inline-flex;align-items:center;gap:4px;color:var(--accent)}
.st-page .st-alpha-icon{width:14px;height:14px}
/* The short "Alpha" word shows at EVERY width, not only lg+: a lone gold
   triangle with a title tooltip tells a touch visitor nothing (tooltips
   need a hover this device doesn't have), and telling visitors the studio
   is unfinished is the entire point of the chip. The ~16px horizontal
   ceiling this row really does have is paid for right below — .st-back
   drops its own "Back" WORD below lg, keeping its arrow (and its
   aria-label, which was always the link's accessible name — see the view:
   `aria: { label: ... }`, never the span). Net effect on the row: the
   label costs ~40px, the dropped word gives back ~34px, and the measured
   below-lg first-viewport top is unchanged. Only .st-alpha-note (the
   longer teaser sentence) stays lg+-only. */
.st-page .st-alpha-label{display:inline;font:700 10px "JetBrains Mono",monospace;
  letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--accent);
  border-radius:999px;padding:2px 6px;white-space:nowrap}
.st-page .st-alpha-note{display:none}
/* Below lg the WORD replaces the triangle rather than joining it: the two
   together measured 2px past the viewport at 390 (studio_sweep.mjs's own
   "no horizontal page scroll" check, EN/metric — the tightest combination
   for this row is not the same one that's tightest vertically), and of the
   two the word is the one that actually says "alpha" without a hover. */
.st-page .st-alpha-icon{display:none}
/* .st-back-label's own display:none (Task 6, chrome slice) moved to a
   plain `hidden` utility in markup — it's unconditional at every width
   (the parent .st-back itself vanishes at lg+ instead), so no lg: variant
   was needed. */

/* ── Lang-topbar round: the language control — a small POPOVER below lg
   (this block: .st-lang-widget/.st-lang-trigger/.st-lang-bar), the SAME
   .st-lang-bar reparented INLINE into the top bar itself at lg+ (the
   @media(min-width:1024px) block further down this file — search
   ".st-lang-widget{display:contents}"; studio.html.erb's own comment on
   that div has the full "why", this file only owns the two shapes' CSS).
   Round 5 Task 2's own original shape — this bar as its own full-width row
   directly under the topbar, below lg — is gone: it cost the below-lg
   first-viewport budget studio_sweep.mjs's own assertStepListInFirstViewport
   guards ~54px it never needed to spend once a popover could hold the same
   pills instead.

   "menu" (app/javascript/controllers/menu_controller.js) owns the popover's
   own open/close, outside-click and Escape handling — the same controller
   the share popover (brew_timeline.css's own .bt-share-panel) and the
   header's own language/account/mobile menus already lean on; this file
   only styles the two states its own [hidden] attribute carries.

   EN pill (.is-base) never carries a remove button — it's the base, not a
   layer. Every OTHER pill is TWO separate <button>s inside one rounded
   wrapper (never one button nesting another — invalid HTML): the switch
   (the pill's own label, min 44px in BOTH dimensions per assertTouchTargets)
   and the remove "×" (same 44x44 floor, never smaller just because it's a
   secondary action within the pill). flex:1 on the switch is what makes
   that 44px floor read naturally once the popover's own align-items:stretch
   (below) widens every pill to the panel's own width — without it the
   switch would keep its own narrow, content-sized width and leave a bare
   gap between it and the "×"; at lg+, where a pill's own width is already
   exactly its content (flex:none in that row), there is no extra space for
   flex:1 to claim, so it's a no-op there. */
.st-page .st-lang-widget{position:relative;flex:none}
.st-page .st-lang-trigger{flex:none;display:grid;place-items:center;min-width:44px;min-height:44px;
  padding:0 6px;border:0;border-radius:8px;background:none;color:var(--ink-soft);
  font:700 12px "JetBrains Mono",monospace;letter-spacing:.03em;cursor:pointer}
.st-page .st-lang-trigger:hover{color:var(--accent);background:var(--surface2)}
/* The panel itself — position:absolute, closed by the browser's own
   [hidden] handling until menu#toggle clears the attribute. left:0 (not
   brew_timeline.css's own .bt-share-panel idiom of right:0): THIS trigger
   sits near the LEFT of a narrow topbar (back arrow, alpha chip, then this
   — studio.html.erb's own source order), not the right, so right:0 here
   would hang the panel off the viewport's own left edge instead. Vertical
   (flex-direction:column), not Round 5 Task 2's old single scrollable row —
   a popover has real vertical room a below-lg topbar row never did, so
   there's no ~60px-regardless-of-count ceiling to design around any more;
   max-height/overflow-y is just a safety net for a hypothetical visitor
   with every one of the six locales active on a very short viewport. */
.st-page .st-lang-bar{position:absolute;top:calc(100% + 6px);left:0;z-index:20;
  display:flex;flex-direction:column;align-items:stretch;gap:6px;
  min-width:220px;max-width:calc(100vw - 32px);max-height:calc(100vh - 80px);overflow-y:auto;
  padding:8px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface);box-shadow:0 10px 28px rgba(0,0,0,.15)}
.st-page .st-lang-bar[hidden]{display:none}
.st-page .st-lang-bar .st-lang-pill,.st-page .st-lang-bar .st-lang-add{flex:none}
.st-page .st-lang-pill{display:inline-flex;align-items:stretch;border:1px solid var(--border);
  border-radius:999px;overflow:hidden;background:var(--surface)}
.st-page .st-lang-pill-switch{flex:1;border:0;background:none;color:var(--ink-soft);font:600 13px "Inter";
  min-height:44px;min-width:44px;padding:0 16px;cursor:pointer;white-space:nowrap}
.st-page .st-lang-pill-switch.is-active{background:var(--ink);color:var(--bg)}
.st-page .st-lang-pill-switch:hover:not(.is-active){color:var(--accent)}
.st-page .st-lang-pill-remove{display:grid;place-items:center;min-width:44px;min-height:44px;border:0;
  border-left:1px solid var(--border);background:none;color:var(--ink-soft);cursor:pointer}
.st-page .st-lang-pill-remove svg{width:13px;height:13px}
.st-page .st-lang-pill-remove:hover{color:var(--bad);background:var(--surface2)}
/* The "+" control — a plain <select> (see studio.html.erb's own comment on
   why: at most 5 remaining options, a native picker is the simplest correct
   thing), styled to read as one more pill-shaped chip rather than a
   standard boxy dropdown. */
.st-page .st-lang-add{min-height:44px;min-width:44px;padding:0 14px;border:1px dashed var(--border);
  border-radius:999px;background:none;color:var(--ink-soft);font:600 13px "Inter";cursor:pointer}
.st-page .st-lang-add:hover{color:var(--accent);border-color:var(--accent)}

/* ── Item 1 (studio-polish round): the destructive-confirm dialog — a
   native <dialog> (studio.html.erb's own .st-confirm-dialog, showModal()
   via recipe_studio_controller.js#confirmDestroy), replacing removeLocale()'s
   former window.confirm() call. Same visual language as the rest of the
   page: 8px radii, --border/--surface/--ink, the pill-shaped 44x44+ button
   idiom .st-lang-pill-switch/.st-save already use above, --bad for the
   destructive action (matching .st-lang-pill-remove's own hover colour).

   ::backdrop dims the page behind it (the browser paints this pseudo-
   element automatically once showModal() runs) — a flat scrim, not a blur,
   matching .st-lang-bar's own box-shadow-only "float above the page"
   treatment rather than introducing a new visual idiom just for this. The
   dialog element itself is unstyled by the UA stylesheet's own centering
   here (border/background/padding all reset below) — position, not
   `display`, is what the UA's own [open] toggles, so no extra show/hide
   rule is needed on top of it. Task 6 (chrome slice): every declared box/
   typography/colour/hover rule below moved into markup — only the
   pseudo-element backdrop survives (explicit KEEP category). */
.st-page .st-confirm-dialog::backdrop{background:rgba(47,31,25,.45)}

/* ── The figures line (shared brew_methods/_facts_line partial, mounted here
   with controller: "recipe-studio") — every rule this partial itself relies
   on for a cell's own look (border, padding, the two nudge buttons' size and
   position) is written scoped to .brew-timeline in brew_timeline.css, and
   the studio deliberately never wraps this instance in that class — Item 3
   (studio-polish round) DID put a grinder picker beside it (.st-figures-
   topbar/.st-figures-grinder, further up this file), but as this page's own
   from-scratch, .st-page-scoped equivalent of .bt-topbar/.bt-grinder, not a
   real .brew-timeline mount (that class carries a LOT more than a topbar
   row — the player's own card chrome, fonts, the whole animated stage —
   none of which belongs on .st-brew-card, which already supplies its own).
   Left alone, a cell here renders with NONE of that: an
   unconstrained input inside an unbordered grid track blows the "1fr 1fr"
   two-up layout brew_timeline.css's own @media(max-width:640px) still
   applies (unscoped) past the viewport's edge, and the nudge buttons — an
   absolutely-positioned wrapper around two SVGs with no explicit size —
   collapse to 0x0 (a replaced element sized only by viewBox, inside a
   shrink-to-fit box, resolves to nothing). So: the studio gets its own
   from-scratch copy of a cell's look below — sized as this page's own
   touch targets (44px) BELOW LG, and switched to the player's own compact
   24x24 stacked-stepper shape at lg+ (see the @media(min-width:1024px)
   block further down — the user's call, "same as the method page", so the
   whole params row fits on ONE line beside the GRIND readout the way
   brew_methods#show's own .bt-topbar does). The player's own
   .brew-timeline-scoped rules are untouched by any of this. */
.st-page .bt-facts{display:flex;flex-wrap:wrap} /* undoes the unscoped 2-col grid at <=640px */
.st-page .bt-cell{position:relative;display:flex;flex-direction:column;justify-content:center;gap:3px;
  min-width:188px;min-height:44px;padding:8px 92px 8px 10px;border:1px dashed var(--border);
  border-radius:7px;background:var(--surface);transition:border-color .12s ease,background-color .12s ease}
.st-page .bt-cell-field{display:flex;align-items:center;gap:3px;min-width:0}
.st-page .bt-cell-pre{font:600 13px/1 "JetBrains Mono",monospace;color:var(--ink-soft)}
.st-page .bt-cell-input{width:4.5ch;min-width:2.5ch;min-height:44px;padding:0;border:0;background:none;
  color:var(--ink);font:600 20px/1.15 "JetBrains Mono",monospace;font-variant-numeric:tabular-nums;
  -moz-appearance:textfield}
.st-page .bt-cell-input::-webkit-outer-spin-button,
.st-page .bt-cell-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.st-page .bt-cell-input:focus{outline:none}
.st-page .bt-cell:focus-within{border-style:solid;border-color:var(--accent)}
.st-page .bt-fact-unit{font:600 11px/1 "JetBrains Mono",monospace;color:var(--ink-soft)}
.st-page .bt-cell.is-edited{border-style:solid;border-color:var(--accent);background:var(--accent-tint-06)}
.st-page .bt-cell.is-edited .bt-fact-label{color:var(--accent)}
.st-page .bt-cell.is-edited .bt-cell-input,
.st-page .bt-cell.is-edited .bt-cell-pre,
.st-page .bt-cell.is-edited .bt-fact-unit{color:var(--accent-2)}

/* Task 2c (v2.2): the readout half of the line ("TOTAL TIME" — the one
   fact key with no `input:`, see brew_methods_helper.rb's own fact_list —
   rendered by _fact_read.html.erb, not _facts_line.html.erb's own .bt-cell
   branch). brew_timeline.css styles .bt-read/.bt-read-val too, but only
   scoped under .brew-timeline — a class this instance deliberately never
   wears (this section's own top comment) — so without a studio-scoped
   equivalent .bt-read fell back to being a plain block box: its label and
   value spans laid out INLINE at the box's own top edge rather than
   stacked and centred, and .bt-read-val carried no font-weight at all
   (browser default 400 — "thin" beside every .bt-cell-input's own 600).
   .bt-facts' own (unscoped) align-items:stretch already matches this box's
   OUTER height to the tallest .bt-cell sharing its flex row — centring
   only ever needed a flex column + justify-content:center on the inside. */
.st-page .bt-read{display:flex;flex-direction:column;justify-content:center;gap:3px;
  padding:8px 10px;min-width:0}
.st-page .bt-read-val{font:700 13px/1.25 "JetBrains Mono",monospace;
  font-variant-numeric:tabular-nums;color:var(--ink)}
.st-page .bt-read-sub{font:500 11px/1.2 "JetBrains Mono",monospace;color:var(--ink-soft)}
.st-page .bt-read-sub:empty{display:none}

/* The two nudge buttons, side by side below lg (not stacked — each has to
   clear 44px in BOTH dimensions on its own there, and stacking two 44px
   targets would double the cell's own height for no reason). At lg+ they
   collapse into the player's own compact 24x24 vertical stack — see the
   @media(min-width:1024px) block further down. */
.st-page .bt-spin{position:absolute;right:4px;top:50%;transform:translateY(-50%);display:flex;gap:2px}
.st-page .bt-spin button{flex:none;width:44px;height:44px;display:grid;place-items:center;
  border:1px solid var(--border);border-radius:8px;background:var(--surface2);
  color:var(--ink-soft);cursor:pointer;padding:0;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
  transition:border-color .12s ease,color .12s ease,background-color .12s ease}
.st-page .bt-spin button:hover{color:var(--accent);border-color:var(--accent);background:var(--surface)}
.st-page .bt-spin button:active{background:var(--accent-tint-20)}
.st-page .bt-spin svg{width:13px;height:13px;stroke-width:3}

/* Task 3 (v2.1): the figures line's own "edit notes" affordance — one small
   icon button, same 44px touch target and hover treatment as a step's own
   .st-row-btn, sitting after every .bt-cell in source order (so it never
   competes with the "1fr 1fr" wrap the cells themselves rely on below
   640px — flex-wrap just carries it onto its own line there). */
.st-page .bt-figures-edit{flex:none;display:grid;place-items:center;width:44px;height:44px;
  border:1px dashed var(--border);border-radius:7px;background:var(--surface);
  color:var(--ink-soft);cursor:pointer;transition:border-color .12s ease,color .12s ease}
.st-page .bt-figures-edit:hover{color:var(--accent);border-color:var(--accent)}
.st-page .bt-figures-edit svg{width:18px;height:18px}
.st-page .bt-figures-edit[aria-expanded="true"]{color:var(--accent);border-style:solid;border-color:var(--accent)}

/* ── Task 2 (v2.1): the centre column's own copy fields — intro (above the
   figures line) and notes (below the step list), the same textareas THE
   WORDS rail group used to hold (now deleted), moved here to read like the
   method page's own reading order (page_header hook -> Brew with us ->
   Notes — see studio.html.erb's own comment) at EVERY width, not just lg+.
   A small mono section label, the studio's own rail-label idiom reused
   rather than duplicated.

   Fix wave (F5, stale below — .st-copy-intro itself is long gone, moved
   into the rail as THE RECIPE's own DESCRIPTION field): this rule's own
   20px margin used to be cancelled by a matching negative margin-top below
   lg, closing the gap a collapsed strip toggle would otherwise leave. That
   cancelling rule has since moved — it now lives on `.st-page .st-pinned`,
   not `.st-page .st-scene-strip` (pinned-context round: .st-pinned, not
   the strip alone, is what now sits adjacent to whatever precedes it in
   flow — see that rule's own comment, further down this file, for the
   current pairing and why the two numbers still have to agree in
   MAGNITUDE).

   Round 5 Task 2: reverting the merged Lexxy box back to these two plain
   textareas gives the below-lg first-viewport budget studio_sweep.mjs's own
   assertStepListInFirstViewport guards BACK the toolbar/max-height overhead
   the rich editor carried (Lexxy's own ~50px toolbar plus its own tightened
   max-height ceiling) — intro's own field-sizing:content floor (84px) is
   comfortably under that, so no further trimming was needed here. (At the
   time, that budget also had to make room for the language pill bar as its
   own full-width row directly under the topbar — the Lang-topbar round
   moved that bar into the topbar itself, a popover below lg, giving the
   budget that row's ~54px back too; see .st-lang-widget's own comment,
   above this rule in source order.) */
.st-page .st-copy{display:flex;flex-direction:column;gap:6px;margin:20px 0}
.st-page .st-copy-label{display:flex;align-items:center;gap:6px;font:600 10px "JetBrains Mono",monospace;
  letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft)}
.st-page .st-copy textarea{width:100%;border:1px solid var(--border);border-radius:8px;background:var(--surface);
  color:var(--ink);font:500 14.5px "Inter";line-height:1.55;padding:10px 12px;resize:vertical;min-height:44px;
  overflow-y:auto;field-sizing:content}
/* Task 1 (round 3): grow-with-content — field-sizing:content (Chrome/Edge
   123+; see .st-field textarea's own comment further down this file for the
   full cross-browser/reparenting/undo reasoning, identical here) is set on
   the shared rule just above (field-sizing doesn't inherit, so each rule
   that wants it sets its own); this adds its own min/max ceiling — notes is
   the studio's longest free-text field at 5000. Task 6 (Step 7, prune):
   .st-copy-intro (a sibling min/max ceiling for a "lead" textarea) is dead
   — that field moved into the rail as THE RECIPE's own DESCRIPTION long
   before this task (studio.html.erb's own .st-rail-field .st-title-input
   comment), leaving `.st-copy-intro` with zero producers app-wide. */
.st-page .st-copy-notes textarea{min-height:120px;max-height:360px}

/* ── Task 2 (round 3): the live overview strip — a bare `.brew-timeline`
   (see studio.html.erb's own comment on why bare, no `.card`, is what the
   dark "paper-tile" pin needs) holding the method page's own `.bt-overview`/
   `.bt-ov-*` tiles (brew_timeline.css) unmodified — every rule those classes
   carry (grid, gap, the row-end connector, the dark pin) applies here for
   free. The one thing this strip's tiles do differently from the read-only
   page's own (brew_methods/_overview.html.erb) is what fills `.bt-ov-scene`:
   a plain <img src="/brew-scenes/:key"> (studio.html.erb's own comment on
   why: no live GSAP mount here) rather than a mounted, animated <svg
   class="scene"> — that <svg> rule (.brew-timeline .scene, same file)
   already sizes itself position:absolute/inset:0/100%×100%; this is the
   same sizing for the <img> stand-in, scoped to this strip alone so it
   never touches any other .bt-ov-scene on the site (none of which have ever
   held an <img> before).

   Collapsed behind a native <details> below lg, always open (summary
   hidden) at lg+ — same mechanism, same reason, as .st-preview-toggle just
   below (see studio.html.erb's own comment: the below-lg first-viewport
   budget assertStepListInFirstViewport guards has no room for a wrapped
   multi-row tile grid on top of everything already stacked above the step
   list there). */
.st-page .st-scene-strip .bt-ov-scene img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
/* Item 4: this strip draws its OWN sequence connector — an inline SVG
   arrow, not brew_timeline.css's own dashed ::after line (unscoped,
   otherwise active here too) — so it's disabled outright, scoped to this
   strip alone; the public method page's own Overview strip keeps its
   existing dashed connector untouched, out of scope for this task
   (studio.html.erb's own comment on the arrow itself has the full "why"). */
.st-page .st-scene-strip .bt-ov-tile::after{content:none}
/* The arrow itself: a normal DOM child of the tile (never a grid/flex item
   of its own — studio.html.erb's own comment), pulled out of flow into the
   tile's own right-hand gap. .bt-ov-tile is already position:relative
   (brew_timeline.css, unscoped) so this needs no positioning context of its
   own. Vertically centred on the SCENE image specifically (not the whole
   tile, caption included) — top:38% lands inside .bt-ov-scene's own
   440:300 aspect box regardless of caption length. Recessive colour
   (--ink-soft) — a connector, not content — and pointer-events:none: it's
   aria-hidden and sits over a live gap, never something a visitor should
   be able to accidentally target. */
.st-page .st-scene-strip .bt-ov-tile svg[data-icon="arrow_right"]{
  position:absolute;top:38%;right:-19px;width:14px;height:14px;color:var(--ink-soft);pointer-events:none}
/* Below lg the strip is a wrapped CSS grid (brew_timeline.css's own base
   .bt-overview rule) — a row-end tile's own next sibling wraps onto a NEW
   row, not the gap this arrow sits in, so it's hidden there; re-shown at
   lg+ below, where the strip is one non-wrapping row instead. */
.st-page .st-scene-strip .bt-ov-tile.is-row-end svg[data-icon="arrow_right"]{display:none}
/* Below 640px brew_timeline.css's own @media(max-width:640px) forces the
   base .bt-overview grid to exactly 2 columns regardless of --ov-cols,
   while is-row-end is still computed against strip_cols server-side
   (studio.html.erb) — the same mismatch the OLD dashed connector already
   had at that width (see its own "the last tile of a row has none"
   comment, right above its base rule) — so the arrow is hidden outright
   below 640px too, rather than drawing one that points at the wrong
   neighbour. */
@media (max-width:640px){
  .st-page .st-scene-strip .bt-ov-tile svg[data-icon="arrow_right"]{display:none}
}
/* Below lg: .st-pinned itself is pulled up to close the gap .st-rail-left's
   own 20px margin-bottom (base rule, above) would otherwise leave in front
   of it — a big blank seam belongs between actual content blocks, not
   before the params row that now opens .st-pinned. studio_sweep.mjs's own
   assertStepListInFirstViewport is the reason this needed measuring at all:
   the tightest real combination it sweeps (390x844, vi/us — longer Vietnamese
   labels plus wider "oz"/"°F" readouts already leave the figures line taller
   than the en/metric one) had only ~32px of headroom left before this strip
   existed at all; the summary's own 44px minimum (assertTouchTargets' own
   floor, not negotiable) alone would have blown that. Closing this one seam
   recovers it without shrinking anything a visitor can actually tap.

   Pinned-context round: this margin used to live on .st-scene-strip itself,
   which used to sit directly after .st-rail-left in source order (studio.
   html.erb's own comment on .st-pinned has the full "why it moved" story).
   Now .st-pinned is what's adjacent to .st-rail-left, so the cancelling
   margin moved up onto it — .st-scene-strip needs none of its own any more,
   it just sits wherever normal flow puts it after the params and their
   notes panel, inside the same wrapper. Zeroed again at lg+ (below) where
   .st-pinned sits in its own grid row instead, with no adjoining sibling
   margin to collapse against. */
.st-page .st-pinned{margin-top:-20px}
/* Two-block round (user's call — "separate params and overview into 2
   blocks, right now there is no margin/padding"): the params row and the
   overview strip used to touch at a measured 0px gap, so two different
   things — the recipe's INPUTS and a PREVIEW of its flow — read as one
   merged band. The strip now stands off by 8px below lg (the tightest
   first-viewport budget studio_sweep.mjs's own assertStepListInFirstViewport
   guards can afford there) and a full 16px at lg+ (below), matching the
   16px the player card already sits below the strip — one rhythm, not three
   different gaps. */
.st-page .st-scene-strip{border:0;margin-top:8px}
.st-page .st-scene-strip-summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:6px;
  min-height:44px;font:600 10px "JetBrains Mono",monospace;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft)}
.st-page .st-scene-strip-summary::-webkit-details-marker{display:none}
.st-page .st-scene-strip-summary::before{content:"▸";color:var(--ink-soft)}
.st-page .st-scene-strip[open] .st-scene-strip-summary::before{content:"▾"}
.st-page .st-scene-strip .brew-timeline{margin-top:4px}
@media (min-width:1024px){
  .st-page .st-pinned{margin-top:0}
  /* Two-block round: at lg+ the params row becomes its own bordered
     INSTRUMENT PANEL (the same card idiom .st-brew-card below it and the
     rail groups beside it already use) — the pinned region's bare dashed
     cells floating on the dotted page background were the only surfaceless
     block in the editor, and the method page this row mirrors keeps its
     own figures inside the Brew-with-us card's header. The strip stays
     bare on purpose: its tiles are already bordered boxes, and wrapping
     them in a second frame would double-box them — panel vs bare tiles is
     exactly the contrast that makes the two blocks legible as TWO. 16px
     gap between them, matching the strip-to-card gap that already
     existed. align-items:stretch is kept deliberately (not center): the
     sweep's one-row assertion compares item tops, which only stay equal
     under stretch. */
  .st-page .st-figures-topbar{background:var(--surface);border:1px solid var(--border);
    border-radius:10px;padding:10px 12px}
  .st-page .st-scene-strip{margin-top:16px}
  .st-page .st-scene-strip-summary{display:none}
  .st-page .st-scene-strip .brew-timeline{display:block!important;margin-top:0}
  /* Same wrinkle .st-preview-toggle::details-content documents below: a
     <details> with no `open` attribute (server always renders this one
     closed, on purpose) hides its non-summary content through its own
     ::details-content box regardless of what `display` the child computes
     to, so that box itself is the one thing that has to be overridden. */
  .st-page .st-scene-strip::details-content{content-visibility:visible!important;block-size:auto!important}

  /* At lg+ the strip is always open (no user action reveals it, unlike
     below lg's own <details>), so its height comes straight out of the
     fixed 100dvh shell's own budget — .bt-overview's base rule (brew_
     timeline.css) wraps into balanced ROWS of tiles (brew_overview_columns,
     studio.html.erb), which was costing 3 wrapped rows (~240px) here,
     nearly HALVING what was left for .st-grid's own step list underneath
     it (a real usability regression, caught by scripts/verify/studio_
     sweep.mjs's own lg+ drag-reorder check going flaky: the row it needed
     to drop onto had scrolled just out of .st-grid's own internal
     scrollport). A single non-wrapping, horizontally-scrolling row — the
     brief's own "may stack/scroll horizontally" alternative, applied here
     instead of below lg (below lg it stacks, see .st-scene-strip .bt-
     overview's own base 2-column wrap at <=640px, brew_timeline.css) —
     holds this to ONE tile's own height regardless of step count, the same
     "cheap and bounded, not proportional to content" reasoning the
     collapsed-by-default disclosure below lg already applies. Scoped to
     THIS strip alone (.st-scene-strip .bt-overview, not bare .bt-overview)
     — the read-only method page's own Overview strip keeps its existing
     wrapped-grid shape untouched, at every width. */
  .st-page .st-scene-strip .bt-overview{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:14px;padding-bottom:6px;
    /* Round 5 Task 5: a subtle right-edge fade whenever the row overflows,
       removed once the visitor scrolls to the end (is-fade class toggled by
       recipe_studio_controller.js#updateSceneStripFade). */
    mask-image:linear-gradient(90deg,#000 calc(100% - 56px),transparent);
    -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 56px),transparent)}
  .st-page .st-scene-strip .bt-overview:not(.is-fade){mask-image:none;-webkit-mask-image:none}
  /* 120px tiles (was 130) with a FIXED 56px scene height (was the 440:300
     aspect, ~89px at this width) — user's call: "make the overview smaller,
     we can view more steps in a screen". The scene art is an object-fit:
     contain <img> (base rule above), so a shorter box just scales the
     drawing down, never crops it; the caption keeps its own 12.5px size
     and wraps exactly as the method page's own does. The strip's own
     height drops from ~117px to ~80px — the saving goes straight to the
     step list underneath (the whole point of the pinned region's own 40dvh
     cap, see .st-pinned's rules further down). */
  .st-page .st-scene-strip .bt-ov-tile{flex:0 0 var(--ov-basis,120px)}
  .st-page .st-scene-strip .bt-ov-scene{aspect-ratio:auto;height:56px}
  .st-page .st-scene-strip .bt-ov-cap{margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* brew_timeline.css's own dashed ::after connector is already disabled
     for this strip at every width (base rule, above) — this file draws its
     own SVG arrow instead. is-row-end no longer means anything about
     wrapping up here (the strip is one non-wrapping scrolling row, just
     above), so every tile's own arrow (short of the true last tile, which
     never renders one — studio.html.erb) belongs back, tucked into the
     tighter 14px flex gap instead of the wrapped grid's own (28px normally
     / 20px <=640px, base rules above) one. */
  .st-page .st-scene-strip .bt-ov-tile.is-row-end svg[data-icon="arrow_right"]{display:block}
  .st-page .st-scene-strip .bt-ov-tile svg[data-icon="arrow_right"]{right:-14px}
  /* The arrow's base top:38% (of the whole tile, caption included) centred
     it on the OLD ~89px aspect-ratio scene box; with the 56px fixed scene
     above, centre-on-scene is 56/2 - 14/2 = 21px. */
  .st-page .st-scene-strip .bt-ov-tile svg[data-icon="arrow_right"]{top:21px}
}

/* 640-1023px: below lg there is no grid shell (the lg+ three-pane rule
   above is scoped to @media(min-width:1024px) alone), so the studio is one
   scrolling column and .st-pinned needs REAL position:sticky to stay in
   view — the lg+ block's own grid-row placement does nothing here. The
   opaque background is load-bearing: without it the player and step list
   scrolling underneath would show through the gap behind the params/strip
   text instead of being hidden by it. z-index:20 only needs to clear this
   column's own content (nothing else in this band is positioned/z-indexed);
   picked to match the lg+ .st-pinned's own implicit stacking via grid
   placement rather than inventing a new value.

   The base (below-lg) rule above still gives .st-pinned margin-top:-20px
   (the seam-closing trick against .st-rail-left's margin-bottom), and unlike
   lg+ (which zeroes it at line ~449) this band never resets it — so a
   sticky, scrollable .st-pinned would carry the negative margin AND the cap
   at once. Verified empirically (task-3-report.md) that this does NOT shift
   where the box sticks or clip its own content: the negative margin moves
   .st-pinned's box 20px earlier in normal flow (closing the seam against
   .st-rail-left, exactly as intended), but `top:0` still means "stick when
   MY OWN top edge reaches the scrollport's top edge" — the margin affects
   where that edge starts, not the sticky offset itself, and margins don't
   apply inside a scroll container's own content box the way padding would,
   so nothing of .st-pinned's own content is clipped either.

   A hairline bottom border (1px solid var(--border), the same idiom the
   lg+ topbar and rail groups already use — see .st-topbar and .st-rail-
   group above) marks the sticky region's own edge once content scrolls
   underneath it; deliberately not a shadow, since none of this file's other
   surfaces use one and the plan's own vocabulary is borders, not elevation.

   Phones (<640px) are deliberately excluded — this is a measured decision,
   not an oversight (see the design spec's own measured-heights table): at
   390x844 the params alone stack to 314px of a 338px first-viewport budget,
   which would leave roughly two step rows visible on the one viewport whose
   budget (firstviewport@390x844 vi/us, ~7px of headroom) is already
   tightest. */
@media (min-width:640px) and (max-width:1023px){
  /* Fix-first round: same reasoning as the lg+ block's own comment on
     .st-page>.st-pinned above — the cap moves off .st-pinned itself and
     onto .st-scene-strip alone, so the params row can never be scrolled out
     of view just because the (below-lg) collapsible strip was opened tall.
     Below lg the strip defaults to closed (a native <details>, only its
     summary line contributing height — see .st-scene-strip's own comment
     further up), so this band rarely needs the inner scroll at all; it
     still exists for the case a visitor opens it on a short viewport. */
  .st-page .st-pinned{position:sticky;top:0;z-index:20;
    display:flex;flex-direction:column;min-height:0;max-height:40dvh;
    background:var(--surface);border-bottom:1px solid var(--border)}
  .st-page .st-pinned>.st-scene-strip{flex:1 1 auto;min-height:0;overflow-y:auto}
}

/* ── The centre column's flow: single column at every width — intro, the
   figures line, the preview, the step list + add-a-step, notes. Below lg
   this used to be v1's own byte-identical "list then preview" pair (a
   176-check sweep guarded it); Task 2 (v2.1) deliberately supersedes that
   constraint — the preview now sits ABOVE the list at every width (source
   order inside .st-grid: .st-preview-toggle then .st-editor — no `order`
   override needed, so below lg and lg+ can never disagree about which
   comes first). The lg+ shape is Task 5's own three-pane shell further down
   this file (see "the lg+ three-pane shell" below): .st-grid becomes a
   single flex COLUMN there instead of the old two-up split. */
.st-page .st-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:24px;margin-top:20px}

/* ── Step list ──
   Task 6 (step-list slice): .st-list/.st-row-head/-n/-thumb/-main/-main-text/
   -name/-desc/-secs all moved into markup — flat box/typography rules with
   no other rule competing for the same property on the same element.
   .st-row/.st-row-pinned/.st-row[data-selected] stay CSS together: both
   .st-row and .st-row-pinned set `border-style` on the SAME element when a
   row is pinned (solid vs dashed) — converting one to a utility while
   leaving the other as CSS (or vice versa) makes the WINNER depend on
   Tailwind's own utility-generation order, not the source order this file
   currently guarantees; not safe to split. .st-row-handle/.st-row-pin (icon
   buttons) and their child svg sizing stay CSS too — their icon comes from
   brew_tl_icon(), which takes no class argument, so converting the button
   without a matching helper change would leave the svg's own size rule
   orphaned; deferred. .st-row-ghost (SortableJS's own ghostClass, forceFallback
   clone) stays CSS — its class name is handed to Sortable as a literal
   string, and it also sets `background` on an element that already carries
   .st-row's own (CSS) background, the same same-property risk as above. */
.st-page .st-row{border:1px solid var(--border);border-radius:10px;background:var(--surface)}
.st-page .st-row[data-selected]{border-color:var(--accent)}
.st-page .st-row-pinned{border-style:dashed}
.st-page .st-row-handle,.st-page .st-row-pin{flex:none;display:grid;place-items:center;
  width:44px;height:44px;border:0;background:none;color:var(--ink-soft);cursor:grab}
.st-page .st-row-handle svg,.st-page .st-row-pin svg{width:18px;height:18px}
/* Task 2 (v2.2): below lg, .st-row-controls shows only on the SELECTED row —
   reusing applySelection()'s own data-selected attr on the <li>
   (.st-row[data-selected], the same state the border-color rule just above
   already keys off — no parallel selection state added here), giving the
   name/description back their width on every OTHER row. The lg+ override
   further down this file (inside @media(min-width:1024px)) restores "always
   visible" there, same as before this task — the enjoy row is unaffected
   either way (it never renders .st-row-controls at all, template: pinned
   rows skip the block entirely — see _step_row.html.erb). Task 3 (round 3):
   this block now holds ONLY the remove button (up/down moved to
   Alt+ArrowUp/Alt+ArrowDown on the row's own opener — see that partial's
   own comment) — the gating itself is unchanged, just narrower now than it
   used to be (one 44px control freed back to the row on every unselected
   row below lg, not three). */
.st-page .st-row-controls{flex:none;display:none;gap:2px}
.st-page .st-row[data-selected] .st-row-controls{display:flex;gap:2px}
.st-page .st-row-btn{display:grid;place-items:center;width:44px;height:44px;border:0;border-radius:8px;
  background:none;color:var(--ink-soft);cursor:pointer}
.st-page .st-row-btn:hover{color:var(--accent);background:var(--surface2)}
.st-page .st-row-btn svg{width:16px;height:16px}
.st-page .st-row-remove:hover{color:#b6412a}

/* ── Per-step fields (name / seconds / why) — hidden until Task 7 opens a
   row; rendered here so the composition is inspectable without JS. ── Task 6
   (fields slice): .st-fields' own grid, .st-field's own base (display only —
   see below), .st-field-wide and .st-field-label all moved into markup.
   .st-field's own flex-direction/gap/min-width stay CSS: .st-field-check/
   -follow override flex-direction to row, and .st-field-note/.st-param-
   fields .st-field both override min-width — converting the base to a
   flex-col/gap/min-w utility would make THOSE overrides lose (a Tailwind
   utility from the base conversion would outrank the still-CSS modifier
   rule, since this sheet's `components` layer never beats `utilities`
   regardless of which one is more specific). */
.st-page .st-field{display:flex;flex-direction:column;gap:4px;min-width:0}
/* A figure note is a sentence, not a number: full row, its own comfortable
   height, and never the 96px .st-field textarea floor below (that one is
   sized for a step's description/why, which are paragraphs). */
.st-page .st-field textarea.st-figure-note{min-height:56px;max-height:160px}
/* Round 4, I2: `select` added here — the "Group:" select
   (_step_fields.html.erb) is the FIRST <select> this rule ever needed to
   cover; without it the browser's own default sizing left it at ~42px,
   under the 44px touch-target floor every other control in this panel
   already clears (studio_sweep.mjs's own assertTouchTargets caught this —
   see assertGroupUiUsable()'s own comment on why its audit had to open a
   populated group's row before it could see this control at all). */
.st-page .st-field input,.st-page .st-field select,.st-page .st-field textarea{border:1px solid var(--border);border-radius:6px;
  background:var(--surface);color:var(--ink);font:500 13.5px "Inter";padding:8px 10px;min-height:44px}
/* Task 1 (round 3): description/why grow with their own content — a
   comfortable 3-4 rows to start (min-height, at this font/line-height), a
   generous ceiling before an internal scrollbar takes over rather than the
   box (or the page around it) growing without bound. field-sizing:content
   (Chrome/Edge 123+) is the actual grow instrument — a pure-CSS one, no JS,
   deliberately: it keeps working through every path that changes a
   textarea's VALUE without a real "input" event ever firing (undo/redo, a
   content-locale switch — both go through recipe_studio_controller.js#
   fillRow) and through Task 5's own lg+ reparenting (the node's WIDTH
   changes when parkFieldsInInspector moves it; layout recomputes on that
   regardless of who moved the node or why — no resize hook to wire either
   way). Browsers without support (older Safari/Firefox at time of writing)
   simply keep today's fixed-min-height, manually-resizable textarea — a
   strict improvement-or-nothing fallback, never a broken one. resize:
   vertical stays either way as the manual escape hatch; max-height still
   caps how far a drag (or the auto-grow itself) can go before scrolling
   takes over. width:100% is explicit here (rather than relying on the flex/
   grid stretch every OTHER .st-field control gets for free) because
   field-sizing:content can otherwise pull the box back to its own
   intrinsic content width on the inline axis too — this pins that axis to
   the grid cell exactly as before, leaving only the block axis (height)
   content-driven. */
.st-page .st-field textarea{width:100%;min-height:96px;max-height:260px;line-height:1.5;resize:vertical;
  overflow-y:auto;field-sizing:content}

/* ── The fields panel's own header — block scene art plus the step's name
   and one-line description. Follow-simplification: this is back in CSS (it
   was utility classes in markup) because the two widths now want genuinely
   different things from it. Below lg the fields hang directly under their
   own collapsed row, which already shows the thumbnail, the name and the
   description — repeating all three inside the form is pure duplication, so
   the header is DISPLAY:NONE there. At lg+ the same node is parked in the
   right rail with no row above it to say which step is open, so it becomes
   the inspector's detail card (see the .st-rail-right block further down).
   Task 6 (Step 7, prune): the "editing: …" language marker this header's
   own comment used to describe, .st-lang-marker, is dead — Round 5 Task 2
   removed its one producer from studio.html.erb (the active language pill
   in the topbar carries that state now), leaving the class with zero
   producers app-wide; a prior review had already flagged it as an orphan.

   `> .st-fields-head`, NOT a bare descendant match: the LEFT rail's own
   grinder-clicks panel (.st-grinder-clicks) reuses this same class for its
   own "Clicks by grinder" header, and an unscoped display:none hid that too
   (caught by styles_snapshot.mjs). The child combinator pins this to the
   step form's own head, which is always a direct child of .st-fields. */
.st-page .st-fields > .st-fields-head{display:none}

/* Task 3 (round 3), fix round 1 — WCAG 2.5.7: the inspector's own "Move
   earlier"/"Move later" buttons (_step_fields.html.erb's own comment has
   the full reasoning) — full-word labels side by side, each its own 44px
   touch target in both dimensions (same floor every other control on this
   page holds itself to), disabled styling matching .st-undo/.st-redo's own
   [disabled] rule elsewhere in this file (dimmed, not-allowed cursor) rather
   than a third, slightly different "disabled" look. Task 6 (fields slice):
   moved into markup, hover/disabled states via Tailwind's own hover:enabled:/
   disabled: variants. */

/* A param's own note — a single-line aside, not a second full-width field;
   reuses .st-field's own input styling above. */
.st-page .st-field-note{flex:1 1 160px;min-width:140px}
.st-page .st-field-note input{font-size:12.5px}

/* Task 3c (round 3): per-grinder click suggestions — nested inside
   .st-figures-fields' own grid (base rule above), spanning both columns
   the same way .st-fields-head/.st-param-fields already do. A short,
   capped (5) repeatable list — its own dashed top rule visually separates
   it from the figure notes above, same idiom .st-fields itself uses to
   separate from the row head it hangs off. Task 6 (fields slice):
   .st-grinder-clicks/-list/-row/-box/-text and .st-add-grinder-click all
   moved into markup — none of them share a property with any other rule
   touching the same element. */
/* ── Per-block EDITABLE params (Task 8) — from_frac/to_frac/grams/temp/…,
   built either by _step_fields.html.erb (first paint) or
   recipe_studio_controller.js#renderParamFields (every rebuild after).
   Follow-simplification: a VERTICAL STACK of .st-field's now, not a
   flex-wrapping row — the whole form is one column, and a 360-420px rail
   had nothing to gain from two 110px params sitting side by side. flex:none
   (not the old `1 1 110px`) matters in a column container: a grow factor on
   this axis is a grow factor on HEIGHT, which would stretch every param row
   to fill whatever space the panel has left. ── */
.st-page .st-param-fields .st-field{flex:none;min-width:0;width:100%}
/* A Brew::Params::FORCED_FOLLOW param — dose/yield/water/temp — is not
   editable here at all: it renders as a label over a plain value line
   ("36 g — from recipe yield") rather than an input. min-height matches the
   44px floor every real control on this page holds to, so a form mixing
   read-only and editable rows keeps an even rhythm. */
.st-page .st-field-readonly{display:flex;flex-direction:column;gap:4px;min-height:44px;justify-content:center}
.st-page .st-field-readonly-value{font:600 13.5px "Inter";color:var(--ink-soft)}
/* A checkbox field ("Pulse pours", "On scale") reads as one row, not a
   column — the checkbox itself never needs 44px of its own height, the
   LABEL it sits inside already carries that touch target. */
.st-page .st-field-check{flex:1 1 100%;flex-direction:row;align-items:center;
  gap:8px;min-height:44px}
.st-page .st-field-check input{width:20px;height:20px;min-height:0;padding:0}

/* The placeholder Sortable leaves at the drop target while a row is
   mid-drag (forceFallback: true, so this is a real class toggle applied to
   the dragged row's own clone, not a native-drag pseudo-element). */
.st-page .st-row-ghost{opacity:.4;background:var(--surface2)}

/* ── Round 4: authorable step groups — the header row + its member rows'
   own indent. A header is a plain sibling <li> of every .st-row in the SAME
   .st-list (recipe_studio_controller.js's own "flat list, membership
   recomputed on drop" architecture — see onDragEnd()'s own comment for why
   NOT nested Sortable instances), so it reuses .st-list's own flex-column
   gap rather than needing a nested list container of its own. A left accent
   border (--accent, the same colour .st-row[data-selected] already uses for
   "this is the thing you're working on") is the whole "container holding
   consecutive rows" idiom this studio needs — Task 2's own reader
   deliberately skipped a colour swatch on ITS text-only section headers for
   the same "the rows already name themselves, a second colour-coding
   surface is redundant" reasoning (see that task's own report); a small
   indent on member rows below is what actually visualises the bracket. ── */
.st-page .st-group-header{border:1px solid var(--border);border-left:3px solid var(--accent);
  border-radius:10px;background:var(--surface2)}
.st-page .st-group-header[data-group-tone="prep"]{border-left-color:var(--ph-prep)}
.st-page .st-group-header[data-group-tone="brew"]{border-left-color:var(--ph-brew)}
.st-page .st-group-header[data-group-tone="milk"]{border-left-color:var(--ph-milk)}
.st-page .st-group-header[data-group-tone="serve"]{border-left-color:var(--ph-serve)}
/* Round 5 Task 3: slim one-line header — label removed, everything on a
   single row, still >=44px touch targets. */
.st-page .st-group-header-row{display:flex;flex-wrap:nowrap;align-items:center;gap:8px 10px;min-height:44px;padding:6px 10px}
.st-page .st-group-name-wrap{display:flex;flex:1 1 160px;min-width:140px;align-items:center}
.st-page .st-group-name-input{border:1px solid var(--border);border-radius:6px;background:var(--surface);
  color:var(--ink);font:600 13.5px "Inter";padding:6px 8px;min-height:44px;width:100%}
/* Only ever shown for a container with nothing dragged into it yet — see
   studio_group_member_counts'/groupMemberCount()'s own comment. */
.st-page .st-group-empty-hint{font:500 11.5px "Inter";color:var(--ink-soft)}
.st-page .st-group-sum{flex:none;font:600 12px "JetBrains Mono",monospace;color:var(--accent-2)}
/* .st-group-follow (the group-level "amounts follow the recipe" toggle) used
   to sit here. Gone with its two siblings — Brew::Params::FORCED_FOLLOW locks
   those params outright, so there is no follow state left to bulk-apply. The
   group's SCALE toggle above keeps its own .st-row-btn styling. */
/* Round 5 Task 3: overview-strip tiles share the group's cycled tone via
   a 3px top band (the same palette the timeline segments use). */
.st-page .bt-ov-tile{position:relative}
.st-page .bt-ov-tile.gt-prep,.st-page .bt-ov-tile.gt-brew,
.st-page .bt-ov-tile.gt-milk,.st-page .bt-ov-tile.gt-serve{padding-top:3px}
.st-page .bt-ov-tile::after{content:"";position:absolute;left:0;right:0;top:0;height:3px;border-radius:0 0 2px 2px;opacity:0}
.st-page .bt-ov-tile.gt-prep::after{background:var(--ph-prep);opacity:1}
.st-page .bt-ov-tile.gt-brew::after{background:var(--ph-brew);opacity:1}
.st-page .bt-ov-tile.gt-milk::after{background:var(--ph-milk);opacity:1}
.st-page .bt-ov-tile.gt-serve::after{background:var(--ph-serve);opacity:1}
/* Round 5: the group's own scale toggle — same 44x44 icon-button idiom,
   with .is-on marking the active state (accent colour + tint background,
   the same "this is engaged" treatment .bt-cell.is-edited and the follow
   indicators already use elsewhere in this file). */
.st-page .st-group-delete{flex:none}
.st-page .st-group-scale{flex:none}
.st-page .st-group-scale.is-on{color:var(--accent);background:var(--accent-tint-06)}
.st-page .st-group-scale svg{width:16px;height:16px}
/* Round 5: a step's own On-scale control sits beside the follow row (same
   st-field-check shape), and its "Auto" pill — the third state of the
   tri-state, ALWAYS visible, highlighted (is-on) while the step has no
   explicit flag — resets to the auto derivation on click. */
.st-page .st-step-scale{flex:1 1 auto}
.st-page .st-scale-auto{flex:none;align-self:center;min-height:44px;padding:4px 10px;border:1px dashed var(--border);
  border-radius:999px;background:none;color:var(--ink-soft);cursor:pointer;
  font:600 10px "JetBrains Mono",monospace;letter-spacing:.06em;text-transform:uppercase}
.st-page .st-scale-auto:hover{color:var(--accent);border-color:var(--accent)}
.st-page .st-scale-auto.is-on{color:var(--accent);border-color:var(--accent);background:var(--accent-tint-06)}
/* A grouped row's own indent — the member half of the header's own left
   accent border above; modest (14px) on purpose, this has to hold at 390px
   with no horizontal scroll (studio_sweep.mjs's own first-viewport budget). */
.st-page .st-row-grouped{position:relative;margin-left:14px}
/* Round 5 Task 3: the member row's own 3px tone band, coloured from the
   same cycled palette as its group header and the timeline. */
.st-page .st-row-grouped::before{content:"";position:absolute;left:-14px;top:0;bottom:0;width:3px;border-radius:0 2px 2px 0}
.st-page .st-row-grouped[data-group-tone="prep"]::before{background:var(--ph-prep)}
.st-page .st-row-grouped[data-group-tone="brew"]::before{background:var(--ph-brew)}
.st-page .st-row-grouped[data-group-tone="milk"]::before{background:var(--ph-milk)}
.st-page .st-row-grouped[data-group-tone="serve"]::before{background:var(--ph-serve)}

/* ── Add a step + total ──
   Task 6 (step-list slice): .st-add/.st-add-box's own base sizing and
   .st-total all moved into markup. .st-add-box input stays CSS: the
   combobox's own <input> here still leans on it for min-height, same
   dependency documented on the rail's own .st-rail-field input rule. */
.st-page .st-add-box input{min-height:44px}
/* Round 4: "add a group" — the same look "Add a grinder" already
   established (.st-add-grinder-click, below) for a secondary, non-primary
   add affordance, kept as its own class rather than reusing that selector
   (two unrelated features sharing one name would coincidentally break
   together the moment either one's own look needs to diverge). */
.st-page .st-add-group-btn{min-height:44px;padding:0 14px;border:1px solid var(--border);
  border-radius:8px;background:var(--surface);color:var(--ink);font:600 12.5px "Inter";cursor:pointer}
.st-page .st-add-group-btn:hover:not([disabled]){color:var(--accent);border-color:var(--accent)}
.st-page .st-add-group-btn[disabled]{opacity:.4;cursor:not-allowed}
/* "Add your own step", beside "Add group" — the same outlined shape, because
   the two are peers in the same row of add-affordances and a second visual
   weight there would read as a hierarchy that isn't one. Shares the rule
   rather than duplicating it; if either ever needs to diverge, split then. */
.st-page .st-add-custom-btn{min-height:44px;padding:0 14px;border:1px solid var(--border);
  border-radius:8px;background:var(--surface);color:var(--ink);font:600 12.5px "Inter";cursor:pointer}
.st-page .st-add-custom-btn:hover{color:var(--accent);border-color:var(--accent)}

/* ── Figures-into-card round (superseded below — kept here for the border/
   padding box it still explains): .st-brew-card used to be the bordered
   "Brew with us" card holding figures line, figures-fields, THEN the
   preview details, all three stacked in normal block flow (margin-top on
   each child after the first doing the spacing, same idiom .st-copy already
   uses). Pinned-context round: the figures line and its notes panel moved
   OUT, into .st-pinned (studio.html.erb's own comment on that div has the
   full "why") — .st-brew-card now wraps the player alone, its ONLY child
   .st-preview-toggle, so there's no more spacing between siblings to manage
   here. It still carries the border/background/radius/overflow:hidden
   .st-preview-toggle used to carry below lg on its own (see that rule's own
   comment, just below) — one card, one border, around the player, not two
   different shapes depending on viewport.

   padding:0 8px below lg is real height even with a single collapsed-
   details child (the below-lg first-viewport budget studio_sweep.mjs's own
   assertStepListInFirstViewport guards is still real — it just has far less
   riding on this card now that the figures line pays into it from
   .st-pinned instead). lg+ gets a roomier inset back (below) — that budget
   was never real there (the shell's own fixed 100dvh height has slack, see
   the shell's own grid-template-rows comment, further down this file). */
.st-page .st-brew-card{border:1px solid var(--border);border-radius:10px;background:var(--surface);
  overflow:hidden;padding:0 8px}
/* One frame, not two (user's call): the player partial ALWAYS renders its
   own .brew-timeline.card chrome — dashed border, radius, shadow, its own
   padding (brew_timeline.css:136) — which is the ONLY frame on the public
   pages where no wrapper exists. Here .st-brew-card already supplies that
   frame, so the inner one is a redundant second border inset inside the
   first. Neutralize it exactly the way the embed context already does
   (.bt-embed .brew-timeline.card, brew_timeline.css:930): the OUTER card
   keeps the border/background/radius, the inner keeps nothing but its own
   content. */
.st-page .st-brew-card .brew-timeline.card{border:0;background:none;box-shadow:none;
  border-radius:0;padding:0}
/* Item 3 (studio-polish round): the card header row — grinder picker + GRIND
   readout beside the figures line, all wrapping together as ONE row, the
   same single-row shape the public method page's own "Brew with us" card
   renders. .bt-facts (the figures line proper) already carries its own
   display:flex/flex-wrap/gap/padding:0 unscoped, straight off brew_timeline.
   css (no .brew-timeline ancestor needed for THAT rule — see this file's own
   comment further down, "── The figures line") — nested here as one flex
   ITEM of .st-figures-topbar rather than re-flattened into it, so the
   figures line keeps wrapping its own cells independently of whether the
   grinder cell beside it is even present (a method with no GrindSetting
   rows at all, or the blank studio, renders no .st-figures-grinder content
   beyond its own <div> — see studio.html.erb's own comment).

   .st-figures-grinder itself starts `display:none` here — BELOW lg only,
   re-enabled inside @media(min-width:1024px) further down this file — a
   deliberate width-dependent exception to "make the studio's header row the
   SAME single row as the method page's own card" (this round's own brief):
   the below-lg first-viewport budget (studio_sweep.mjs's own
   assertStepListInFirstViewport, 390x844) measured this row directly
   against a live server and it does NOT fit — the grinder label + a full
   44px-tall combobox + a two-line grind readout, wrapping onto their own
   line below the already-tight ratio/dose/yield/temp/time row, pushed the
   tightest combination (vi/us) from ~837px to ~922px against an 844px
   ceiling, ~78px over. lg+ has no such ceiling (the shell's own fixed
   100dvh height has slack, same reasoning .st-pinned's own comment above
   already leans on) — this is the ONE piece of Item 3 that doesn't survive
   the merge below lg unchanged; see this round's own report. The figures
   line itself (ratio/dose/yield/temp/time) is untouched below lg — exactly
   as tight as it was before this round. Pinned-context round: this row's
   own ancestry moved from .st-brew-card to .st-pinned; its own layout is
   untouched. */
.st-page .st-pinned>.st-figures-topbar{display:flex;flex-wrap:wrap;align-items:stretch;gap:8px 10px}
.st-page .st-figures-grinder{display:none;align-items:center;gap:8px;flex:1 1 220px;min-width:0}
/* Task 6 (Step 7, prune): .st-figures-grinder-lbl (a label beside the
   grinder combobox that used to sit here) is dead — the picker itself
   moved to the left rail's own GRINDER group (studio.html.erb's own
   comment on .st-figures-grinder, just above); this label's own producer
   moved with it, without carrying this class name along. */
/* A NEW combobox identity (studio.html.erb's own comment on why — disjoint
   from .st-method-box/.st-grinder-click-box), sized the same 44px-floor way
   .st-method-box's own input already is (that rule's own comment). */
.st-page .st-grinder-box{flex:1 1 auto;min-width:0;max-width:15rem}
.st-page .st-grinder-box input{min-height:44px}
.st-page .st-figures-topbar .bt-facts{flex:1 1 480px;min-width:0}
/* Pinned-context round: was .st-brew-card>.st-figures-fields — this node's
   own parent moved to .st-pinned along with the header row it belongs to
   (studio.html.erb's own comment on .st-pinned). Still margin-top only
   (spacing from the header row above it, when it's visible), 6px below lg /
   12px at lg+ — unchanged values, same first-viewport-budget reasoning
   .st-pinned's own comment (top of this file's below-lg -20px rule) already
   covers for that band; lg+ has the same slack .st-figures-grinder's own
   comment above leans on. */
.st-page .st-pinned>.st-figures-fields{margin-top:6px}
@media (min-width:1024px){
  .st-page .st-brew-card{padding:16px 20px}
  .st-page .st-pinned>.st-figures-fields{margin-top:12px}
  /* Item 3: the grinder picker + GRIND readout re-appear here, lg+ only —
     see .st-figures-grinder's own base (below-lg) comment, further up this
     file, for why they start `display:none` at all. flex:0 1 auto, not the
     below-lg 220px basis: the readout is two short lines ("fine / 200-300
     µm"), and every px it doesn't claim is a px the figures line beside it
     can use to stay on ONE row (the compact-cell block just below). */
  .st-page .st-figures-grinder{display:flex;flex:0 1 auto}
  .st-page .st-figures-topbar .bt-facts{flex:1 1 auto}
  /* Compact cells at lg+ (user's call — "make the input params 1 row, same
     as the method page"): the exact shape brew_timeline.css's own
     .brew-timeline-scoped instruments already render on the public pages —
     ~100px-min cells, a 20px input with no 44px floor, and two 24x24
     steppers STACKED at the cell's right edge instead of the below-lg 44x44
     side-by-side pair. Desktop is mouse-first (studio_sweep.mjs's own lg+
     rail-fields comment makes the same call for 40px rail inputs), and
     24x24 still clears WCAG 2.5.8's own minimum target size — the sweep's
     collectTargets() exempts these two buttons at lg+ and asserts the 24px
     floor instead. The width this frees (188px min + 92px of stepper
     padding per cell) is what lets GRIND + RATIO + DOSE + YIELD + TEMP +
     TOTAL TIME share one row from ~1440px up; narrower lg+ widths still
     wrap, exactly as the method page's own topbar does. */
  .st-page .bt-cell{min-width:100px;min-height:0;padding:6px 30px 7px 9px;justify-content:flex-start}
  .st-page .bt-cell-field{align-items:baseline}
  .st-page .bt-cell-input{min-height:0}
  .st-page .bt-read{padding:6px 4px 7px}
  .st-page .bt-spin{flex-direction:column;gap:0;transform:translateY(-40%)}
  .st-page .bt-spin button{width:24px;height:24px;border:0;border-radius:4px;background:none}
  .st-page .bt-spin button:hover{color:var(--accent);border-color:transparent;background:var(--accent-tint-06)}
  .st-page .bt-spin svg{width:11px;height:11px}
}

/* ── Preview pane: collapsed behind a native <details> below lg, always
   open (and its summary hidden) at lg+. No border/background of its own any
   more (figures-into-card round) — .st-brew-card, above, carries that now;
   this element is just the disclosure mechanism around the player. ── */
.st-page .st-preview-toggle{border-top:1px dashed var(--border)}
.st-page .st-preview-summary{list-style:none;cursor:pointer;display:flex;align-items:center;
  min-height:44px;padding:0 2px;font:600 13.5px "Inter";color:var(--ink)}
.st-page .st-preview-summary::-webkit-details-marker{display:none}
.st-page .st-preview-summary::before{content:"▸";margin-right:8px;color:var(--ink-soft)}
.st-page .st-preview-toggle[open] .st-preview-summary::before{content:"▾"}
.st-page .st-preview-body{padding:8px 0 0}
@media (min-width:1024px){
  /* flex-shrink:0 moved to .st-brew-card, below — THAT element is the direct
     child of .st-grid's height-constrained flex column now (figures-into-
     card round), not this <details> any more, so it's the one whose
     flex-basis could resolve through a descendant ::details-content box to
     ~0 and get crushed by default shrink. This rule no longer sits inside a
     flex container at all (its parent, .st-brew-card, is plain block flow),
     so there's nothing here left for flex-shrink to do. */
  .st-page .st-preview-toggle{border-top:0;flex-shrink:initial}
  .st-page .st-preview-summary{display:none}
  .st-page .st-preview-body{display:block!important;padding:0}
  /* display:block above is not enough on its own: a <details> with no
     `open` attribute hides its non-summary content through its own
     ::details-content box (Chrome 131+), which content-visibility:hidden's
     — and skips rendering/painting/hit-testing/the accessibility tree for
     — regardless of what `display` the child itself computes to. There is
     no `open` attribute to flip here (the server renders this <details>
     closed on every paint, on purpose — see the comment at the top of this
     rule block), so the ONE thing that actually has to be overridden at lg+
     is that box itself. block-size:auto undoes the 0-height collapse
     content-visibility:hidden leaves behind once painting is restored. */
  .st-page .st-preview-toggle::details-content{content-visibility:visible!important;block-size:auto!important}
  .st-page .st-brew-card{flex-shrink:0}

  /* Item 3: the player's own two-column .stage grid (brew_timeline.css)
     sizes its FIRST column with a bare `minmax(0,1fr)` track, free to grow
     past .stage-surface's own max-width:560px cap in a ROOMY card (that
     rule's own comment: "1fr of a 1900px card drew a 740px-wide scale with
     a 500px-tall column of white space beside it") — the timeline column
     (.stage-side/.controls, column 2) then starts wherever that now-
     over-wide track happens to end, past where the 560px-capped scene art
     actually stops. Measured directly in the studio's own centre column
     first (700px card at 1440, 284px at 1024 — both well under the width
     .stage-surface's own cap would ever engage at, confirmed via
     getBoundingClientRect: the existing 1fr/1.4fr split already lands
     tight, no void, at either width): the studio's card is narrow enough
     that this bug doesn't fire here TODAY, so `minmax(0,560px)` outright (a
     flat px cap) would have been actively wrong at 1440 — it forced column
     1 to claim 560 of the card's own 608px stage width, leaving column 2
     a 22px sliver, a worse regression than the bug it was meant to fix.
     `minmax(0,min(560px,42%))` instead: 42% reproduces the ORIGINAL 1fr-of-
     2.4fr share (1/2.4 = 41.6%) almost exactly whenever the container is
     narrow enough that the cap never engages (studio's own case, at every
     width this task asks to verify — 1440 and 1024 both included, so this
     is a no-op there, confirmed by re-measuring after), while still
     capping the ABSOLUTE width at 560px the moment a container ever DOES
     get wide enough to reproduce the original bug (matching .stage-
     surface's own cap, one number, not duplicated arbitrarily) — handing
     every pixel beyond that ceiling to column 2's own `1fr` instead of
     stranding it as a gap nobody uses. Scoped to the studio's OWN embedded
     preview (.st-brew-card .stage) rather than changed globally: brew_
     methods/show, drinks/show, /play, /embed and /r/:token all share this
     same .stage class on cards sized nothing like the studio's own centre
     column, and a global change risks regressing five already-shipped
     pages for a gap that (per this task's own brief) is scoped to the
     studio. Scene size (aspect-ratio, not width) and the stacked below-lg
     shape (.stage's own max-width:760px single-column rule) are both
     untouched — this only ever fires here, at lg+. */
  .st-page .st-brew-card .stage{grid-template-columns:minmax(0,min(560px,42%)) minmax(0,1fr)}

  /* Compact-player round (user's call — "make the scale and the timeline
     smaller, it looks way too cramped"): in the editor the embedded player
     is a REFERENCE instrument, not the page's main attraction, so its two
     big visuals get a studio-only diet: the scale scene trades the 440:300
     aspect box (~209px tall at this width) for a fixed 170px (the mounted
     scene SVGs are absolute inset:0 at 100%x100% — they scale into the
     shorter box, never crop), the LCD's padding trims, and the timeline
     strip tightens — 16px track (was 22), 16px axis (was 20), slimmer
     chips (22px, was 26) with smaller gaps. lg+ only: below lg the same
     player renders inside the collapsible Preview details where the
     taller, touch-sized controls stay the right call. All scoped to
     .st-brew-card — brew_methods/show, drinks/show, /play, /embed and
     /r/:token share these classes and keep their full sizes. Everything
     this shrinks lives inside .st-preview-body, which the sweep's own
     touch-target collector already exempts (the player is measured on the
     public pages, not here), so no 44px promise is broken either. */
  .st-page .st-brew-card .stage{gap:16px;row-gap:8px;margin-top:6px;padding-top:10px}
  .st-page .st-brew-card .stage-visual{aspect-ratio:auto;height:170px}
  .st-page .st-brew-card .bt-scale-lcd{padding:7px 8px 8px}
  .st-page .st-brew-card .tl-phases{margin-bottom:4px}
  .st-page .st-brew-card .tl-track{height:16px}
  .st-page .st-brew-card .tl-axis{height:16px}
  .st-page .st-brew-card .tl-strip{margin-bottom:8px}
  .st-page .st-brew-card .tl-steps{gap:5px;margin-top:6px}
  .st-page .st-brew-card .tl-chip{min-height:22px;padding:3px 8px}
  .st-page .st-brew-card .tl-chip-lbl{font-size:11.5px}
}

/* ── Task 1 (v2.2): below lg, THE RECIPE / THE BEAN / THE PHOTOS reach a
   phone as collapsible sections in the flow — the SAME <details> the lg+
   rail already renders (studio.html.erb), never a second copy of any
   field. .st-rail-left switches from display:none (further down this
   file) to a normal block right here, at the top of the single column;
   .st-rail-right (the lg+ inspector) stays hidden below lg exactly as
   before. Every group starts CLOSED down here regardless of which ones the
   server rendered `open` for lg+'s own default (collapsed-by-default is
   the spec's own load-bearing constraint — recipe_studio_controller.js#connect
   closes them, once, on connect; see its own comment for why a tiny JS
   pass is the ONE mechanism this rewrite leans on rather than a second,
   CSS-only one). Sized for touch (44px) throughout, reusing the exact
   class vocabulary the lg+ block further down this file already defined
   for this SAME markup — this is deliberately not a copy of those
   declarations: the lg+ overrides (280px track, 40px fields, column
   layout, non-touch media controls) still live in their own @media block
   below and win there purely on source order, so none of this ever
   touches desktop's own byte-identical rendering. */
.st-page .st-rail-left{display:block;margin-bottom:20px}
.st-page .st-rail-group{border-bottom:1px solid var(--border)}
.st-page .st-rail-group:last-child{border-bottom:0}
.st-page .st-rail-group>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;
  min-height:44px;padding:12px 2px;font:700 11px "JetBrains Mono",monospace;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-soft)}
.st-page .st-rail-group>summary::-webkit-details-marker{display:none}
.st-page .st-rail-group>summary::before{content:"▸";color:var(--ink-soft)}
.st-page .st-rail-group[open]>summary::before{content:"▾"}
/* Task 6 (rail slice): .st-expiry-hint, .st-rail-body/-row/-field/-label/
   -hint and .st-method-box's own base sizing all moved into markup as
   Tailwind utilities (see studio.html.erb) — every one of them was either a
   flat box/typography rule with no responsive twin, or (like .st-method-box)
   had a BYTE-IDENTICAL lg+ copy further down this file, so no lg: variant
   was needed either. The class names themselves stay on every element —
   styles_snapshot.mjs/studio_sweep.mjs select by these names — only their
   declarations moved. .st-rail-field's own `input,select,textarea` rule
   just below is NOT part of this: shared/ui/_combobox.html.erb's own
   <input> (mounted inside .st-method-box/.st-grinder-box/.st-country-box)
   carries no border/background/font/height of its own — it depends on
   this descendant rule for exactly those properties, not just the
   padding-left the two override rules below exist to fix. Re-tested per
   this task's own brief ("re-test the .st-grinder-box/.st-country-box
   input overrides once the base rule is gone") and found NOT safe to
   remove: deleting this rule would strip the three comboboxes nested in
   the rail of their border, background, colour, font and 44/40px height,
   a real regression the harness would catch. Left as CSS; the two
   padding-left overrides below stay live for the same reason. */
.st-page .st-rail-field input,.st-page .st-rail-field select,.st-page .st-rail-field textarea{
  width:100%;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--ink);
  font:500 14.5px "Inter";padding:10px;min-height:44px}
.st-page .st-rail-field textarea{resize:vertical;min-height:64px}
/* padding-left:38px is the below-lg twin of the lg+ override further down
   this file (@media(min-width:1024px) .st-page .st-method-box input) — same
   fix, same reason: the .st-rail-field input rule just above sets
   padding:10px on every rail input, which (0,2,1) beats the combobox's own
   Tailwind pl-10 and shoves the value text under the search glass icon
   (left-3.5 w-4, ~14px+16px). Without this twin the lg+ rule only ever
   covered >=1024px; below lg the icon-overlap bug was still live (F2,
   v2.2 fix wave). */
.st-page .st-method-box input{min-height:44px;padding-left:38px}
/* The SAME collision, same fix, for the two comboboxes that later joined the
   rail: THE GRINDER's own picker and THE BEAN's country picker. Worth
   spelling out because it bit twice: shared/ui/_combobox.html.erb is
   Tailwind-classed (pl-8 compact / pl-10 default is what reserves room for
   the search glass), the studio is hand-written CSS, and `.st-rail-field
   input` (0,2,1) outranks a utility class (0,1,0) — so every combobox
   dropped into a rail field loses its left padding and paints its icon on
   top of its own placeholder until it gets a line here. 32px, not 38px:
   these two render compact (left-2.5 w-3.5, a smaller glass than the
   method box's). */
.st-page .st-grinder-box input,.st-page .st-country-box input{padding-left:32px}
/* THE RECIPE's own Name field (railTitleSlot in studio.html.erb) — its ONE
   home at every width now, no more topbar/rail reparenting in JS. Below lg
   it reads exactly like every other rail field above, not the old topbar
   heading look (that base rule is gone — the input never renders outside
   a .st-rail-field any more). The lg+ override further down this file
   (`.st-rail-field .st-title-input`, inside @media(min-width:1024px)) is
   untouched and wins there purely on source order, so desktop's own look
   stays byte-identical. */
.st-page .st-rail-field .st-title-input{flex:none;min-height:44px;padding:10px;
  border:1px solid var(--border);border-radius:6px;background:var(--surface);
  font:500 14.5px "Inter";font-weight:500;color:var(--ink)}
.st-page .st-rail-field .st-title-input:hover,
.st-page .st-rail-field .st-title-input:focus{background:var(--surface)}
.st-page .st-rail-field .st-title-input:focus{outline:2px solid var(--accent);outline-offset:1px}

/* THE PHOTOS group's own upload form + gallery grid, reachable below lg
   for the first time too — verified to show the exact same owner-gated
   form / sign-in hint the lg+ rail already shows (studio.html.erb renders
   no new logic for this, just unhides). Buttons here are sized for touch
   (44px) rather than the lg+ block's own smaller, mouse-first sizing
   further down this file — same reasoning and same non-conflict as the
   rail-field input split just above. Task 6 (rail slice): .st-media-errors/
   -form/-banner-preview/-grid/-thumb(+img,+form) all moved into markup —
   every one of them was byte-identical to its lg+ copy further down this
   file, so a plain utility class (no lg: variant) covers both widths.
   .st-media-upload/-remove stay CSS below: their own lg+ copies genuinely
   differ (36px vs 44px, 22px vs 44px icon buttons), a real responsive
   split left for a follow-up slice. */
.st-page .st-media-upload{min-height:44px;padding:0 14px;border-radius:8px;border:1px solid var(--border);
  background:var(--surface);color:var(--ink);font:600 13px "Inter";align-self:flex-start}
.st-page .st-media-upload:hover{border-color:var(--accent);color:var(--accent)}
.st-page .st-media-remove{display:grid;place-items:center;width:44px;height:44px;border-radius:999px;
  border:0;background:rgba(0,0,0,.55);color:#fff;cursor:pointer}
.st-page .st-media-remove:hover{background:rgba(0,0,0,.75)}
.st-page .st-media-remove svg{width:18px;height:18px}

/* ── /brew-studio: the method picker (recipes#studio_home) ── */
.st-page.st-home{max-width:900px}
.st-page .st-home-intro{margin:28px 0 22px;text-align:center}
.st-page .st-home-title{margin:0;font-family:"Source Serif 4",serif;font-weight:700;font-size:28px;
  line-height:1.2;color:var(--ink)}
.st-page .st-home-sub{margin:8px 0 0;color:var(--ink-soft);font:400 14.5px "Inter"}
.st-page .st-home-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px;
  list-style:none;margin:0;padding:0}
/* Task 3b (round 3): equal-height cards. A grid ITEM (this <li>) already
   stretches to the row's own tallest sibling for free (align-items:stretch
   is the grid default) — the bug was that .st-home-card itself, a flex
   column with no height of its own, never actually FILLED that stretched
   <li>, so a longer spec line (Chemex, Vietnamese Phin — see
   .st-home-card-sub below) or a taller-looking thumb still left its own
   card visibly shorter than its row siblings even though the <li> around
   it was already the right height. display:flex here + width:100% on the
   card below is what makes the card fill it (a flex child with no explicit
   height defaults to align-items:stretch on ITS cross axis too, but only
   once its own container is a flex box that's actually IN the taller
   shape). */
/* Phase B: a column, because a method card now has TWO children — the card
   itself and the drink lane beneath it (studio_home.html.erb). No visual
   change for the "Start from scratch" card, which still has one. */
.st-page .st-home-grid>li{display:flex;flex-direction:column}
.st-page .st-home-card{display:flex;flex-direction:column;align-items:center;gap:6px;padding:16px 12px;
  border:1px solid var(--border);border-radius:12px;background:var(--surface);text-align:center;
  text-decoration:none;color:var(--ink);transition:border-color .12s ease,box-shadow .12s ease;width:100%}
.st-page .st-home-card:hover{border-color:var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.06)}
/* Task 3b (round 3): a genuinely FIXED-size box (not width:100% up to a
   max-width, which still varied with the grid column's own current width)
   — every method's scene thumb now draws into the exact same 120x82 frame,
   object-fit:contain never upscaling past it. This doesn't equalise how
   much of ITS OWN viewBox each apparatus's drawing fills (that's the
   scene art's own proportions, out of this task's scope) — it does mean
   the FRAME every card presents that art in is now identical, which is
   what "uniform" means at the picker layer. */
.st-page .st-home-card-art{width:120px;height:82px;object-fit:contain}
.st-page .st-home-card-name{font:600 14px "Inter";color:var(--ink)}
/* Task 3b (round 3): no-wrap + truncate, not "reserve enough space for the
   longest label" — a longer spec line (Chemex's "1:15 · Medium-fine ·
   93°C" vs. Espresso's "1:2 · Fine · 93°C") used to wrap onto a second
   line only for the cards that needed it, growing THEIR row (and, before
   the stretch fix above, only THEIR own card) taller than the rest. One
   line, ellipsised, same as the step list's own truncated description
   subline (Task 3, round 3) — reserving fixed vertical space for a
   worst-case two-line wrap would waste room on every card that doesn't
   need it. */
.st-page .st-home-card-sub{font:500 11.5px "JetBrains Mono",monospace;color:var(--ink-soft);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
/* Task 3b (round 3): "Start from scratch" — distinct and quiet (dashed
   border, no fill change, softer ink) rather than another solid card
   competing with the real methods for attention; the plus glyph fills the
   exact same 120x82 frame every other card's own scene thumb draws into,
   so this card is never a different HEIGHT either, just a different mark
   inside that frame. */
.st-page .st-home-card-blank{border-style:dashed;color:var(--ink-soft)}
.st-page .st-home-card-blank:hover{color:var(--accent)}
.st-page .st-home-card-blank .st-home-card-name{color:inherit}
.st-page .st-home-card-plus{width:120px;height:82px;display:grid;place-items:center}
.st-page .st-home-card-plus svg{width:28px;height:28px}

/* ── Task 5: the lg+ three-pane shell (recipes/studio.html.erb, not the
   /brew-studio picker above) ── Everything ABOVE this point in the file is
   the below-lg stack — a sweep guards it (scripts/verify/studio_sweep.mjs).
   Task 2 (v2.1) deliberately edited PART of that stack (the centre column's
   own reading order — see .st-copy/.st-grid's own comments above); Task 1
   (v2.2) edited another part (the left rail — see its own comment further
   up this file). Neither is byte-identical to v1 any more, on purpose, per
   each task's own brief. Below lg every selector in THIS block, though,
   either targets an element that's hidden by the mobile-first defaults just
   below (so CSS alone keeps the very first paint identical, with no
   dependency on JS having run yet), or lives inside @media
   (min-width:1024px) and simply never matches. */

.st-page .st-brand,
.st-page .st-topbar-divider,
.st-page .st-topbar-tool,
.st-page .st-topbar-spacer,
.st-page .st-close,
/* Task 1 (v2.2): only the RIGHT rail (the lg+ inspector) hides below lg
   now — the left rail unhides further up this file (its own comment), the
   in-flow section stack a phone reaches THE RECIPE/THE BEAN/THE PHOTOS
   through. */
.st-page .st-rail-right{display:none}

@media (min-width:1024px){
  /* Full-bleed: drop the 1100px max-width/centering from the bare .st-page
     rule at the very top of this file (which below-lg still reads
     untouched) and turn the page itself into the shell — a fixed-height
     grid, three columns, FIVE rows: the top bar (auto, spans all three
     columns), the intro copy field (auto, centre column only — Task 2,
     v2.1), the figures line (auto, centre column only), the figures line's
     OWN notes fields (auto, centre column only, hidden/0-height whenever
     "figures" isn't selected — Task 3, v2.1), and everything else (1fr,
     centre column only — its own overflow-y is what actually scrolls; see
     .st-grid below, which now also holds the notes copy field as its own
     third flex child). The two rails span rows 2 THROUGH 5 (they run the
     full height beside the intro field, the figures line (+ its own notes
     row) and the list, matching the design doc's own sketch), each
     `overflow-hidden` with its own `overflow-y-auto` inner scroller
     (.st-rail-scroll) — the same shell/rail split ~/zz/dev/micepad's
     badge_designer_v3/show.html.erb uses. height:100dvh means the PAGE
     itself never scrolls — only these inner regions do. */
  /* :not(.st-home): the method PICKER page shares the .st-page class but is
     an ordinary scrolling page at every width — this fixed-height grid shell
     applied to it once hid every card below the fold with no scrollbar
     (bughunt finding 1). The descendant rules below are harmless there (the
     picker has none of the editor's elements); only this container rule has
     to exclude it. */
  .st-page:not(.st-home){
    max-width:none;margin:0;padding:0;
    /* Task 1 (round 3): the right rail's own track widened 360px -> 420px —
       "wider inspector fields" (that task's own brief) — giving the
       description/why textareas parked in it (.st-fields' grid-column:1/-1
       spans the full track) noticeably more line length to grow-with-content
       into, same reasoning as the auto-grow rules themselves (see
       .st-field textarea's own comment, below-lg section of this file).
       scripts/verify/studio_sweep.mjs's own fixed-track assertion is updated
       to match (deliberately, per that task's own "update any check pinning
       textarea sizes" allowance).

       Item 4 (studio-polish round): FIVE columns, not three — a
       minmax(0,1fr) "gutter" on each outer edge, the three-pane shell
       (280px / capped centre / 420px) in the middle three. 580px is
       1280 - 280 - 420 — the same max-w-7xl (80rem/1280px) content cap
       app/views/layouts/application.html.erb's own <main> uses on every
       OTHER page, so the studio reads as the same product on a wide
       monitor instead of stretching its step rows/card edge to edge. Below
       1280px the two gutters simply shrink toward 0 (minmax's own floor)
       and the centre track — capped, never fixed — shrinks right along
       with them, so nothing changes at all from 1024px up to 1280px: this
       is purely a WIDE-monitor cap, not a narrower shell.

       The topbar, below, still spans `grid-column:1/-1` — now all FIVE
       tracks, gutters included — so it alone stays full-bleed, edge to
       edge, at every width past 1280px: it's app chrome (back/undo/redo/
       save/the language switcher), not page content, and every one of its
       own controls already anchors to one physical edge or the other
       (.st-back on the left, .st-save/.st-close on the right) rather than
       to the content column's own edges — capping it too would have
       floated those controls in from the true edge of the window for no
       reason, the one place in this round where "match the method page
       exactly" was the wrong call (see this round's own report). The
       three-pane shell (left rail / centre / right rail) keeps working
       completely unchanged below this — each one just moved over by one
       grid line (2/3/4 instead of 1/2/3) to make room for the gutters
       flanking them. */
    /* Revised: the cap belongs on the CENTRE track, not on the shell as a
       whole. Capping the total at the site's 1280 made the editor SMALLER
       than it had ever been — the two rails eat 700px the method page
       doesn't have, so the Brew-with-us card landed at 540px against that
       page's own 1232px. 960px here means nothing changes at or below
       ~1660px (the centre just fills the remainder exactly as before, no
       regression on a laptop), and past that the gutters open up so the
       editor stops stretching edge to edge on a wide monitor — which is
       the actual ask. */
    /* Round 5 follow-up: edge-anchored badge-designer layout — left rail
       240px, right rail 360px, centre editor capped at 1100px and centred
       by the two flexible gutters. */
    display:grid;grid-template-columns:240px minmax(0,1fr) minmax(0,1100px) minmax(0,1fr) 360px;
    /* THREE rows, not four: the lead/intro box moved into the rail as THE
       RECIPE's own DESCRIPTION field long ago, so the row it used to occupy
       is gone rather than left empty. Topbar, then row 2, then .st-grid's
       own `1fr`.

       Figures-into-card round (superseded): row 2 used to be the live
       overview strip alone — the figures line and its own figures-fields
       notes lived one row down instead, INSIDE .st-brew-card, itself the
       first child of .st-grid's own row. Pinned-context round: row 2 is now
       .st-pinned (studio.html.erb's own comment on it has the full "why"),
       which holds the figures line + its notes panel + the strip together
       — .st-page>.st-pinned's own rule, below, replaces what used to be
       .st-page>.st-scene-strip's. .st-grid keeps its `1fr` and now holds
       ONLY the player (.st-brew-card) and the step list/notes below it —
       more of that row is scrolling content than before, not less. */
    grid-template-rows:auto auto 1fr;
    height:100dvh;overflow:hidden;
  }
  /* flex-wrap at lg+ so the alpha warning can show its whole sentence: the
     row prefers one line, and only when the controls plus that sentence
     genuinely don't fit does it take a second line — a warning that wraps
     beats one that truncates mid-word. Row 1 is `auto`, so the shell gives
     back whatever height this actually needs. */
  .st-page .st-topbar{grid-column:1/-1;grid-row:1;margin-bottom:0;padding:10px 16px;
    border-bottom:1px solid var(--border);flex-wrap:wrap;row-gap:4px}

  /* Below-lg's own back-arrow-with-text steps aside for the brand | divider
     | "Brew Studio" | spacer | undo/redo | Save | ✕ idiom — undo/redo/save
     are the SAME three <button> elements either way (only their SURROUNDING
     chrome swaps), so recipe_studio_controller's own targets/actions never
     have to know which shape is currently showing. Task 1 (v2.2): the title
     input carries no top-bar presence to vacate any more — it renders only
     in THE RECIPE's own railTitleSlot (studio.html.erb), at every width, so
     there's nothing here to hide/reparent and no empty flex space to worry
     about either. Task 6 (chrome slice): .st-back's own display:none here
     moved to `lg:hidden` directly on the element, alongside its below-lg
     styling. */
  .st-page .st-brand,.st-page .st-topbar-tool{display:inline-flex;align-items:center}
  .st-page .st-topbar-divider{display:block;width:1px;height:22px;background:var(--border);flex:none}
  .st-page .st-topbar-tool{color:var(--ink-soft);text-decoration:none;font:600 14px "Inter"}
  .st-page .st-topbar-tool:hover{color:var(--accent)}
  .st-page .st-topbar-spacer{display:block;flex:1}
  .st-page .st-close{display:grid;place-items:center;flex:none;width:44px;height:44px;border-radius:8px;
    color:var(--ink-soft);text-decoration:none}
  .st-page .st-close:hover{color:var(--accent);background:var(--surface2)}
  .st-page .st-close svg{width:18px;height:18px}

  /* Studio-alpha round: the ALPHA chip's lg+ shape — there's no ~16px
     ceiling up here the way there is below lg (this topbar row spans a
     1024px+ shell with abundant width either side of the spacer's flex:1),
     so the chip can afford BOTH a visible short label (.st-alpha-label,
     "Alpha") and its note (.st-alpha-note) in FULL. The note used to be
     capped at 320px with text-overflow:ellipsis, which cut the sentence
     mid-word ("…saved recipes may n…") — a warning nobody can finish
     reading does not warn anyone, so the cap and the ellipsis are both
     gone and the row wraps instead (see .st-topbar's own lg+ rule). Row 1
     of the shell is `auto`, so a wrapped second line costs only the height
     it actually needs. */
  .st-page .st-alpha-chip{gap:6px}
  .st-page .st-alpha-icon{display:inline-block}
  .st-page .st-alpha-label{padding:2px 7px}
  /* The whole sentence, never an ellipsis: a warning the visitor can only
     half-read is worse than no warning. No max-width and no truncation —
     the note is `flex:none` inside a row that can wrap (see .st-topbar's
     own lg+ rule), so when the row runs out of horizontal room the chip
     and its sentence move down together and stay legible instead of
     silently cutting mid-word. Accent orange, not the old gold: this is a
     caution, and the gold read as decoration. */
  .st-page .st-alpha-note{display:inline-block;flex:none;font:500 12px "Inter";color:var(--accent)}

  /* Lang-topbar round: the SAME .st-lang-bar node (studio.html.erb's own
     .st-lang-widget comment has the full "why this is one DOM, not two")
     drops its below-lg popover shape entirely up here. display:contents on
     the wrapper removes THAT div's own box from the flex layout, so
     .st-lang-bar becomes a direct flex child of .st-topbar itself — right
     after "Brew Studio" and before the spacer (studio.html.erb's own
     source order — Item 2 moved the ALPHA chip from this same "before the
     spacer" slot to AFTER it instead, so this bar no longer shares it with
     the chip, just with the brand/divider/tool group), "the obvious slot"
     with abundant width either side of the spacer's own flex:1 at this
     shell width. The trigger that opens the
     popover below lg has nothing left to do up here (menu#toggle never even
     runs — nothing about the panel's own [hidden] state has to be
     reconciled on a resize either way, because nothing ever MOVES between
     two different DOM parents, only how this one already-in-place node
     PAINTS — see studio.html.erb's own comment for the full reasoning), so
     it simply disappears. [hidden] is overridden explicitly rather than
     left to the browser default: the panel starts server-rendered `hidden`
     (below lg's own closed-by-default popover), and a visitor who lands
     directly at lg+ (never once resizing down) should never depend on JS
     having toggled that attribute off to see their own pills.

     !important, inside @layer base — both load-bearing, confirmed against
     the live cascade (chrome devtools' own computed-style panel, not a
     guess): Tailwind v4's own preflight ships `[hidden]:where(:not(...))
     {display:none!important}` INSIDE its `@layer base` (see the compiled
     tailwind-*.css — search "[hidden]"). Per the CSS Cascade Layers spec,
     !important declarations invert the normal layer order: a NAMED layer
     always outranks the UNLAYERED bucket this file's every other rule lives
     in, so a plain unlayered `!important` here — even at higher specificity
     — still loses to Tailwind's own [hidden] reset. Joining the SAME "base"
     layer name (cascade layers merge by name across stylesheets) puts this
     rule in the same tier as Tailwind's, where ordinary specificity decides
     again — `.st-page .st-lang-bar[hidden]` outranks Tailwind's own
     `[hidden]:where(...)` (:where() always carries zero specificity) — so
     it wins there. Every property here still needs its own !important
     regardless of the layer: without it, "important beats normal
     regardless of layer" would flip the OTHER way, and this whole rule
     would lose to this file's own unlayered, normal-weight below-lg
     .st-lang-bar rule above (unlayered outranks any named layer for
     NORMAL declarations — the exact opposite of the important case). */
  @layer base{
    .st-page .st-lang-widget{display:contents!important}
    .st-page .st-lang-trigger{display:none!important}
    .st-page .st-lang-bar,
    .st-page .st-lang-bar[hidden]{
      display:flex!important;flex-direction:row!important;flex-wrap:nowrap!important;
      align-items:center!important;gap:8px!important;
      position:static!important;top:auto!important;left:auto!important;z-index:auto!important;
      min-width:0!important;max-width:none!important;max-height:none!important;
      overflow-y:visible!important;overflow-x:auto!important;
      padding:0!important;border:0!important;border-radius:0!important;
      background:none!important;box-shadow:none!important;
    }
  }

  /* The two rails — `overflow-hidden` shells with their own
     `overflow-y-auto` inner scroller (.st-rail-scroll), so a long bean card
     or a long step list only ever grows its OWN column, never the page.
     grid-row 2/5 spans every row below the top bar (intro/overview-strip/
     grid — Figures-into-card round shrank this back from 2/7 to 2/5 when
     the figures line and its own figures-notes row folded into .st-grid's
     own row instead of reserving two of their own above it; see the
     shell's own grid-template-rows comment above). */
  .st-page .st-rail{display:flex;flex-direction:column;grid-row:2/4;min-height:0;overflow:hidden}
  /* margin-bottom:0 undoes the below-lg .st-rail-left rule's own 20px gap
     (top of this file — spacing between the in-flow section stack and
     whatever follows it in the single-column layout). Left unreset here,
     that 20px shortened the left rail's own border-right by exactly that
     much inside this grid cell, the one visible desktop regression a
     below-lg-only rule change should never cause (F1, v2.2 fix wave) — the
     right rail carries no such margin and always reached the bottom. */
  /* Rails sit at the outer edge tracks of the edge-anchored grid. */
  .st-page .st-rail-left{grid-column:1;border-right:1px solid var(--border);margin-bottom:0}
  .st-page .st-rail-right{grid-column:5;border-left:1px solid var(--border)}
  .st-page .st-rail-scroll{flex:1;min-height:0;overflow-y:auto;padding:8px 16px 20px}

  /* Collapsible group headers — one <details> per group (studio.html.erb),
     all but THE BEAN open by default. Native disclosure triangles, same
     ▸/▾ idiom the preview toggle above already uses. */
  /* Task 6 (rail slice): .st-rail-body/-row/-field/-label/-hint and the
     media block's box/typography rules (errors/form/banner-preview/grid/
     thumb/thumb-img/thumb-form) moved into markup — this lg+ copy of each
     was byte-identical to its below-lg base rule (top of this file), so a
     single plain utility class covers both widths already; only the pseudo-
     element disclosure triangles and the two genuinely-responsive rules
     (.st-rail-field's own input/select/textarea box, .st-media-upload/
     -remove) stay CSS below. */
  .st-page .st-rail-group{border-bottom:1px solid var(--border)}
  .st-page .st-rail-group:last-child{border-bottom:0}
  .st-page .st-rail-group>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;
    min-height:44px;padding:12px 2px;font:700 11px "JetBrains Mono",monospace;letter-spacing:.08em;
    text-transform:uppercase;color:var(--ink-soft)}
  .st-page .st-rail-group>summary::-webkit-details-marker{display:none}
  .st-page .st-rail-group>summary::before{content:"▸";color:var(--ink-soft)}
  .st-page .st-rail-group[open]>summary::before{content:"▾"}
  .st-page .st-rail-field input,.st-page .st-rail-field select,.st-page .st-rail-field textarea{
    width:100%;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--ink);
    font:500 13.5px "Inter";padding:8px 10px;min-height:40px}
  .st-page .st-rail-field textarea{resize:vertical;min-height:64px}
  .st-page .st-media-upload{min-height:36px;padding:0 14px;border-radius:8px;border:1px solid var(--border);
    background:var(--surface);color:var(--ink);font:600 13px "Inter";align-self:flex-start}
  .st-page .st-media-upload:hover{border-color:var(--accent);color:var(--accent)}
  .st-page .st-media-remove{display:grid;place-items:center;width:22px;height:22px;border-radius:999px;
    border:0;background:rgba(0,0,0,.55);color:#fff;cursor:pointer}
  .st-page .st-media-remove:hover{background:rgba(0,0,0,.75)}
  .st-page .st-media-remove svg{width:12px;height:12px}

  /* THE RECIPE's own Name field, restyled here to read as a rail control at
     lg+ (40px fields, tighter padding, no bold serif) — the mobile-first
     twin of this same selector further up this file (Task 1, v2.2) styles
     it for touch (44px) below lg instead; both target the SAME rendered
     node (studio.html.erb renders it once, in this one spot, at every
     width now — no reparenting left to document here). This rule wins at
     lg+ purely on source order (equal 3-class specificity, later in the
     file), leaving the mobile-first one to apply everywhere else. */
  .st-page .st-rail-field .st-title-input{flex:none;min-height:40px;padding:8px 10px;
    border:1px solid var(--border);border-radius:6px;background:var(--surface);
    font:500 13.5px "Inter";font-weight:500}
  .st-page .st-rail-field .st-title-input:hover,
  .st-page .st-rail-field .st-title-input:focus{background:var(--surface)}
  .st-page .st-rail-field .st-title-input:focus{outline:2px solid var(--accent);outline-offset:1px}

  /* .st-method-box's own max-width:none/width:100% (Task 6 rail slice)
     moved into markup — identical to its below-lg copy, top of this file. */
  /* The rail-field input rule above resets padding to 8px 10px, which shoves
     the combobox's absolutely-positioned search glass (left-3.5, w-4) under
     the value text — this wins by source order and clears it. */
  .st-page .st-method-box input{min-height:40px;padding-left:38px}
  .st-page .st-grinder-box input,.st-page .st-country-box input{padding-left:32px}

  /* The right rail — the inspector. Either the empty-state hint
     (server-rendered, restored by recipe_studio_controller.js's own
     updateInspectorEmptyState) or, once a step is selected, that row's OWN
     .st-fields node — reusing .st-fields' existing rules unscoped from any
     particular row width, the same markup rendering identically whether
     it's inline (below lg) or parked here. */
  .st-page .st-inspector-empty{margin:0;font:500 13.5px "Inter";color:var(--ink-soft)}

  /* The inspector's detail card — the ONE place .st-fields-head is visible
     (it's display:none everywhere else; see that rule's own comment near the
     top of this file). Below lg the fields sit under their own collapsed
     row, which already names the step; parked in this rail they have no such
     context, so the header earns its place here and only here. Read-only by
     construction — the Name and Description INPUTS are a few rows below it,
     and recipe_studio_controller.js#syncInspectorHead retypes these two
     lines as those are edited. Both truncate to one line: a card that grows
     with a 2000-character description would push the actual form off the
     bottom of a rail that has to hold the whole step. */
  .st-page .st-rail-right .st-fields > .st-fields-head{display:flex;gap:10px;align-items:flex-start;padding:10px;
    border:1px solid var(--border);border-radius:8px;background:var(--surface);margin-bottom:6px}
  .st-page .st-rail-right .st-fields > .st-fields-head .st-field-thumb{flex:none;width:48px;height:48px;
    object-fit:contain;border-radius:8px;background:var(--surface2)}
  .st-page .st-rail-right .st-fields-head-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
  .st-page .st-rail-right .st-fields-head-name{font:700 15px "Inter";color:var(--ink);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .st-page .st-rail-right .st-fields-head-desc{font:500 12.5px "Inter";color:var(--ink-soft);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

  /* The centre column at lg+, THREE rows: row 1 is the topbar (above), row
     2 is .st-pinned — the figures line, its own notes panel, and the
     overview strip, all three together now (studio.html.erb's own comment
     on .st-pinned has the full "why") — row 3 is .st-grid, `1fr`, the only
     one of the three that scrolls (overflow-y:auto, below), holding the
     player, the step list, add-a-step, and the notes copy field.

     Pinned-context round: row 2 used to be .st-scene-strip alone (this
     selector's own base rule, top of this file, still carries that class's
     below-lg styling); the figures line and its notes panel used to live
     one row down instead, folded INSIDE .st-grid's own row as children of
     .st-brew-card. Now all three of row 2's occupants share ONE wrapper,
     .st-pinned, so it's that div — not .st-scene-strip — that claims the
     grid cell here. Both .st-pinned and .st-grid stay DIRECT children of
     .st-page (no wrapper around either) for the same reason .st-grid always
     has: a stable, easily-selected ancestry other rules and checks can key
     off. Item 4: grid-column 3, not the old 2 — same "every track moved
     over by one" shift .st-rail-left/-right's own comment describes,
     further up. */
  /* max-height:40dvh, not a fixed height: a method with more editable
     figures than espresso, or Vietnamese labels (which wrap taller than
     English), can push this row's content past 40% of the viewport, and a
     fixed height would clip it. dvh, not vh, so a mobile browser's
     collapsing address bar can't move the cap mid-scroll.

     Fix-first round: the cap used to sit on .st-pinned ITSELF
     (overflow-y:auto on the whole region), which meant whichever child came
     LAST — the overview strip — was what silently lost space once combined
     content exceeded 40dvh. Measured on real laptops: 368px of content
     against a 360px budget at 1440x900 (8px hidden) and against a 307px
     budget at 1366x768 (61px hidden — most of the strip's own labels), with
     no visible cue that a scrollbar existed inside a region that reads as
     "pinned, not scrollable". The params row is the reason this region is
     pinned at all — it's what a visitor is actively editing — so it must
     never be the part that disappears. Turning .st-pinned into a flex
     COLUMN hands the shrink/scroll behaviour to whichever child asks for it
     (min-height:0 + overflow-y:auto, below, on .st-scene-strip alone):
     .st-figures-topbar and .st-figures-fields keep their natural
     (max-content) height by default — a flex item's automatic minimum size
     is its own content size unless its OWN overflow is set to something
     other than visible, which neither of those two gets — so they are never
     compressed. The strip is the one child given overflow-y:auto +
     min-height:0, so it alone shrinks to whatever's left inside the cap and
     scrolls past that. Context (the flow the figures produce) is the right
     thing to give up first when space is short; the figures themselves are
     not. .st-pinned's own overflow is intentionally left at its default
     (visible) now that the strip is the sole internal scroller — verified
     this is safe, not merely convenient: the params row + notes panel's own
     measured heights (156px/126px/314px across the spec's own viewport
     table) stay far under every 40dvh budget measured for this shell
     (307px+), so there is no realistic content that overflows .st-pinned's
     own box before the strip has already been squeezed to 0. */
  .st-page>.st-pinned{grid-column:3;grid-row:2;padding:16px 20px 0;min-width:0;margin-top:0;
    display:flex;flex-direction:column;min-height:0;max-height:40dvh}
  .st-page>.st-pinned>.st-scene-strip{flex:1 1 auto;min-height:0;overflow-y:auto}
  /* Supersedes the old two-up split this same selector's base rule (top of
     this file) carried below lg — .st-grid becomes a single flex COLUMN at
     lg+ instead, .st-brew-card/.st-editor/notes in that exact source order
     (Task 2, v2.1 dropped the `order` overrides this rule used to carry —
     DOM order alone now matches the visual order at every width, so below
     lg and lg+ can never disagree about which comes first — see the base
     .st-grid rule's own comment near the top of this file). */
  .st-page .st-grid{grid-column:3;grid-row:3;display:flex;flex-direction:column;gap:20px;
    margin-top:16px;padding:0 20px 24px;overflow-y:auto;min-width:0;min-height:0}
  /* .st-grid's own `gap:20px` (flex column, above) already spaces every
     child from the next — the base .st-copy rule's own margin-top:20px
     would double that here, so it's zeroed for this one lg+ instance. */
  .st-page .st-copy-notes{margin-top:0}

  /* Task 2 (v2.2): undo the below-lg "selected row only" gating on
     .st-row-controls (base rule, above) — at lg+ every row's own remove
     button (Task 3, round 3: the only control left in this block — see its
     own comment) stays always visible, same as before this task (desktop
     byte-identical).
     Equal specificity (two classes) to the base rule, later in the file, so
     this wins purely on source order; the higher-specificity
     `.st-row[data-selected] .st-row-controls` rule (base, above) still also
     matches a selected row here, in full agreement. */
  .st-page .st-row-controls{display:flex}
}

/* ── The bean's COUNTRY picker (shared/ui/combobox, compact) ──────────────
   A flag per origin, from the vendored lipis/flag-icons SVGs (see
   app/assets/images/flags/README.md) — real flags, never emoji, which this
   project bans as icons outright. The 4:3 artwork gets a hairline border and
   a hair of radius so a white-heavy flag (Japan, Indonesia's lower half)
   still reads as a flag rather than as a gap on the surface it sits on.
   Task 6 (rail slice): .st-country-field/.st-country-box moved into
   markup (min-w-0/w-full, both a plain one-property rule at every width). */
.st-page .cb-icon{border:1px solid var(--border);border-radius:2px;background:var(--surface)}


/* ── Task A8: the "you are editing the official recipe" chip ─────────────
   Visual twin of .st-alpha-chip (its own comment, above, is the reasoning
   for the shape: a short word visible at every width because a lone icon
   with a title tooltip tells a touch visitor nothing, and the full
   sentence as the accessible name so it is announced once). --bad rather
   than the alpha amber: alpha means "this may change under you", this
   means "what you change here changes it for everyone". */
.st-page .st-primary-chip{flex:none;display:inline-flex;align-items:center;gap:4px;color:var(--bad)}
.st-page .st-primary-label{display:inline;font:700 10px "JetBrains Mono",monospace;
  letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--bad);
  border-radius:999px;padding:2px 6px;white-space:nowrap}
@media (min-width:1024px){
  .st-page .st-primary-chip{gap:6px}
  .st-page .st-primary-label{padding:2px 7px}
}

/* ── Task A10: the icon control in a step's own field panel ──────────────
   A file input and a text button, on one line, so "Remove" reads as an
   action on the field above it rather than as another field. Hidden
   entirely for a step that is not the visitor's own (fillRow toggles it —
   see recipe_studio_controller.js#refreshBadge). */
.st-page .st-field-icon{display:flex;flex-direction:column;gap:4px}
.st-page .st-field-icon input[type=file]{font:500 12px "Inter";color:var(--ink)}
.st-page .st-icon-clear{align-self:flex-start;min-height:32px;padding:0 10px;
  border:1px solid var(--border);border-radius:8px;background:var(--surface);
  color:var(--ink-soft);font:600 11.5px "Inter";cursor:pointer}
.st-page .st-icon-clear:hover{color:var(--bad);border-color:var(--bad)}

/* ── Phase B: the drink lane on a studio-home method card ────────────────
   A footer link inside the card's own box, outside the card's <a> (a link
   inside a link is not markup any browser agrees on). Quiet by default:
   the card's primary action is still "build this method". */
.st-page .st-home-card-drink{display:block;min-height:44px;margin-top:6px;
  padding:12px 8px 0;border-top:1px solid var(--border);color:var(--ink-soft);
  font:600 12.5px "Inter";text-decoration:none;text-align:center}
.st-page .st-home-card-drink:hover{color:var(--accent)}
