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

/* ── 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. */
.st-page .st-topbar{display:flex;align-items:center;gap:4px;margin-bottom:16px}
.st-page .st-back{display:inline-flex;align-items:center;gap:4px;flex:none;min-height:44px;padding:0 4px;
  color:var(--ink-soft);text-decoration:none;font:500 14px "Inter"}
.st-page .st-back:hover{color:var(--accent)}
.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,.st-page .st-redo{flex:none;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-undo svg,.st-page .st-redo svg{width:17px;height:17px}
.st-page .st-undo:hover:not([disabled]),.st-page .st-redo:hover:not([disabled]){color:var(--accent);background:var(--surface2)}
.st-page .st-undo[disabled],.st-page .st-redo[disabled]{opacity:.3;cursor:not-allowed}
.st-page .st-save{flex:none;min-height:44px;padding:0 18px;border:0;border-radius:8px;
  background:var(--ink);color:var(--bg);font:600 14px "Inter";cursor:pointer}
.st-page .st-save[disabled]{opacity:.4;cursor:not-allowed}
/* Task 3b: "Preview" — icon-only, the same 44x44 square treatment as .st-undo/
   .st-redo just above (a labelled button here was the one sweep-caught
   regression this change introduced — the mobile top bar's own width is
   already spoken for by back/title/units/undo/redo/save). Never disabled:
   unlike Save, previewing needs no dirty draft to be worth doing. */
.st-page .st-preview-btn{flex:none;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-preview-btn svg{width:18px;height:18px}
.st-page .st-preview-btn:hover{color:var(--accent);background:var(--surface2)}
/* Task 3c (v2.1): the in-studio units toggle — same small pill idiom as the
   header's own switcher (shared/_header.html.erb), sized to THIS page's own
   44px touch targets (the header's h-9/36px pill is fine on a mouse-first
   desktop chrome bar, but this page holds itself to assertTouchTargets at
   every width). min-width, not just min-height: "g/°C"/"oz/°F" is short
   enough that padding alone wouldn't clear 44px on its own. */
.st-page .st-units{flex:none;display:inline-flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;padding:0 12px;border:1px solid var(--border);border-radius:8px;
  background:none;color:var(--ink-soft);font:600 13px "Inter";cursor:pointer;white-space:nowrap}
.st-page .st-units:hover{color:var(--accent);border-color:var(--accent)}

/* 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. */
.st-page .st-alpha-chip{flex:none;display:inline-flex;align-items:center;gap:4px;color:var(--gold)}
.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(--gold);
  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-page .st-back-label{display:none}

/* ── Round 5 Task 2: the language pill bar — sits directly below the main
   topbar (its own thin, full-width row; see studio.html.erb's own comment
   on why it isn't crammed into the already-packed topbar itself).
   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).

   nowrap + horizontal scroll (.scroll-slim — application.css's own site-
   wide thin-scrollbar idiom, reused rather than a second copy — same
   pattern Task 4's own "slim scrollbars" ask will lean on elsewhere in
   this file), NOT flex-wrap: up to six pills (en + all five CONTENT_
   LOCALES) at 44px-tall touch targets genuinely can't fit one row at
   390px wide, and letting the row wrap onto a second line pushed the
   whole centre column down far enough to blow studio_sweep.mjs's own
   assertStepListInFirstViewport 844px budget (measured directly: a
   wrapped two-row bar cost ~114px, landing the step list's own first row
   at 951px). A single scrollable row costs a fixed ~60px regardless of
   pill count instead. */
.st-page .st-lang-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:8px;margin-bottom:8px;
  overflow-x:auto;overflow-y:hidden}
.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{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)}

/* ── 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 (no
   grinder picker/scale strip sits beside it here — see studio.html.erb's own
   comment). 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) rather than the player's 24x24 — 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 (not stacked — each has to clear 44px
   in BOTH dimensions on its own, and stacking two 44px targets would double
   the cell's own height for no reason). */
.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): this rule's own 20px margin (top AND bottom) is what
   .st-scene-strip's negative margin-top, below lg (further down this file,
   .st-page .st-scene-strip), silently cancels out to close the gap
   .st-copy-intro's own bottom margin would otherwise leave in front of a
   collapsed strip toggle — see that rule's own comment for the full
   headroom-budget reasoning. The two numbers have to keep agreeing in
   MAGNITUDE (20px here, -20px there); change one without the other and
   that gap reopens (or overcorrects negative) the next time either rule is
   touched.

   Round 5 Task 2: the language pill bar (.st-lang-bar, ABOVE this in
   source order) adds its own ~55px to the below-lg first-viewport budget
   studio_sweep.mjs's own assertStepListInFirstViewport guards — see that
   rule's own comment. Reverting the merged Lexxy box back to these two
   plain textareas gives that budget 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 to keep
   the sweep green with the pill bar in place. */
.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); these two just add their own min/max ceiling
   — intro is a short 300-char field, notes is the studio's longest
   free-text field at 5000. */
.st-page .st-copy-intro textarea{min-height:84px;max-height:220px}
.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}
/* Below lg: pulled up to close the gap .st-copy-intro's own 20px bottom
   margin (base .st-copy rule, above) would otherwise leave in front of a
   collapsed one-line disclosure — a big blank seam belongs between actual
   content blocks, not before a 44px toggle bar. 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
   — intro and a collapsed toggle sitting flush, same as this strip already
   sits flush against the figures line right below it — recovers it without
   shrinking anything a visitor can actually tap. Zeroed again at lg+ (below)
   where the strip is always open and .st-copy-intro's own margin-bottom
   there is harmless (a real content block, not a collapsed toggle). */
.st-page .st-scene-strip{border:0;margin-top:-20px}
.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-scene-strip{margin-top:0}
  .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}
  .st-page .st-scene-strip .bt-ov-tile{flex:0 0 130px}
  /* The row-end connector (brew_timeline.css's own ::after) assumes a
     wrapped GRID's column gaps — meaningless (and visually wrong, drawn
     mid-row) once every tile sits in one scrolling row instead. */
  .st-page .st-scene-strip .bt-ov-tile::after{display:none}
}

/* ── 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 ── */
.st-page .st-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.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-head{display:flex;align-items:center;gap:10px;padding:8px 10px}
.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}
.st-page .st-row-n{flex:none;display:grid;place-items:center;width:24px;height:24px;border-radius:999px;
  background:var(--accent);color:#fff;font:700 11px "JetBrains Mono",monospace}
/* Task 6: the block's own scene art, one per row — see _step_row.html.erb's
   own comment for the shared URL a server-rendered row and a freshly
   client-added one both resolve it from. */
.st-page .st-row-thumb{flex:none;width:32px;height:32px;border-radius:6px;object-fit:contain;
  background:var(--surface2)}
.st-page .st-row-main{flex:1;min-width:0;display:flex;align-items:center;gap:8px;
  min-height:44px;padding:2px 0;cursor:pointer}
/* Task 3 (round 3): name + description stack in a column now (the row's own
   subline, below), .st-row-secs stays a flex sibling at the row's own trailing
   edge so it still centres vertically against the whole two-line block rather
   than just the name's own line. */
.st-page .st-row-main-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.st-page .st-row-name{font:600 14.5px "Inter";color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Task 3 (round 3): the step's own resolved description (base_copy[:text],
   see _step_row.html.erb's own comment) — one line, truncated exactly like
   .st-row-name just above (same overflow/ellipsis/nowrap trio), just smaller
   and muted (--ink-soft) to read as a subline rather than competing with the
   name for attention. :empty covers the templated blank row (base_copy: {}
   server-side — see studio.html.erb) and any block whose own description
   somehow resolves to nothing — collapses to zero height rather than
   reserving a blank second line. */
.st-page .st-row-desc{font:500 12px "Inter";color:var(--ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.st-page .st-row-desc:empty{display:none}
.st-page .st-row-secs{flex:none;margin-left:auto;font:600 12px "JetBrains Mono",monospace;color:var(--accent-2)}
/* 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. ── */
.st-page .st-fields{display:grid;grid-template-columns:1fr 90px;gap:8px 10px;
  padding:0 12px 12px;border-top:1px dashed var(--border);margin-top:2px;padding-top:10px}
.st-page .st-field{display:flex;flex-direction:column;gap:4px;min-width:0}
.st-page .st-field-wide{grid-column:1/-1}
.st-page .st-field-label{font:600 10px "JetBrains Mono",monospace;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-soft)}
/* 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}

/* ── Task 6: the fields panel's own header (block scene art + the
   "editing: …" language marker) and the language marker's own look,
   reused verbatim wherever else it appears (the left rail's Name/Intro/
   Notes labels — see studio.html.erb). ── */
.st-page .st-fields-head{grid-column:1/-1;display:flex;align-items:center;gap:10px;margin-bottom:2px}
.st-page .st-field-thumb{flex:none;width:64px;height:44px;object-fit:contain;border-radius:8px;
  background:var(--surface2)}
.st-page .st-lang-marker{font:700 10px "JetBrains Mono",monospace;letter-spacing:.06em;
  text-transform:uppercase;color:var(--accent-2)}
.st-page .st-rail-label .st-lang-marker{margin-left:6px}

/* 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. */
.st-page .st-field-move{grid-column:1/-1;display:flex;gap:8px;margin-bottom:2px}
.st-page .st-move-btn{flex:1;min-height:44px;padding:0 10px;border:1px solid var(--border);border-radius:8px;
  background:var(--surface);color:var(--ink);font:600 13px "Inter";cursor:pointer}
.st-page .st-move-btn:hover:not([disabled]){color:var(--accent);border-color:var(--accent)}
.st-page .st-move-btn[disabled]{opacity:.4;cursor:not-allowed}

/* 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. */
.st-page .st-grinder-clicks{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;
  margin-top:6px;padding-top:10px;border-top:1px dashed var(--border)}
.st-page .st-grinder-clicks-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
.st-page .st-grinder-click-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.st-page .st-grinder-click-box{flex:1 1 160px;min-width:140px}
.st-page .st-grinder-click-text{flex:1 1 110px;min-width:100px;border:1px solid var(--border);border-radius:6px;
  background:var(--surface);color:var(--ink);font:500 13px "Inter";padding:8px 10px;min-height:44px}
.st-page .st-add-grinder-click{align-self:flex-start;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-grinder-click:hover:not([disabled]){color:var(--accent);border-color:var(--accent)}
.st-page .st-add-grinder-click[disabled]{opacity:.4;cursor:not-allowed}

/* ── 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). A
   flex-wrapping row of .st-field's, reusing that class's own input/textarea
   styling above rather than a second copy of it. ── */
.st-page .st-param-fields{grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 10px;
  margin-top:2px}
.st-page .st-param-fields .st-field{flex:1 1 110px;min-width:110px}
.st-page .st-field input:disabled{opacity:.5}
.st-page .st-field-resolved{flex:none;align-self:center;min-height:44px;display:flex;align-items:center;
  font:600 11.5px "JetBrains Mono",monospace;color:var(--ink-soft)}
/* A checkbox field ("Pulse pours", "follow the recipe") 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,.st-page .st-field-follow{flex:1 1 100%;flex-direction:row;align-items:center;
  gap:8px;min-height:44px}
.st-page .st-field-check input,.st-page .st-field-follow 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-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:8px 10px}
.st-page .st-group-name-wrap{display:flex;flex-direction:column;gap:2px;flex:1 1 160px;min-width:140px}
.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-page .st-group-follow{flex:none;display:flex;align-items:center;gap:6px;min-height:44px}
.st-page .st-group-follow input{width:20px;height:20px;min-height:0;padding:0}
.st-page .st-group-follow span{font:600 10px "JetBrains Mono",monospace;letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-soft)}
/* Reuses .st-row-btn's own 44x44 icon-button look (base rule, above) rather
   than a second copy of it. */
.st-page .st-group-delete{flex:none}
/* 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{margin-left:14px}

/* ── Add a step + total ── */
.st-page .st-add{margin-top:16px;display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.st-page .st-add-box{max-width:22rem;flex:1;min-width:220px}
.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}
.st-page .st-total{margin:0;font:600 12.5px "JetBrains Mono",monospace;color:var(--ink-soft)}

/* ── Preview pane: collapsed behind a native <details> below lg, always
   open (and its summary hidden) at lg+. ── */
.st-page .st-preview-toggle{border:1px solid var(--border);border-radius:10px;background:var(--surface);overflow:hidden}
.st-page .st-preview-summary{list-style:none;cursor:pointer;display:flex;align-items:center;
  min-height:44px;padding:0 14px;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:12px}
@media (min-width:1024px){
  /* flex-shrink:0 — this <details> is a child of .st-grid's height-
     constrained flex column, and its flex-basis resolves through the
     ::details-content box to ~0, so default shrink collapsed the whole
     player to 0 height (body still painted 534px, clipped by the base
     rule's overflow:hidden — invisible, which is why the sweep's
     body-height guard missed it; that guard now measures THIS box). */
  .st-page .st-preview-toggle{border:0;background:none;border-radius:0;flex-shrink:0}
  .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}
}

/* ── 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:"▾"}
/* Anonymous-recipe expiry nudge — sits inside THE RECIPE rail group, above
   its own .st-rail-body (studio.html.erb), calmer than the preview page's
   own accent-tinted signed-out banner: a quiet neutral surface, no accent
   border, since this one persists across the whole editing session rather
   than a single preview visit. [hidden] (native attribute, toggled by
   recipe_studio_controller.js#showExpiryHint) is enough on its own — no
   extra display:none rule needed. Same at every width — never overridden
   inside the lg+ media query below. */
.st-page .st-expiry-hint{margin:10px 2px 0;padding:8px 10px;border-radius:8px;
  background:var(--surface2);font:500 12px/1.45 "Inter";color:var(--ink-soft)}
.st-page .st-expiry-hint a{margin-left:4px;color:var(--accent);font-weight:600;text-decoration:none}
.st-page .st-expiry-hint a:hover{text-decoration:underline}
.st-page .st-rail-body{display:flex;flex-direction:column;gap:12px;padding:0 2px 18px}
.st-page .st-rail-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.st-page .st-rail-field{display:flex;flex-direction:column;gap:4px;min-width:0}
.st-page .st-rail-label{font:600 10px "JetBrains Mono",monospace;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-soft)}
.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}
.st-page .st-rail-hint{margin:0;font:500 12.5px "Inter";color:var(--ink-soft);line-height:1.5}
.st-page .st-method-box{max-width:none;width:100%}
/* 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 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. */
.st-page .st-media-errors{margin:0;padding-left:18px;font:500 12.5px "Inter";color:var(--bad)}
.st-page .st-media-form{display:flex;flex-direction:column;gap:12px}
.st-page .st-media-banner-preview{width:100%;aspect-ratio:2/1;object-fit:cover;border-radius:8px;
  border:1px solid var(--border)}
.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-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.st-page .st-media-thumb{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;
  border:1px solid var(--border)}
.st-page .st-media-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.st-page .st-media-thumb form{position:absolute;top:4px;right:4px;margin:0}
.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). */
.st-page .st-home-grid>li{display:flex}
.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). */
    display:grid;grid-template-columns:280px minmax(0,1fr) 420px;
    /* Task 2 (round 3) inserted a fifth `auto` row for the live overview
       strip (row 3, between the intro and the figures line — see
       .st-page>.st-scene-strip below), pushing the figures line/figures-
       notes/`.st-grid` rows down by one each; `1fr` stays the last row
       either way. */
    grid-template-rows:auto auto auto auto auto 1fr;
    height:100dvh;overflow:hidden;
  }
  .st-page .st-topbar{grid-column:1/-1;grid-row:1;margin-bottom:0;padding:10px 16px;
    border-bottom:1px solid var(--border)}

  /* 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. */
  .st-page .st-back{display:none}
  .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 a fuller one-line note (.st-alpha-note, still not the
     FULL warning — see recipes.studio.alpha.note's own comment,
     config/locales/en.yml — that full sentence still only ever lives on
     title/aria-label; this is a teaser, not a duplicate). white-space:
     nowrap + text-overflow:ellipsis + min-width:0 on the note (not
     flex-wrap) for the same reason .st-lang-bar never wraps either — this
     row's own auto grid-row (row 1 of the lg+ shell) has no vertical
     budget check the way below-lg does, but a wrapped SECOND line here
     would still grow row 1 and steal that exact height back from the
     list's own 1fr row for no real gain, so a graceful truncation beats a
     silent height regression every time. */
  .st-page .st-alpha-chip{gap:6px}
  .st-page .st-alpha-icon{display:inline-block}
  .st-page .st-alpha-label{padding:2px 7px}
  .st-page .st-alpha-note{display:inline-block;min-width:0;max-width:320px;overflow:hidden;
    text-overflow:ellipsis;white-space:nowrap;font:500 12px "Inter";color:var(--ink-soft)}

  /* 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/6 spans every row below the top bar (intro/figures/figures'
     own notes/grid — Task 2, v2.1 grew this from 2/4 to 2/5; Task 3, v2.1's
     own figures-notes row grew it again, to 2/6; Task 2, round 3's own
     overview-strip row grew it once more, to 2/7). */
  .st-page .st-rail{display:flex;flex-direction:column;grid-row:2/7;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. */
  .st-page .st-rail-left{grid-column:1;border-right:1px solid var(--border);margin-bottom:0}
  .st-page .st-rail-right{grid-column:3;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. */
  .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-body{display:flex;flex-direction:column;gap:12px;padding:0 2px 18px}
  .st-page .st-rail-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
  .st-page .st-rail-field{display:flex;flex-direction:column;gap:4px;min-width:0}
  .st-page .st-rail-label{font:600 10px "JetBrains Mono",monospace;letter-spacing:.08em;
    text-transform:uppercase;color:var(--ink-soft)}
  .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-rail-hint{margin:0;font:500 12.5px "Inter";color:var(--ink-soft);line-height:1.5}

  /* Task 7: THE PHOTOS group's own upload form + gallery grid — same
     .st-rail-field/.st-rail-label vocabulary as every other rail control,
     never a competing style. */
  .st-page .st-media-errors{margin:0;padding-left:18px;font:500 12.5px "Inter";color:var(--bad)}
  .st-page .st-media-form{display:flex;flex-direction:column;gap:12px}
  .st-page .st-media-banner-preview{width:100%;aspect-ratio:2/1;object-fit:cover;border-radius:8px;
    border:1px solid var(--border)}
  .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-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
  .st-page .st-media-thumb{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;
    border:1px solid var(--border)}
  .st-page .st-media-thumb img{width:100%;height:100%;object-fit:cover;display:block}
  /* button_to's own wrapping <form> (action_view/helpers/url_helper.rb) —
     absolutely positioned over the thumb rather than sized itself, so it
     never disturbs the grid cell's own aspect-ratio box. */
  .st-page .st-media-thumb form{position:absolute;top:4px;right:4px;margin:0}
  .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-page .st-method-box{max-width:none;width:100%}
  /* 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}

  /* 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 centre column, now FIVE rows (Task 2, v2.1 added the intro row;
     Task 3, v2.1 added the figures-notes row just below; Task 2, round 3
     added the overview-strip row between intro and the figures line — see
     the shell's own grid-template-rows comment above): the intro copy field
     sits fixed (row 2, auto height, mirroring .bt-facts' own treatment —
     it's a short single field, no scroll needed), the overview strip sits
     fixed right after it (row 3, same reasoning — a handful of small tiles,
     never worth its own scroller), the figures line sits fixed (row 4,
     same reasoning again), the figures line's OWN notes fields sit fixed
     right after it (row 5 — hidden/0-height whenever "figures" isn't
     selected, so this row simply collapses rather than reserving empty
     space), and everything else — preview, step list, add-a-step, and the
     notes copy field too — is row 6, `1fr` with its own overflow-y so a
     long list only ever grows ITS row, never the page (mirrors the two
     rails' own .st-rail-scroll). All FIVE stay DIRECT children of .st-page
     (no wrapper) — see studio.html.erb's own comment on why: `.st-page >
     .bt-facts` is how this file's own preview-vs-studio figures-line checks
     (and scripts/verify/studio.mjs's own) tell the two .bt-facts instances
     on this page apart; the overview strip sits BEFORE it and the
     figures-notes row AFTER it, both in source order, for the same reason
     (never wrapped around it, which would change that selector's own
     match). */
  .st-page>.st-copy-intro{grid-column:2;grid-row:2;padding:16px 20px 0;min-width:0;margin-top:0}
  .st-page>.st-scene-strip{grid-column:2;grid-row:3;padding:8px 20px 0;min-width:0;margin-top:0}
  .st-page>.bt-facts{grid-column:2;grid-row:4;padding:8px 20px 0;min-width:0}
  /* Reuses .st-fields' own display:grid/columns/gap (base rule, above) —
     only the OUTER box (margin/padding/border, all sized for sitting
     INSIDE a row's own padded head) needs overriding here, to match its
     siblings' 20px side gutter instead. Tied specificity with that base
     rule (two classes each) — this wins on source order alone, since it's
     later in the file; see that rule's own comment for why. */
  .st-page>.st-figures-fields{grid-column:2;grid-row:5;margin:0;padding:8px 20px 12px}
  /* 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, preview/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:2;grid-row:6;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}
}
