/*
 * Order-flow stylesheet — step 1, "Delivery & Install", and the persistent
 * order rail.
 *
 * Served as a static asset and pulled in by `<FlowStyles>` rather than
 * `import`ed, for exactly the reasons given at the top of chrome.css: a bundled
 * import attaches the chunk to the shared `[brand]` layout segment and Next then
 * emits a <link> to it on every brand's every page. A hoisted <link> to a public
 * asset is rendered only by the page that needs it.
 *
 * Transplanted from design/produnk/FlowStep1.dc.html (drawn at 1440),
 * mechanically tokenised: colours and typefaces read `--brand-*`, sizes and
 * radii read the platform constants in globals.css. Board values ship verbatim —
 * the odd numbers (5.5px paddings elsewhere on this site, this board's 26px
 * inset indent, its 76px option label) are measured, not sloppy (playbook §3).
 *
 * Class prefix `sf-flow-`, which the partition in `tests/order-flow.test.ts`
 * sweeps against this flow's markup. SAME FLAG THE PRODUCT SHEET RAISED:
 * `tests/category-page.test.ts` sweeps chrome.css with `/\.sf-(?!cat-|home-)/`,
 * which now also claims every `sf-pdp-` and every `sf-flow-` name and has markup
 * for neither. Both prefixes have to join that exclusion; this pass does not
 * edit that file (another agent owns it), so it is FLAGGED, not applied.
 *
 * THE RAIL IS FLOW-LOCAL, DELIBERATELY. It is the unified cart/rail/drawer
 * pattern (tokens §4) and it has exactly one consumer today. When the SECOND
 * one lands — the parts drawer, or step 3's checkout summary — the rail's rules
 * (`.sf-flow-rail*`, `.sf-flow-line*`, `.sf-flow-inset*`, `.sf-flow-badge`,
 * `.sf-flow-edit*`, `.sf-flow-option*`, `.sf-flow-chip`, `.sf-flow-qty*`) move
 * to a shared sheet with a shared prefix and this one keeps what is genuinely
 * the flow's: the step indicator and the two step states. A shared component
 * with one caller is a guess about the second.
 *
 * THE BOARDS DECLARE NO LEADING, so every line box on them that does not name
 * one is `normal`. globals.css's body 1.65 would otherwise deepen every card,
 * every chip and every mono figure; `.sf-flow` resets it once, the same way
 * `.sf-pdp` and `.sf-cat-hero` do. globals.css also styles every h1-h3 with the
 * heading face at weight 500, a -0.015em track, 1.12 leading and `text-wrap:
 * balance` — every heading below restates what the board draws instead.
 *
 * THE BOARD IS CONTENT-BOX and this site is border-box, so every place the
 * drawing carries an EXPLICIT width or height ALONGSIDE padding or a border —
 * the zip field, the step dots — ships the border-box equivalent with its
 * arithmetic written beside it, not the board's authored number. Copying the
 * authored numbers instead is a proven ~0.8% reading against a board (the
 * product page's zip rows).
 */

.sf-flow {
  /* THE BOARD'S DEFAULT TEXT is the browser's own 16px at `normal` leading —
   * its <helmet> sets neither. globals.css sets the site's body to 17/1.65, so
   * everything the board leaves unstyled (its quiet links, its bare divs) would
   * render a size and a leading larger than drawn. Both are restated once here
   * rather than per element, the same way `.sf-pdp` restates the leading. */
  font-size: var(--text-emphasis);
  line-height: normal;
  /* NO `padding-bottom` ANY MORE (Ryan's ruling A3, 2026-08-27). The board's
   * wrapper carries 48px under the grid before the page ends, and that job
   * belongs to the FOOTER now: `.sf-footer` owns its own space above, on every
   * page, so no page carries footer clearance of its own. Leaving this here
   * would be 48px added to the site-wide 80. */
}

/* ------------------------------------------------------------ focus ring -- */

/*
 * The site-wide ruled ring (Ryan, 2026-08-26): thickened, WCAG-compliant, and
 * the same on every surface. `--sf-focus-ring` and its halo are declared on
 * `:root` by chrome.css; the fallbacks repeat the value exactly once, in this
 * sheet's own focus block, so a page that somehow loaded this sheet without that
 * one still draws a compliant ring — same discipline as category.css and
 * home.css.
 */
.sf-flow a:focus-visible,
.sf-flow button:focus-visible,
.sf-flow input:focus-visible,
.sf-flow select:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/* The rail's controls sit on white inside a bordered card; the halo separates
 * the ring from the card's own hairline. */
.sf-flow-rail button:focus-visible,
.sf-flow-rail a:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff));
}

/* Screen-reader-only text. `position: absolute` needs a positioned ancestor or
 * it extends the DOCUMENT's scrollable overflow (playbook §5, the sr-span
 * overflow bug) — `.sf-flow-rail__disclose` is `position: relative` for exactly
 * this. */
.sf-flow-sr {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* ------------------------------------------------------ the calc mark -- */

/*
 * "WE ARE WORKING ON IT" — three pulsing dots (Ryan, 2026-09-01).
 *
 * WHY IT IS HERE, ABOVE THE BLOCKS, and not inside `.sf-flow-ck__` or
 * `.sf-flow-rail__`: it is drawn in THREE places that are in two different
 * blocks — the address card's band, the rail's note under the money moment,
 * and the place-order button — and a mark that lives inside one of them would
 * have to be reached from the others by selector. It is a utility beside
 * `.sf-flow-sr` above, which is the only other thing in this sheet with the
 * same shape of life.
 *
 * IT IS THE HOUSE'S OWN FIGURE, redrawn rather than invented: `sections.css`'s
 * `.sf-embed__dot` behind the AI answer card is the same three dots at the same
 * cadence. That sheet is not loaded on the checkout (flow.css is a static asset
 * served by path), so the drawing is repeated where it is needed rather than
 * imported to no effect — the same trade `flow-title.tsx` makes in the other
 * direction with chrome.css.
 *
 * NO RING, NO SWEEP, NO GRADIENT. The storefront is flat and its accent is the
 * orange on the button this usually sits under; a spinner would be the only
 * rotating thing on the site, and a second orange under an orange button reads
 * as two states arguing.
 *
 * `position: relative` because the label is sometimes an `.sf-flow-sr` span,
 * which needs a positioned ancestor or it extends the document's scrollable
 * overflow (playbook §5, the sr-span overflow bug).
 */
.sf-flow-calc {
  align-items: center;
  display: inline-flex;
  gap: 8px;
  position: relative;
}

.sf-flow-calc__dots {
  align-items: center;
  display: inline-flex;
  flex: none;
  gap: 5px;
}

/* The sentence beside the dots. It carries no size or colour of its own —
 * the band, the note and the button each set their own type, and a mark that
 * imposed one would look pasted into two of the three. */
.sf-flow-calc__label {
  min-width: 0;
}

.sf-flow-calc__dot {
  animation: sf-flow-calc-pulse 1.05s ease-in-out infinite;
  background: currentcolor;
  border-radius: var(--radius-pill, 999px);
  height: 5px;
  width: 5px;
}

/* `currentcolor` above and nothing here: the dots take the ink of whatever
 * they were dropped into — muted in the band, muted in the rail's note, the
 * button's own disabled ink on the bar — so one drawing reads correctly on
 * three grounds without three modifiers. */
.sf-flow-calc__dot:nth-child(2) {
  animation-delay: 0.15s;
}

.sf-flow-calc__dot:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes sf-flow-calc-pulse {
  0%,
  80%,
  100% {
    opacity: 0.28;
  }

  40% {
    opacity: 1;
  }
}

/* A reader who asked for less motion gets a still row of dots. The STATE is
 * still drawn — three dots and a sentence saying what is being calculated —
 * only the pulse stops, which is the same bargain `sections.css` strikes. */
@media (prefers-reduced-motion: reduce) {
  .sf-flow-calc__dot {
    animation: none;
    opacity: 0.6;
  }
}

/* ----------------------------------------------------------- page title -- */

/*
 * THE FLOW'S TITLE BAND — the platform band, minus the crumbs (Ryan's ruling,
 * 2026-08-26; the component is `flow-title.tsx` and carries the reasoning).
 *
 * chrome.css owns every rule that draws it. What is left here is the ONE thing
 * that is only true of a band with no crumb row: `.sf-title-band__row` carries
 * `margin-top` — 18px at desktop, 10 on a phone — and that margin is the air
 * BETWEEN the crumbs and the title. With no crumbs above it, it is 18px of
 * nothing at the top of the band, on top of the band's own 26.
 *
 * Written as a modifier rather than by overriding the row wherever it appears,
 * because the crumbed band on the category page is the same element and must
 * not move.
 */
.sf-title-band--flow .sf-title-band__row {
  margin-top: 0;
}

/*
 * AND ON THE PHONE IT PINS — WHOLE, WHICH IS WHAT MAKES IT CHEAP (Ryan's
 * ruling, 2026-08-27, reversing the "deliberately not sticky" line above and in
 * `flow-title.tsx`: title sticky, stepper not, order band pinned beneath it).
 *
 * NOT the chrome's driven band. `.sf-title-band[data-driven]` pins at
 * `--sf-pinned-offset − band-h + stuck-h` and shrinks its title into a line so a
 * crumb row and a page control can travel off the top behind the bar. This band
 * has neither: one word, one row, nothing to shed. So it pins at the chrome's
 * own offset with the height it already has, and BECAUSE nothing changes size
 * when it sticks there is no flow-height invariant to keep — which is the whole
 * reason it may be `position: fixed` rather than driven.
 *
 * AND IT MUST NOT BE DRIVEN, which is now a MEASUREMENT rather than a taste
 * (checked against the category batch that landed the same day). chrome.css's
 * mobile stuck line is built around the category band's QUIET DROPDOWN:
 * `--sf-band-stuck-line-m` is that control's 44px box, and the stuck height it
 * makes is 20 + 44 + 12 + 1 = 77px. This band's WHOLE resting height at 390 is
 * 56.516. Mounting a driver here would therefore pin it at
 * `54 − 56.516 + 77 = 74.5` — the band hanging 20px below the nav bar with a
 * strip of page behind it — and would have every reader of the token, this
 * page's order band included, reserve 77px for a 56.5px band. The band is not a
 * title-and-control line; it is a title. So the driver stays off and the token
 * is this band's own height.
 *
 * `fixed` RATHER THAN `sticky`, and the difference is the point (see
 * `flow-pin.tsx`): a sticky band travels only inside its containing block, and
 * `<main>` on this page ends where the footer begins — a hundred-odd pixels
 * below the flow in the short states. Its RESTING position is already the pinned
 * one (it sits directly under the pinned nav row), so taking it out of flow
 * changes nothing at rest, and `.sf-flow` reserves the height it no longer
 * occupies.
 *
 * `--sf-band-stuck-h` IS THEREFORE ITS WHOLE HEIGHT, not zero. The token means
 * "the height a band occupies once it is stuck under the nav row", and
 * everything that pins below the chrome reads it — `html { scroll-padding-top }`
 * on this page, and the order band's own offset below. Measured by `flow-pin.tsx`
 * and published on `:root`; the declared values here are its pre-driver
 * fallback, and they are the two cuts' RENDERED heights (measured 2026-08-27,
 * Schibsted Grotesk 700): 26 + 37 + 26 + 1 = 90 above the mobile cut, where the
 * title sets at 30px, and 14 + 23.516 + 18 + 1 = 56.516 below it, where it sets
 * at 21. The fraction is kept for the reason `title-band-sticky.tsx` keeps its
 * own: this height lands one band's top edge on another's bottom, and a rounded
 * pair would show a seam of page between them.
 *
 * ABOVE THE COLLAPSED-BAND CUT IT IS STILL STATIC, and the token still zero: the
 * ruling is the phone's choreography, the desktop rail is a column that pins
 * itself, and a 90px band pinned across a desktop viewport is furniture nobody
 * asked for.
 *
 * The doubled `:has()` is deliberate and is not a specificity trick for its own
 * sake: chrome.css's selector scores (0,2,0) and this must beat it from a sheet
 * whose position in the cascade is decided by React's hoisting order, not by
 * anything written down. Two matched conditions score (0,3,0) and win wherever
 * they are, and the second condition — `.sf-flow` — is a true statement about
 * this page rather than a repeat of the first.
 */
:root:has(.sf-title-band--flow):has(.sf-flow) {
  --sf-flow-title-h: 90px;
  --sf-band-stuck-h: var(--sf-flow-title-h);
}

@media (max-width: 767.98px) {
  :root:has(.sf-title-band--flow):has(.sf-flow) {
    --sf-flow-title-h: 56.516px;
  }
}

@media (min-width: 920px) {
  :root:has(.sf-title-band--flow):has(.sf-flow) {
    --sf-band-stuck-h: 0px;
  }
}

@media (max-width: 919.98px) {
  .sf-title-band--flow {
    left: 0;
    position: fixed;
    right: 0;
    /* The chrome's own rung, and nothing added: the band IS the row under the
     * nav bar. */
    top: var(--sf-pinned-offset, 54px);
    /* chrome.css's own band z-index — over the page, under the header's 40, so
     * the nav bar and the badge hanging off it stay in front. */
    z-index: 20;
  }

  /* THE HEIGHT IT NO LONGER OCCUPIES, given back to the page as padding rather
   * than as a margin: `.sf-flow` is `<main>`'s first in-flow child once the band
   * is fixed, and a top margin there would collapse out through `<main>` and
   * appear above the page instead of inside it. Padding cannot collapse. */
  .sf-flow {
    padding-top: var(--sf-flow-title-h, 56.516px);
  }
}

/* -------------------------------------------------------- step indicator -- */

/*
 * THE PROGRESSION BAR HAS A GROUND (Ryan's ruling, 2026-08-26): with the order
 * rail below it gone full-bleed, the stepper needed to read as a band too, and
 * the ruling names it "slightly darker than the body". That is `well` (#F1F0EC)
 * — tokens §1's own "gray bands" job, and the one step down from the page's
 * `paper` (#FBFBFA) that the palette actually has. `line` (#E8E7E3) is the
 * hairline value and would have been a band the width of a border; `surface`
 * is lighter, not darker.
 *
 * SO THE STACK AT THE TOP OF THE PAGE IS THREE DECLARED GROUNDS, darkening
 * downwards and then resolving: nav blue, paper title band, well stepper band,
 * page. Each one is separated from the next by a change of ground rather than
 * by a rule, which is what stops the region reading as four stacked cards.
 *
 * FULL-BLEED, AND STRUCTURALLY so — this element is now OUTSIDE `.sf-container`
 * and renders one inside itself (`flow-steps.tsx`). A negative margin would
 * have been half a band above 1440. Nothing here can overflow: the band is a
 * plain block at 100% and the measure inside it is the same `.sf-container`
 * every other row on the page uses.
 */
.sf-flow-steps {
  background: var(--brand-color-well, #f1f0ec);
  /* Symmetric, because it is a band now: the board's lone `padding-top: 22px`
   * described a row of marks floating on the page ground with the grid's own
   * 28px margin under it. A ground needs both edges. 18 either side sets the
   * band at 64px tall against the 28px step dots. */
  padding-block: 18px;
}

/* The board draws the row as a `space-between` flex with ONE child, so the step
 * list is a flex ITEM and shrinks to fit. As a block `<ol>` it would fill the
 * measure, and the crop would not be the same region. That flex now lives on
 * the band's inner measure rather than on the band, which is the only thing the
 * band-ification moved. */
.sf-flow-steps > .sf-container {
  display: flex;
  justify-content: space-between;
}

.sf-flow-steps__list {
  align-items: center;
  display: flex;
  /* The board's items carry their own 16px connector margins; the list itself
   * draws no gap. */
  gap: 0;
  list-style: none;
  margin: 0;
  padding: 0;
}

.sf-flow-steps__item {
  align-items: center;
  display: flex;
  gap: 10px;
}

.sf-flow-steps__rule {
  background: var(--brand-color-line-mid, #d8d6d0);
  flex: none;
  height: 1px;
  /* The board draws the connector as a SIBLING between two items, 16px clear on
   * each side, in a row with no gap. Here it is the first child of the item it
   * precedes, in a row that HAS a 10px gap — so it carries the full 16 on its
   * left and 6 on its right, and the item's own gap makes that 16 too. */
  margin: 0 6px 0 16px;
  width: 56px;
}

.sf-flow-steps__dot {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: 1.5px solid var(--brand-color-line-mid, #d8d6d0);
  border-radius: 50%;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  flex: none;
  font-size: var(--text-caption);
  font-weight: 700;
  /* The board's 26 is its CONTENT box, plus its border on each side — and the
   * border is the measured 1px, not the authored 1.5: Chrome rounds a
   * fractional border width to whole device pixels, so at DPR 1 the board's own
   * dot renders 28x28 (measured on the board itself). 1.5px selection borders
   * elsewhere on the site are unaffected — they sit on auto-sized boxes where
   * nothing does arithmetic with them. */
  height: 28px;
  justify-content: center;
  width: 28px;
}

.sf-flow-steps__dot--current {
  background: var(--brand-color-ink, #17181a);
  border-color: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
}

/*
 * A COMPLETED STEP: the same ink fill as the current one, with a check in place
 * of the numeral. Unreachable from step 1 — the only step that exists — and it
 * is here because the phone rung above drops the labels: without the check, a
 * filled dot behind the current step would be indistinguishable from it, and
 * the row would say two steps are current.
 */
.sf-flow-steps__dot--done {
  background: var(--brand-color-ink, #17181a);
  border-color: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
}

/* A COMPLETED STEP IS A LINK (2026-08-27, with step 2) — the flow's way back,
 * and the reason no step draws a Back button. It wraps the dot and the label
 * without changing either: the row's own metrics are on those two elements, so
 * this box only has to stop being an underlined text link and keep the pair
 * on one line. */
.sf-flow-steps__back {
  align-items: center;
  display: inline-flex;
  gap: 10px;
  text-decoration: none;
}

.sf-flow-steps__label {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  font-weight: 500;
}

.sf-flow-steps__label--current {
  color: var(--brand-color-ink, #17181a);
  font-weight: 700;
}

/*
 * A COMPLETED STEP'S LABEL IS `ink-soft`, NOT `muted` — the step-2 board draws
 * three DIFFERENT label treatments, and step 1's board could only show two of
 * them (nothing is completed on step 1), so this rung was missing and the
 * completed label was rendering as an upcoming one. Found by the `a2-steps`
 * gate at the ACCEPTANCE threshold: #6C6A62 against #44464A is a max-channel
 * delta of 40, well over 24, on the only region that draws the state.
 *
 * It reads correctly as well as faithfully: a step behind you is darker than
 * one ahead of you and lighter than the one you are on, which is the ordering
 * the three greys are for.
 */
.sf-flow-steps__label--done {
  color: var(--brand-color-ink-soft, #44464a);
}

/* ------------------------------------------------------------- the grid -- */

/*
 * THE BOARD'S TWO-COLUMN GRID, AT THE WIDER OF THE TWO WIDTHS ITS BOARDS DRAW:
 * `1fr 380px` with a 32px gap, `align-items: start` so the rail can stick. The
 * board draws it inside `margin: 0 64px`, which is the page gutter
 * `.sf-container` already owns — duplicating it here would double it (the same
 * call the product page's grid made).
 *
 * 360/28 → 380/32 (Ryan's ruling, 2026-08-27: "go with a wider rail, give the
 * dense order room to breathe"). THE BOARDS THEMSELVES DISAGREED and the ruling
 * is which one the shell renders: `FlowStep1` and `FlowAccessories` draw
 * `1fr 360px` at 28, `FlowCheckout` and `ConfirmPage` draw `1fr 380px` at 32.
 * Neither family is a typo — both are measured drawings — and the shell is ONE
 * grid across all three steps, so this was never a per-step choice. The
 * checkout/confirmation family wins; steps 1 and 2's drawings become DELIBERATE
 * DIVERGENCES, declared per region in `flowgate.mjs` as a 24px-narrower content
 * crop and a 20px-wider rail crop. What the same move settles for free: step 3
 * and `/order-confirmation` (`confirm.css`, `1fr 380px` at 32) stop being
 * flagged AGAINST the shell and now measure the same rail on both surfaces.
 *
 * THE RAIL STILL NARROWS TO 300 at the 1239.98 rung (the board's own caption
 * rule, below) — the ruling widened the rail, it did not delete the ladder. The
 * 32px gap is inherited there, and stops at the 919.98 cut where the band goes
 * full-bleed and names its own 20.
 *
 * WHAT THE 20px DOES NOT AUTOMATICALLY BUY BACK: the option rows' label still
 * takes its own line (see `.sf-flow-option__label`). At 380 the board's
 * label-beside-chips row FITS AGAIN at this rung — measured, not assumed — but
 * only at this rung, and reversing a drawing decision is Ryan's call, not a
 * consequence of a width.
 */
.sf-flow__grid {
  align-items: start;
  display: grid;
  /* `minmax(0, 1fr)`, not a bare `1fr`: an `fr` track's automatic minimum is its
   * MIN-CONTENT, and this column holds nowrap furniture (the wide Continue, the
   * install price row). A bare `1fr` is floored by them well before the mobile
   * cut. */
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: 32px;
  margin-top: 28px;
}

/* The pinned stack's sentinel: a marker at the order band's resting top, in
 * normal flow and worth nothing in it. What it CARRIES is declared at the
 * collapsed-band cut. */
.sf-flow__pin {
  height: 0;
}

.sf-flow-step {
  display: flow-root;
}

/* --------------------------------------------------------- the furniture -- */

/* The board's `.card`. */
.sf-flow-card {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
}

/*
 * The board's `.btn` — accent is commerce (tokens §3).
 *
 * NORMALISED TO THE 48px PLATFORM PRIMARY (Ryan, 2026-08-26). The board's own
 * `15px 28px` on a 16/normal line box renders 50 tall, which is not a value the
 * scale has — §3 says primary buttons are 48 and the product page's buy bar is
 * the reference implementation (`min-height: 48px`, padding on the sides only).
 * This is the same box by the same means: the height is DECLARED and the text is
 * centred in it, so the button no longer changes size with whatever leading a
 * line box happens to inherit. Widths are unchanged — the side padding is the
 * board's.
 */
.sf-flow-btn {
  align-items: center;
  background: var(--brand-color-accent, #c4530a);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  /* THE PRIMARY RUNG, from globals.css §3 (Ryan's "a button is a button",
   * 2026-09-01). The numbers below are exactly what this rule already carried —
   * this button was the one the whole site is now normalised TO — so reading
   * them from the tokens is a no-op here by construction, and the fallbacks
   * repeat each value once, the `--sf-pinned-offset` discipline. */
  font-size: var(--button-text, 16px);
  font-weight: var(--button-weight, 700);
  justify-content: center;
  min-height: var(--control-height, 48px);
  padding: 0 var(--button-pad-x, 28px);
  text-align: center;
  /* BOTH OF THESE ARE WORN BY AN `<a>` NOW (2026-08-27): every Continue out of
   * step 1 became a link when step 2 landed, because the order is the URL and
   * a control that changes it goes somewhere. globals.css sets `a { color:
   * inherit }` and nothing else, so the UA's underline is still on and has to
   * be said off. A no-op on the buttons that also wear it. */
  text-decoration: none;
}

.sf-flow-btn:hover {
  background: var(--brand-color-accent-strong, #9c420a);
}

/* The board draws the tier-2 primary at 56px of side padding. */
.sf-flow-btn--wide {
  padding: 0 56px;
}

/*
 * THE ACCESSORY ROW's height (step 2's board): the card's control row is
 * unified at 40 — "Full details · stepper · Add to order" all measure the same,
 * which is the caption's own word for it and what `--control-height-sm` is.
 */
.sf-flow-btn--row,
.sf-flow-ghost--row {
  align-items: center;
  display: inline-flex;
  font-size: var(--button-text-sm, 14.5px);
  /* THE WEIGHT COMES DOWN WITH THE RUNG (2026-09-01). `.sf-flow-btn` above is
   * 700 because the primary rung is; dropping only its height left an "Add to
   * order" drawing 40/14.5/700 next to a "Full details" drawing 40/14.5/600 in
   * the SAME control row. A rung is a height AND a type — that is the whole
   * point of pinning them together. */
  font-weight: var(--button-weight-sm, 600);
  justify-content: center;
  min-height: var(--control-height-sm, 40px);
  padding: 0 var(--button-pad-x-sm, 18px);
  /* BOTH OF THESE HOST AN `.sf-flow-sr` SPAN — "Add to order" and "Remove" are
   * the same words on eight cards, so each carries the accessory's name for a
   * reader listing the page's controls. An absolutely-positioned span whose
   * ancestors are all static takes the PAGE as its containing block and extends
   * the document's scrollable overflow (playbook §5, the sr-span overflow bug),
   * so the control it belongs to is positioned. */
  position: relative;
}

/*
 * ═══ W84 — THE ORDER MARK ON "ADD TO ORDER" (Ryan, 2026-09-03) ═══
 *
 * "Add To Order button gains the order icon." The mark is `CartMark` — the same
 * glyph the rail draws beside "Your order" and the product page's buy bar draws
 * on its own primary — so the three conversion controls a shopper meets on the
 * way through wear one mark rather than three.
 *
 * `inline-flex`, NOT `block`, and this is the trap the playbook records: an
 * `<svg>` inside a `display: block` span stays inline and picks up the line
 * box's 3px descender, which would sit the mark low against a centred label. A
 * flex wrapper blockifies it.
 *
 * THE GAP IS THE BUY BAR'S, SCALED TO THE RUNG. That button draws a 17px mark
 * 10px from its words inside a 48px box; this row is 40, so the mark is 15 and
 * the gap 8 — the same proportion, not a second convention.
 */
.sf-flow-btn__mark {
  align-items: center;
  display: inline-flex;
  margin-right: 8px;
}

/* THE ROW MODIFIER IS NOW THE BASE GHOST'S OWN BOX (2026-09-01). It used to be
 * the one place the ghost was told to declare its height, because the base was
 * padding-derived; the button audit moved that discipline into `.sf-flow-ghost`
 * itself, so the pair above only still exists to give `.sf-flow-btn--row` — the
 * PRIMARY — the compact rung for the accessory row. The ghost's own weight is
 * already the rung's, and the shared rule now says it for both, so the separate
 * `.sf-flow-ghost--row { font-weight }` declaration retires here. */

/* The rail's CTA: full width, at the same declared 48. */
.sf-flow-btn--block {
  display: flex;
  margin-top: 14px;
  padding: 0;
  width: 100%;
}

/*
 * The board's `.ghost`: ink-outlined, wayfinding, never commerce.
 *
 * TRANSPARENT, NOT WHITE — and it took a threshold-2 reading to see it. The
 * board declares no background on `.ghost`, so on the tier-1 self-install card
 * (which is `paper`, not `surface`) the button shows the card through it. White
 * against paper is a max-channel delta of 4: invisible to the acceptance
 * threshold, and a 2.959% region at threshold 2. A `<button>` needs it said out
 * loud, because the UA gives it one.
 */
.sf-flow-ghost {
  /* THE COMPACT RUNG, DECLARED (Ryan's "a button is a button", 2026-09-01).
   *
   * IT USED TO BE `padding: 12px 22px` ON `inline-block`, and that is precisely
   * the shape the button audit was called to kill: the height came out of the
   * padding plus whatever line box the button had inherited, so THIS ONE CLASS
   * measured 44px in the tier-2 card and 34px in the phone bar — one class, two
   * buttons. Now the height is stated and the label is centred in it, so the
   * box is the same wherever the rule lands. Same means as `.sf-flow-btn`
   * above, which has worked this way since the 48px normalisation.
   *
   * W1, IN ONE LINE: this is the phone bar's Checkout, and `.sf-flow-rail__
   * disclose` beside it is the "Your order" expander. They were 34 and 40, one
   * transparent and one `paper`-tinted. They are both this box now. */
  align-items: center;
  background: transparent;
  border: 1px solid var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  justify-content: center;
  min-height: var(--control-height-sm, 40px);
  padding: 0 var(--button-pad-x-sm, 18px);
  text-align: center;
  text-decoration: none;
}

/*
 * THE PHONE BAR's checkout, outlined (Ryan, 2026-08-26). The bar's real call to
 * action is the zip field and its Continue in the card below, so this one wears
 * the wayfinding treatment rather than the commerce one — an orange button
 * pinned under the header invites the reactionary tap the ruling is about. Its
 * BOX is the board's, unchanged; only the ground and the ink moved.
 */
/*
 * `--sm` NARROWS THE SIDES, IT NO LONGER SHRINKS THE BUTTON (Ryan's W1,
 * 2026-09-01). It used to cut the type to 13.5 and the padding to `8px 14px`,
 * which drew a 34px box next to a 40px expander in the same 12px-tall band —
 * the inconsistency Ryan named. The rung is not a modifier's to change; what a
 * cramped bar legitimately needs is less room on the SIDES, so that is all this
 * says now.
 */
.sf-flow-ghost--sm {
  flex: none;
  padding: 0 14px;
}

/* Every control on this page that cannot act yet keeps its exact box and says
 * so — the house rule. The cursor is the only thing that changes. */
.sf-flow [aria-disabled="true"] {
  cursor: default;
}

.sf-flow-btn[aria-disabled="true"]:hover {
  background: var(--brand-color-accent, #c4530a);
}

/* The board's quiet link: muted, underlined, the `a` rule in its <helmet>. */
/* NO FONT SIZE: the board's quiet links are bare `<a>` elements that inherit
 * whatever they sit in — 16 inside a card, 13.5 where a parent says so. The
 * rail's Edit is the one the board sizes explicitly, and it has its own rule. */
.sf-flow-quiet {
  color: var(--brand-color-muted, #6c6a62);
  text-decoration: underline;
}

.sf-flow-quiet:hover {
  color: var(--brand-color-ink-soft, #44464a);
}

/*
 * The same quiet treatment on a BUTTON, for the one quiet affordance that opens
 * an overlay instead of navigating. A control that does not navigate is not an
 * anchor (the storefront's standing rule), so it is a `<button>` — and a button
 * arrives with a UA background, border, padding, font and alignment that all
 * have to be undone before `.sf-flow-quiet` can be seen at all.
 */
.sf-flow-quiet--button {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  /*
   * THE CAPTION REGISTER, and it is part of W4's fix.
   *
   * `font: inherit` was taking SIXTEEN pixels from the card around it — which
   * made "See install pricing for every model" larger than the price label,
   * larger than the button's own words, and the loudest text in the totals block
   * after the figure itself. That is the half of Ryan's "grayed crowded link"
   * that was not about crowding. 13.5 is "Up next"'s size, which is the
   * reference he named, and it is the size every other quiet aside on this step
   * already uses.
   *
   * AFTER `font: inherit`, which resets it. Both of this modifier's users — the
   * service-centre disclosure and the install-pricing link — are asides beside
   * something louder, so this belongs on the modifier rather than being scoped
   * to one of them.
   */
  font-size: var(--text-small);
  padding: 0;
  text-align: inherit;
}

/* The board's `.bul`. globals.css sets `list-style: none` and zeroes the
 * padding on every list, so both have to be said again. */
.sf-flow-bullets {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  line-height: 1.6;
  margin: 0;
  padding-left: 18px;
}

.sf-flow-bullets li {
  list-style: disc;
}

/* The board's `.upnext` — the live site's signature line under every primary. */
.sf-flow-upnext {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin-top: 10px;
}

.sf-flow-upnext__what {
  color: var(--brand-color-ink-soft, #44464a);
  font-weight: 600;
}

/* The board's `.well` + `.cgi`: the placeholder that stands in for a render. */
/*
 * W121 (Ryan, 2026-09-04): THE REAL PRODUCT PHOTOGRAPH, where a drawn well was.
 *
 * `object-fit: cover` is what squares a 280x187 mirrored thumbnail into the
 * rail's 44px tile without distorting it — the intrinsic `width`/`height`
 * attributes stay honest so the browser reserves the right box before the
 * bytes land, and this crops rather than stretches what arrives.
 *
 * THE SAME RADIUS AND GROUND THE WELL WEARS, because it stands in the same
 * place for the same reason: the tile is a fixed square in a flex row and the
 * row's rhythm must not change with whether a line has a photograph.
 */
/*
 * W143 (Ryan, 2026-09-04): THE TILE AND THE NAME ARE LINKS, AND LOOK LIKE
 * NOTHING.
 *
 * "LINK to the detail page with NO visual change." So this rule's whole job is
 * to make an anchor invisible: `color: inherit` because a UA anchor is blue and
 * these sit in two different type treatments (the tile has none, the name is
 * `ink` at the line's weight), and `text-decoration: none` because that is the
 * thing Ryan named. Nothing else is set — no display change, no padding, no
 * cursor override — so the row's box tree is exactly what it was.
 *
 * HOVER IS THE ONE PLACE IT SPEAKS, and only just: Ryan's words were "hover can
 * stay subtle if anything", and an underline on hover is the platform's own
 * idiom for "this was a link all along". It costs nothing at rest, which is the
 * ruling, and it stops the row being a link nobody can discover.
 *
 * THE FOCUS RING IS NOT SUPPRESSED. `.sf-flow-rail a:focus-visible` at the top
 * of this sheet rings every link in the rail, and a keyboard reader must be
 * able to see where they are — "no visual change" is a rule about REST, and a
 * focused control is not at rest.
 */
.sf-flow-line__link {
  color: inherit;
  text-decoration: none;
}

.sf-flow-line__link:hover {
  text-decoration: underline;
}

/*
 * ═══ W175 (Ryan, 2026-09-04): THE RAIL'S TILES GET A LIFT ═══
 *
 *   "I was talking about the 'Your Order' product thumbnail photos. You said
 *    the THOR Gold (with white background) was looking weak. The drop shadow
 *    should fix it." … "Give it to all, but it will help there the most."
 *
 * ─── WHY THE WHITE ONE IS THE WEAK ONE ──────────────────────────────────────
 *
 * The tile's own ground is `well` (#f1f0ec), but a photograph shot on white
 * FILLS it — so the THOR Gold tile renders as a white rounded square sitting on
 * the rail body, which is `surface` (#ffffff). White on white: the tile has no
 * edge at all, and the hoop appears to float in the row. Every other tile is
 * saved by its own photograph's ground, which is why Ryan's "it will help there
 * the most" is exactly right — and why the fix belongs on ALL of them rather
 * than on one: a tile's structure must not depend on what was photographed.
 *
 * ─── THE SCALING LAW IS THE PRECEDENTS' OWN, NOT A NEW OPINION ──────────────
 *
 * `card.css` sets the house lift for a CARD: `0 1px 2px / 6%` contact plus
 * `0 6px 18px / 8%` ambient. `installers.css` already had to scale that down
 * once, for the 100px profile disc, and wrote the rule down while doing it:
 * "the same ambient lift the card takes, HALVED IN REACH (`0 4px 12px`) — the
 * circle is a quarter of the card's size and an 18px blur under a 100px disc
 * reads as a smudge."
 *
 * A 44px TILE IS UNDER HALF THE DISC AGAIN, so the reach halves again: the
 * ambient lands at `0 2px 6px`. The contact layer tightens rather than
 * disappearing — at this size it is the layer doing the actual work of drawing
 * an edge under a white photograph — so it keeps its 1px offset and takes the
 * disc's 10% rather than the card's 6%.
 *
 * ─── IT COSTS NO GEOMETRY, WHICH IS WHY IT COULD BE DONE AT ALL ─────────────
 *
 * `box-shadow` paints outside the border box and does not participate in
 * layout (card.css §"IT COSTS NO GEOMETRY"), so every recorded MEASURE on the
 * rail is the number it was. What moves is PAINT — which the pixel gates do
 * see, so the rail crops are re-recorded against this change, attributed.
 *
 * THE WELL AND THE RADIUS STAY. This adds a shadow and nothing else: the tiles
 * that have no mirrored photograph draw `.sf-flow-well` instead and are
 * deliberately untouched — a drawn placeholder is not a photograph, and lifting
 * one would make an absent asset look like a present one.
 */
.sf-flow-line__thumb {
  background: var(--brand-color-well, #f1f0ec);
  border-radius: var(--radius-control);
  box-shadow:
    0 1px 2px rgb(23 24 26 / 10%),
    0 2px 6px rgb(23 24 26 / 8%);
  flex: none;
  object-fit: cover;
}

.sf-flow-well {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border-radius: var(--radius-inner);
  display: flex;
  flex: none;
  justify-content: center;
}

.sf-flow-well__label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: 9px;
}

/* ------------------------------------------------------- step 1A: the zip -- */

.sf-flow-zip {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 36px 40px;
  text-align: center;
}

.sf-flow-zip__title {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-product);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  text-wrap: wrap;
}

.sf-flow-zip__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  line-height: 1.55;
  max-width: 560px;
}

.sf-flow-zip__form {
  align-items: center;
  display: flex;
  gap: 10px;
  margin-top: 8px;
}

/* The board's `.input`, at the zip card's own 18px and 180px measure. */
.sf-flow-zip__field {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  /* NORMALISED (Ryan, 2026-08-26): the board sets this field at 18px, which is
   * the CARD-HEADER step of the scale (tokens §2) and one rung above every
   * other control on the site. The platform control size is the emphasis 16 —
   * the field's 48px height and its 180px measure are the board's and are
   * unchanged, so only the type came down. */
  font-size: var(--text-emphasis);
  /* MEASURED, NOT ASSUMED: this board's `.input` rule declares `box-sizing:
   * border-box` itself — it is the one place the drawing is not content-box —
   * so its 180 and its 48 are already border boxes and ship unchanged. (The
   * board's step dots are content-box, and they DO carry the arithmetic.) */
  min-height: 48px;
  padding: 0 14px;
  text-align: center;
  width: 180px;
}

.sf-flow-zip__field::placeholder {
  color: var(--brand-color-muted, #6c6a62);
  opacity: 1;
}

.sf-flow-zip__error {
  color: var(--brand-color-accent-strong, #9c420a);
  font-size: var(--text-small);
}

/* ------------------------------------------- step 1B: the tier-2 composition -- */

/*
 * THE STOCK CARD, RE-STACKED (Ryan, 2026-08-26). The board drew the delivery
 * doorway as a right-hand aside on the same row as the ship-today sentence; the
 * ruling gives it its OWN full line with air. So the card is a column: the
 * status/headline/note block, then the timeline strip under it. The 22px gap is
 * the §3 in-card rhythm — a rule between them would read as two cards, which is
 * what the single card is deliberately not.
 */
.sf-flow-stock {
  display: flex;
  flex-direction: column;
  gap: 22px;
  padding: 22px 26px;
}

.sf-flow-stock__status {
  align-items: center;
  display: flex;
  gap: 10px;
  /* THE BOARD'S ROW IS 18 TALL, and the reason is worth writing down: it draws
   * the in-stock mark as a ● GLYPH, which falls back to a font with taller
   * metrics and grows the line box. The mark here is a drawn box instead — a
   * glyph's size and baseline are a fallback font's business, and this dot has
   * to be the same dot everywhere — so the row states the height the board
   * measures rather than inheriting it from whatever font a machine has. */
  min-height: 18px;
}

/*
 * W10 — THE STATUS THAT IS NOT "IN STOCK".
 *
 * `pdp-stock.ts`'s first transplant rule, applied to this card: the word is the
 * config's, verbatim, and ANYTHING that is not exactly "In-Stock" draws in the
 * warning ink — which is what the legacy's own `class="red"` does with every
 * status it has ever had. Same box, same dot, same metrics as the good tone, so
 * the card's geometry does not move when a hoop goes on backorder.
 */
.sf-flow-stock__warn {
  align-items: center;
  color: var(--brand-color-accent-strong, #9c420a);
  display: inline-flex;
  font-size: var(--text-small);
  font-weight: 700;
  gap: 7px;
}

.sf-flow-stock__good {
  align-items: center;
  color: var(--brand-color-good, #1f7a4d);
  display: inline-flex;
  font-size: var(--text-small);
  font-weight: 700;
  /* MEASURED off the board: its "● In stock" run puts the word at 10.984px from
   * the span's left edge (an 11px glyph advance including the space). The dot
   * here is 7px, so the gap is what is left of that measurement — the board's
   * own spacing, reproduced without depending on which font a machine falls
   * back to for ●. */
  gap: 3.984375px;
}

/* The board draws the in-stock mark as a ● glyph in the text run. Drawn as a
 * box instead: a bullet glyph's size and baseline are the text font's business,
 * and this one has to be the same dot on every platform. */
.sf-flow-stock__dot {
  background: currentcolor;
  border-radius: 50%;
  display: inline-block;
  height: 7px;
  width: 7px;
}

/*
 * `.sf-flow-stock__zip` STOOD HERE AND IS GONE (W9, 2026-09-01). The stock line
 * used to print "· 30305" — the only place step 1 showed what the shopper had
 * typed, and with no way to change it. The zip now lives once, in
 * `.sf-flow-zipbar`, which carries the way back with it.
 */

.sf-flow-stock__line {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-card-header);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  margin-top: 6px;
  text-wrap: wrap;
}

.sf-flow-stock__note {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  line-height: 1.5;
  margin-top: 4px;
}

/* A FULL LINE NOW, so it takes the card's measure and reads left like
 * everything above it. (It used to be an aside that shrank against the sentence
 * beside it; there is no sentence beside it any more.) */
.sf-flow-stock__aside {
  text-align: left;
}

.sf-flow-stock__aside-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin-top: 6px;
}

/*
 * THE INSTALL PAIR. Two equal tracks on the board; after Ryan's 2026-08-26
 * ruling the desktop rung below flips them and gives the installation service
 * the larger one. This declaration is the SHARED base and the phone's shape.
 */
.sf-flow-pair {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 20px;
}

/* The board draws the two cards at DIFFERENT paddings — 20/22 on the
 * professional card, 22/26 on the self-install one. Transplanted as drawn
 * (playbook §3: no normalisation). */
.sf-flow-pair__card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px 22px;
}

.sf-flow-pair__card--self {
  padding: 22px 26px;
}

/* The card's own call to action, pushed to the bottom of the column so the two
 * cards' buttons sit on one line however far their bullets run. */
.sf-flow-pair__action {
  margin-top: auto;
}

.sf-flow-pair__head {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  display: flex;
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-emphasis);
  font-weight: 700;
  gap: 10px;
  letter-spacing: normal;
  line-height: normal;
  text-wrap: wrap;
}

/* A bare <svg> in an inline box picks up the line box's descender; the flex
 * wrapper blockifies it (playbook §5). */
.sf-flow-pair__mark {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  flex: none;
}

.sf-flow-pair__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  line-height: 1.55;
}

/* Money is a mono duty (tokens §2), in a sentence as anywhere else. */
.sf-flow-pair__price {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
}

/*
 * CONTINUE ACTIONS RIGHT-ALIGN — the platform rule (Ryan, 2026-08-26; tokens
 * §3), first recorded on this page. The board centred this block under the two
 * cards; the rule puts the primary and its "Up next" line at the right edge of
 * the column, which is where the eye leaves a step.
 */
.sf-flow-continue {
  align-items: flex-end;
  display: flex;
  flex-direction: column;
  margin-top: 26px;
  text-align: right;
}

/* The same block INSIDE a card — tier 1's "Add install & continue" — where the
 * card's own padding is the margin and the column above it already has one. */
/*
 * W4 — THE ACTIONS BLOCK INSIDE THE INSTALL CARD.
 *
 * It carried `margin-top: 0`, which put the quiet link's underline flush against
 * the money row above it and the button flush against the link. The three things
 * in this block are three different decisions — see every model's price, add the
 * install, and what comes next — and they now read as three.
 *
 * THE GAPS ARE "UP NEXT"'s, which is the reference Ryan named: that line sits
 * 10px under its button, so the link sits 12px above it and the block sits 18px
 * under the money row. Nothing is centred and nothing is stretched; the column
 * stays right-aligned, which is the platform rule for anything advancing a
 * multi-step flow (tokens §3).
 */
.sf-flow-continue--incard {
  margin-top: 18px;
}

/*
 * `.sf-flow-continue--incard .sf-flow-quiet--button` STOOD HERE AND IS GONE
 * (W29, 2026-09-02).
 *
 * It gave 12px of air above "See install pricing for every model" — the only
 * quiet button that has ever been in this block — and Ryan removed that link:
 * "we're in the order flow, the model is already chosen. Buttons move up."
 *
 * SWEPT WITH THE MARKUP, which is the `.sf-inst-est__go` lesson (W12) written
 * down: that rule survived the deletion of its only control, was carried forward
 * through a stylesheet audit as a live control to re-tokenise, and was caught
 * only when installers.css finally got the orphan sweep flow.css already had.
 * The class-level sweep cannot see a dead DESCENDANT combination — both halves
 * are still used elsewhere — so this one is swept by hand, in the same edit that
 * removed the control.
 *
 * WHAT THE BLOCK IS NOW: the primary and its "Up next" caption, which carries
 * its own 10px. That is the two-part composition W4's numbers were derived for,
 * minus the link that sat above it.
 */

/* The rule applied to a control that is not in a `.sf-flow-continue` column of
 * its own: tier 1's "Continue without install", which is a paragraph. */
.sf-flow-continue--right {
  text-align: right;
}

/* ------------------------------------------- step 1B: the tier-1 composition -- */

/*
 * W9 — THE ZIP BAR. A pin, the zip, and the way back.
 *
 * ABOVE THE FIRST CARD AND OUTSIDE IT, which is deliberate: it belongs to the
 * STEP, not to the stock card or the service-centre card, and both 1B states
 * carry the same one in the same place. Legacy puts it in the same position for
 * the same reason (`#zip_edit`, where the zip form was).
 *
 * ═══ W22 — IT IS THE RESULTS HEADER, SO IT READS LIKE ONE (2026-09-02) ═══
 *
 * Ryan: "Results header (zip + change, top of results): too small. Bigger font,
 * air gap top AND bottom — it must clearly read 'these are the results for this
 * zip.'"
 *
 * IT WAS THE CAPTION REGISTER — 13.5 muted, the same rung "Up next" uses — on
 * the theory that it was "a statement of fact with an affordance on the end, not
 * a control". True of the affordance, wrong about the LINE: everything below it
 * on both 1B states is an answer to it, and a heading set two rungs under the
 * body it heads reads as a leftover rather than a frame.
 *
 * THREE CHANGES, and each is one of the three things he named:
 *
 *   BIGGER — `--text-body` (15), the reading floor these pages carry (the type
 *     floor ruling, 2026-08-22: "nothing under 13.5px on below-the-fold / flow
 *     boards"; this is a heading, so it clears the floor rather than sitting on
 *     it). The zip itself goes to `--text-emphasis` (16) and stays mono, so the
 *     value the line is about is the biggest thing in it.
 *   AIR ABOVE — 18px. It had none: it sat flush under the stepper band, which is
 *     what made it read as part of the chrome above rather than the head of the
 *     answer below.
 *   AIR BELOW — 12 → 16, so the gap under the line is smaller than the gap over
 *     it. That asymmetry is what binds a heading to what it heads.
 *
 * W27 — AND IT IS THE SAME BAR ON BOTH ARMS, which is the point of it being one
 * component in one place: `flow-step1.tsx` renders `<FlowZipBar>` as the first
 * child of both 1B states, and this is the only rule that sizes it. Measured
 * before this pass at 390 on both arms: x=15, y=261.516, w=360, h=16, 13.5px,
 * margin 0 0 12 — identical to the thousandth. `flowstates.mjs` now asserts that
 * rather than leaving it to hold by luck.
 */
.sf-flow-zipbar {
  align-items: center;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  font-size: var(--text-body);
  gap: 8px;
  margin: 18px 0 16px;
}

.sf-flow-zipbar__mark {
  /* FLEX, so the inline SVG cannot pick up its line box's descender and sit a
   * pixel low against the zip beside it (playbook §5, the glyph trap). */
  display: flex;
  flex: none;
}

/* W22: the zip is the biggest thing on the line, because it is what the line is
 * about. Mono and 600 unchanged — every value on this storefront is set in the
 * mono face. */
.sf-flow-zipbar__zip {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-emphasis);
  font-weight: 600;
}

/*
 * THE WAY BACK sits directly beside the zip rather than at the far end of the
 * row. Legacy hangs its pencil off the right of the zip text (`#zip_edit .btn {
 * right: -54px }`), and the reason is the same one: an affordance that changes a
 * value belongs next to the value, not across the page from it — at 360px a
 * `space-between` row would put the two at opposite margins with nothing between
 * them to say they were related.
 */
.sf-flow-zipbar__change {
  color: var(--brand-color-muted, #6c6a62);
  text-decoration: underline;
}

.sf-flow-zipbar__change:hover {
  color: var(--brand-color-ink-soft, #44464a);
}

/*
 * W3 — THE SERVICE-CENTRE CARD IS A COLUMN NOW, not a row.
 *
 * It used to be `space-between` with a "Details" link pinned to the right edge —
 * which at 360px put a four-word link on its own line against the far margin,
 * disowned from the name it belonged to. The disclosure it became is a full row
 * of its own under the name, and the table it opens is full width.
 */
.sf-flow-center {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 22px 26px;
}

.sf-flow-center__name-row {
  align-items: center;
  display: flex;
  gap: 10px;
  margin: 0;
}

.sf-flow-center__details {
  /* The table carries its own border; this is the air above it. */
  margin-top: 2px;
}

.sf-flow-center__action {
  margin: 0;
}

.sf-flow-center__eyebrow {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin-bottom: 6px;
}

.sf-flow-center__name {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-card-header);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  text-wrap: wrap;
}

.sf-flow-center__note {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  line-height: 1.5;
  margin-top: 2px;
}

/*
 * THE TIER-1 PAIR, the same two kinds of card as `.sf-flow-pair` under
 * different names. The board's own ratio already gave the install service the
 * wider track; the desktop rung below moves it to the RIGHT with the ruling,
 * and states the ratio there so both pairs are the same shape in one place.
 */
.sf-flow-crew {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 20px;
}

.sf-flow-crew__service {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 22px 26px;
}

.sf-flow-crew__self {
  background: var(--brand-color-paper, #fbfbfa);
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 22px 26px;
}

.sf-flow-crew__head {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-emphasis);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  text-wrap: wrap;
}

.sf-flow-crew__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  line-height: 1.55;
}

/*
 * The install's price, in the board's price-table treatment: the 2px money rule
 * above it, the label at 15/700, the figure at 30px mono in accent.
 *
 * THE BOARD ALSO DRAWS A SALE HERE — a struck $600 and a BACK TO SCHOOL SALE
 * badge over a $399 — and the commercial layer had one price and no promotions
 * at all, so neither was built.
 *
 * THE STRIKE IS BUILT NOW (2026-09-01), THE BADGE STILL IS NOT. The install
 * price comes from the destination ZONE's `installation_pricing` row, and those
 * rows genuinely carry a sale price beside a regular one — Tomball prices a THOR
 * Gold install at $600 struck to $399, which is exactly what the board drew. So
 * the strike is a FACT the data supports and it is drawn when the zone has one.
 * The badge is not: "BACK TO SCHOOL SALE" names a promotion nothing in the data
 * knows about, and a struck figure already says everything true here.
 */
.sf-flow-crew__price {
  align-items: center;
  border-top: var(--border-money);
  display: flex;
  justify-content: space-between;
  padding-top: 12px;
}

/*
 * ═══ W4 — THE TOTALS AREA, RE-SET (Ryan, 2026-09-01) ═══
 *
 * His words: "a mess on mobile — ~4 colors, 4 font sizes/weights (tiny struck
 * 600, humongous 399, grayed crowded link, big orange button)." He named "Up
 * next → Accessories" as the reference for spacing and treatment, and W1's
 * ruling — the CHECKOUT button's size is the canonical one — for the button.
 *
 * WHAT WAS ACTUALLY WRONG, counted off the 390px screenshot:
 *
 *   · FOUR TYPE SIZES in eight vertical pixels of information — 15 bold ink
 *     (label), 15 muted struck (was), 30 mono accent (figure), 16 bold white
 *     (button) — with a 13.5 muted link between them.
 *   · THE 30px FIGURE against a 15px label is a 2:1 jump. It is the largest
 *     thing on the whole step, louder than the step's own heading, for a number
 *     that is a line item on a $2,399 order.
 *   · NO AIR ANYWHERE: the quiet link's underline touched the button's top edge,
 *     which is what "grayed crowded link" is describing.
 *
 * WHAT IT IS NOW: TWO sizes and TWO colors in the money row, and every gap
 * declared. The figure stays the loudest thing in the block and stops being the
 * loudest thing on the page. Nothing here invents a size — 21 and 14.5 are both
 * on the platform scale and both already in use on this step.
 *
 * DESKTOP KEEPS THE BOARD'S 30px FIGURE (see the mobile rung at the foot of this
 * sheet). The board drew this card at 1440 where a 30px figure sits in a
 * 540px-wide card with room around it; the complaint, and the fix, are about
 * 360.
 */
.sf-flow-crew__price {
  /* Air on BOTH sides of the money rule, not just above it. The row used to
   * have 12px over it and nothing under, so it read as attached to the bullets
   * and detached from the actions it belongs to. */
  padding-bottom: 14px;
}

/*
 * W30 — WHAT IS PRICED, AND WHO DOES IT, IN THAT ORDER (Ryan, 2026-09-02).
 *
 * The label used to be one run: "Installation · Pro Dunk Tomball". Ryan: "what's
 * being priced is INSTALLATION IN THE ZIP — who services it is secondary." So
 * the run became a two-line column: the zip on the label's own rung, the centre
 * under it in the caption register.
 *
 * A COLUMN AND NOT TWO SIBLINGS, because the row is a `space-between` flex whose
 * right-hand pair is held there by `.sf-flow-crew__price-was`'s `margin-left:
 * auto`. A third top-level child would have been distributed into the middle of
 * the row, adjacent to nothing.
 */
.sf-flow-crew__price-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* It may give before the figure does — the figure is a mono price that must
   * not wrap, and this column holds two runs that can. */
  min-width: 0;
}

.sf-flow-crew__price-label {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  font-weight: 600;
}

/*
 * The centre, SECONDARY: the caption register in muted, which is the quietest
 * rung this step uses and the same one "Up next" and the option notes take.
 *
 * IT IS A THIRD TYPE SIZE IN A BLOCK W4 CUT TO TWO, and that is deliberate
 * rather than a regression: W4's count was of the MONEY ROW's own runs (a 15
 * bold label, a 15 struck was, a 30 accent figure, all on one line), and those
 * are unchanged. This run is a caption UNDER the label, in the register captions
 * already have here — and the alternative Ryan ruled out was leaving the
 * centre's name inside the label at the label's own weight.
 */
.sf-flow-crew__price-by {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  font-weight: 400;
}

/*
 * The zone's list price, struck. It shares the row with the label and the
 * figure, so it takes `margin-left: auto` rather than the row taking a third
 * `justify-content` value — the label stays hard left and the pair stays hard
 * right, which is what the board draws.
 */
.sf-flow-crew__price-was {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  margin-left: auto;
  margin-right: 10px;
  text-decoration: line-through;
}

.sf-flow-crew__price-figure {
  color: var(--brand-color-accent, #c4530a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-page-title);
  font-weight: 700;
}

/*
 * W5 — THE AIR RYAN ASKED FOR between "Watch a real install…" and "Continue
 * without install".
 *
 * `margin-top: auto` is what pushes the secondary to the bottom of the card, and
 * on DESKTOP that is the whole spacing: the card is as tall as the install card
 * beside it, so there is always slack for the auto margin to eat. On MOBILE the
 * cards are stacked and each is only as tall as its own content, so `auto`
 * resolves to ZERO and the button ends up directly under the link's underline —
 * which is exactly what he saw.
 *
 * A MINIMUM, not a replacement: `auto` still wins wherever there is room, so the
 * desktop composition is untouched and the phone gets a floor of 20px.
 */
.sf-flow-crew__secondary {
  margin-top: auto;
  padding-top: 20px;
}

/* ------------------------------------------------- step 2: the accessories -- */

/*
 * STEP 2 — ACCESSORIES, transplanted from
 * design/produnk/FlowAccessories.dc.html (drawn at 1440).
 *
 * The board's own numbers, verbatim: a 220px media column beside a flexing
 * body, a 168px stage over a five-tile strip at 40px, an 18px card padding, a
 * 20px column gap, and a control row unified at 40px — its caption's word, and
 * the reason `--control-height-sm` exists.
 *
 * TWO THINGS THE BOARD DRAWS THAT THESE RULES DO NOT, both recorded where they
 * are decided rather than here: a review count beside every name (no source in
 * the data — `accessories.ts`) and the quantity written into the rail line's
 * label as well as its qty column (said twice — `flow-rail.tsx`).
 *
 * ONE THING THESE RULES DRAW THAT THE BOARD DOES NOT: the ADDED state. The
 * board draws three unadded cards, so the treatment is derived out of the
 * platform's own selection language (tokens §3: 1.5px accent) rather than
 * invented — see `.sf-flow-acc__card[data-added]` for the arithmetic that gets
 * to 1.5 without moving anything.
 */

/*
 * ═══ W88 — THE TITLE SECTION IS GONE AND THE FILTERS TOOK ITS ROW ═══
 * (Ryan, 2026-09-03)
 *
 * "Remove the 'Goes with your hoop / Optional. Everything here ships…' title
 * section. Filters move UP in-line with the Continue button." So the head is
 * still the board's row — something at the left, the primary and its "Up next"
 * at the right — and what stands at the left is now the chips.
 *
 * `align-items: center` RATHER THAN THE BOARD'S IMPLICIT STRETCH: the row's two
 * children are a wrapping strip of 48px controls and a two-line Continue block,
 * and centring is what puts the chips on the primary's axis instead of on its
 * top edge.
 */
/*
 * ═══ W108 — THE FILTERS TOP-ALIGN ON THE BUTTON (Ryan, 2026-09-04) ═══
 *
 * "Category filter row top-aligns with the CONTINUE BUTTON (currently centered
 * against button + 'up next' stack)."
 *
 * MEASURED BEFORE AND AFTER, because the words name a cause and the fix has to
 * answer the cause: at 1440 the row's right-hand child is a COLUMN — a 48px
 * button with an 18px "Up next" line under it, 76 tall — and `align-items:
 * center` was centring the 48px chip strip against that 76, so the chips sat 14
 * pixels below the button they are meant to line up with (button y 318, chips y
 * 332). `flex-start` puts both tops on 318, which is the ruling exactly.
 *
 * IT IS THE ROW'S RULE, NOT THE CHIPS', so the two children agree by
 * construction rather than by a margin that would have to be re-derived every
 * time the "Up next" line changes height.
 */
.sf-flow-step2__head {
  align-items: flex-start;
  display: flex;
  gap: 24px;
  justify-content: space-between;
}

/*
 * THE HEAD'S CONTINUE CARRIES NO TOP MARGIN. `.sf-flow-continue` is the block
 * that sits UNDER a step's content and buys its own air; in the head row it is
 * a column beside the filters, and the 26px it brings with it made the row 24px
 * taller than the board draws it. Measured against the board rather than
 * eyeballed — the gate's crop-size assertion is what named the number.
 */
.sf-flow-step2__head .sf-flow-continue {
  margin-top: 0;
}

/*
 * `.sf-flow-step2__title` AND `.sf-flow-step2__sub` STOOD HERE AND ARE GONE
 * (W88). The heading named the step the STEPPER directly above it already
 * names, and the muted sentence said what the delivery note on every card says.
 * Deleting the rules rather than tokenising a block nobody renders is the
 * lesson `.sf-inst-est__go` left in installers.css: a tidier corpse is still a
 * corpse, and `tests/order-flow.test.ts`'s orphan sweep is what keeps the two
 * honest.
 *
 * A KNOCK-ON WORTH NAMING: this page had TWO `<h1>`s — the chrome's title band
 * and this one. It now has the band's, alone.
 */

/* The chip row, in the head row now: no top margin of its own (the flex row
 * owns the spacing) and it may wrap under itself on a narrow measure. */
.sf-flow-step2__facets {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/*
 * ═══ W88 — A FILTER IS A BUTTON'S HEIGHT (Ryan: "are you sure we're keeping
 * our button consistent?") ═══
 *
 * The board drew these as `.ghost-sm` chips — 13px/500 inside a box that
 * measured 32 tall — and that was defensible while they sat on a row of their
 * own under a heading. W88 stands them BESIDE Continue, and a 32px control next
 * to a 48px one in the same row is the exact defect BTN-UNIFY was written to
 * catch. Ryan named it on sight.
 *
 * SO THEY TAKE THE PRIMARY RUNG, whole: 48px tall, 16/700 type, the house's
 * `--radius-control`. Not the compact 40 — the ruling is "the same height as
 * Continue", and Continue is the page's primary.
 *
 * AND THEY JOIN THE SWEPT FAMILY. `btngate` used to EXCLUDE `sf-flow-facet`
 * with the note "chips = pill; a filter is not an action". Two things about
 * that exclusion were wrong here and are recorded in the gate itself: these
 * chips never were pills (they have drawn `--radius-control` since they were
 * built), and a filter standing in a row of actions is judged by the row it is
 * in. The exclusion still holds for the genuine pills it was written for.
 *
 * IT IS A LINK, not a toggle — the filter is in the URL — so the UA underline
 * has to be said off, and the state it carries is `aria-current` rather than
 * `aria-pressed` (see the component). `inline-flex` + `justify-content` because
 * a declared height needs its label centred in it; `display: inline-block` on a
 * 48px box would leave the words riding the top.
 */
.sf-flow-facet {
  align-items: center;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  font-size: var(--button-text, 16px);
  font-weight: var(--button-weight, 700);
  justify-content: center;
  min-height: var(--control-height, 48px);
  padding: 0 18px;
  text-decoration: none;
}

/* THE ACTIVE CHIP IS THE ACCENT BORDER at the board's own weight — the site's
 * selection language, and the board draws it at 1px here rather than the 1.5
 * the rail's chips use. Board values ship verbatim (playbook §3).
 *
 * ITS WEIGHT IS THE RUNG'S NOW (W88). The board's 600-on-selection was a step
 * up from an unselected 500; at the primary rung both are 700, and a selected
 * chip that changed WEIGHT as well as border colour would also change width,
 * which moves every chip after it. The accent edge is the whole signal. */
.sf-flow-facet--on {
  border-color: var(--brand-color-accent, #c4530a);
}

.sf-flow-step2__empty {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-body);
  margin: 18px 0 0;
}

/* The board's foot: 26px of air under the last card. Its Continue is CENTRED on
 * the drawing and right-aligned here — the platform rule (tokens §3, Ryan
 * 2026-08-26) postdates the board and was recorded on this flow's step 1. */
.sf-flow-step2__foot {
  margin-top: 26px;
}

/* ---- the card list ---- */

/*
 * THE AIR ABOVE THE FIRST CARD IS W88'S — Ryan asked for a "nice air gap
 * between that row and the cards", and the row it is measured from is now a row
 * of 48px controls rather than a heading and a chip strip. 32px is the site's
 * own section step (the checkout's cards are 24 apart, its sections 32), so the
 * gap says "a new region starts here" in a number the page already uses. The
 * 18 that stood here was the gap under a chip row that was itself 18 under a
 * heading — it never had to separate two regions on its own.
 */
.sf-flow-acc {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 32px;
}

/* THE FRAME IS `.sf-card`'s (W89) — ground, border, radius and lift. What is
 * left here is what is genuinely this card's. See card.css's header for why a
 * frame property must not come back to this rule. */
.sf-flow-acc__card {
  display: flex;
  gap: 20px;
  padding: var(--sf-flow-acc-pad);

  /*
   * THE CARD'S OWN INSET, NAMED — the installer card's `--sf-inst-card-pad`
   * move, for the same reason. The review view (W86) bleeds to the card's edge
   * by cancelling this number and pays it back as its own row padding, so the
   * two have to BE one number: a bar that bled 18 and paid back 16 would sit a
   * hair off-centre against the bullets it replaced.
   */
  --sf-flow-acc-pad: 18px;
}

/*
 * ON THE ORDER — derived (no board draws it). The platform's selection language
 * is a 1.5px accent perimeter, and a card cannot simply take one: changing a
 * 1px border to 1.5 moves every pixel inside it by half. So the card keeps its
 * 1px border, in accent, and an inset 0.5px shadow completes the 1.5 — nothing
 * reflows, and the perimeter measures what the token says.
 *
 * IT COMPOSES THE LIFT RATHER THAN REPLACING IT (W89). `box-shadow` is one
 * property: a bare `inset` here would silently delete the shared card's two
 * shadow layers, so an ADDED card would be the only flat card in the list. The
 * inset ring is drawn FIRST — shadow layers paint front to back — so the accent
 * perimeter sits on top of its own lift.
 */
.sf-flow-acc__card[data-added] {
  border-color: var(--brand-color-accent, #c4530a);
  box-shadow:
    inset 0 0 0 0.5px var(--brand-color-accent, #c4530a),
    var(--sf-card-lift);
}

/* ---- the media column ---- */

/*
 * ═══ W107 — THE MEDIA COLUMN FILLS ITS HEIGHT (Ryan, 2026-09-04) ═══
 *
 * "Desktop: wasted space under the left photo — thumbnails become a 2x2 GRID
 * (four photos = two rows of two)."
 *
 * MEASURED, at 1440, before the change: the stage ended 214px into a column the
 * card stretched to 272-546px, so every card carried between 58 and 332 pixels
 * of nothing under a single 40px row of tiles. The row was five tiles wide
 * because the board drew five; it is four now (`ACCESSORY_THUMB_SLOTS`), which
 * is what "two rows of two" means.
 *
 * A COLUMN THAT FILLS, NOT A TALLER FIXED STRIP. The card's height is set by
 * whichever column is longer, and the body column's length varies with the
 * description — so any FIXED strip height is right for one card and wrong for
 * the other four. `flex: 1` on the grid with `1fr` rows spends whatever is left,
 * on every card, at every content length: the dead space is gone by
 * construction rather than by a number that happened to fit today.
 *
 * WITH A FLOOR AND A CEILING, because "fills the space" alone has two bad ends:
 * the shortest card would squeeze the tiles under the useful size, and the pole
 * pad's card — whose 190px lettering simulator makes it half again as tall as
 * any other — would draw four tiles 200px tall. 72 and 110 bound it; the pole
 * pad card keeps some dead space, which is honest, and it is FLAGGED rather than
 * chased with a special case.
 */
.sf-flow-acc__media {
  display: flex;
  flex: none;
  flex-direction: column;
  width: 220px;
}

/* The board's stage: a 168px well. It is a BUTTON, so the UA's own control
 * paint has to be said off — `.sf-flow-well` supplies the ground.
 *
 * `overflow: hidden` SINCE W87: it holds a photograph now, and the well's own
 * radius has to clip it. */
.sf-flow-acc__stage {
  border: 0;
  cursor: pointer;
  height: 168px;
  overflow: hidden;
  padding: 0;
  position: relative;
  width: 100%;
}

/*
 * ═══ W87 — THE PHOTOGRAPH ON THE STAGE ═══
 *
 * `object-fit: cover` because the box is a FIXED 220 x 168 the board drew for a
 * placeholder that had no aspect ratio, and the catalog's photographs run from
 * 3:2 landscape to 2:3 portrait. Letterboxing them into it would draw the
 * media-well's dark ground down both sides of half the cards.
 *
 * IT COVERS THE MONO LABEL rather than replacing it: the label is still in the
 * markup, still positioned by `.sf-flow-well`, and a document whose photograph
 * has not been mirrored draws it exactly as before. `inset: 0` + `position:
 * absolute` is what lets one element sit over the other without either of them
 * changing the button's box.
 */
.sf-flow-acc__stage-img {
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

/*
 * TWO COLUMNS, ROWS AS NEEDED (W107). Four slots make exactly two rows; the
 * board's 5px gap and 6px lead are unchanged, because what Ryan moved is the
 * arrangement, not the spacing.
 *
 * `minmax(0, 1fr)` ON BOTH AXES, not a bare `1fr`: an `fr` track's automatic
 * minimum is its MIN-CONTENT, and these tiles hold a mono label that would floor
 * the column and push the grid past the 220px media width — the same trap
 * `.sf-flow__grid` records one screen up.
 */
.sf-flow-acc__thumbs {
  display: grid;
  flex: 1;
  gap: 5px;
  grid-auto-rows: minmax(0, 1fr);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 6px;
  /* Two 110px rows plus the gap, and two 72px rows plus the gap. See the media
   * column's note for what each end is protecting against. */
  max-height: 225px;
  min-height: 149px;
}

/* A GRID ITEM NOW (W107). `flex: 1` and the board's fixed 40px height were what
 * made it one of five in a row; the grid sizes it on both axes, so the tile is
 * whatever its cell is and the `min-width: 0` that kept a flex item from being
 * floored by its label is now carried by the track's own `minmax(0, 1fr)`. */
.sf-flow-acc__thumb {
  border: 0;
  cursor: pointer;
  min-width: 0;
  overflow: hidden;
  padding: 0;
  position: relative;
}

/* W87: the tile's photograph — the same cover-and-clip the stage makes, at the
 * strip's 40px. The tile draws the 280px derivative, which is the file the
 * shared viewer's own 44px rail draws: one rendition, two consumers. */
.sf-flow-acc__thumb-img {
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

/* The board's `+N` tile: the dark media-well end of the gradient, white and
 * bold, which is the signpost reading as a control rather than a photograph. */
.sf-flow-acc__thumb--more {
  align-items: center;
  background: var(--brand-color-media-well-a, #23262e);
  border-radius: var(--radius-inner);
  display: flex;
  justify-content: center;
}

.sf-flow-acc__thumb-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: 7px;
}

.sf-flow-acc__thumb--more .sf-flow-acc__thumb-label {
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-micro);
  font-weight: 700;
}

/* The play badge: a 14px accent circle in the tile's corner, the mark centred
 * in it. */
.sf-flow-acc__play {
  align-items: center;
  background: var(--brand-color-accent, #c4530a);
  border-radius: 50%;
  bottom: 3px;
  color: var(--brand-color-surface, #ffffff);
  display: flex;
  height: 14px;
  justify-content: center;
  position: absolute;
  right: 3px;
  width: 14px;
}

/* ---- the body column ---- */

/*
 * ═══ W106 — FOUR SECTIONS, WITH REAL AIR BETWEEN THEM (Ryan, 2026-09-04) ═══
 *
 * "The card reads as four sections — (1) name+price (2) reviews (3) stocking
 * (4) description — with real air BETWEEN sections; and WITHIN stocking, 'In
 * stock' and 'delivers with your hoop…' move CLOSER (they're related and it
 * doesn't read). The existing website has done a very good job of that."
 *
 * ─── THE REFERENCE, MEASURED RATHER THAN RECALLED ───────────────────────────
 *
 * Ryan named the live site, so the live site was read: the accessory detail
 * page's right column authors its rhythm in plain numbers —
 *
 *     #order_now_top    the price row
 *     #rating           margin-top: 20px      (the tight pair after the price)
 *     #description      margin-top: 40px      (a section step)
 *     #description_bullets  margin: 40px 0 0 20px, each bullet margin-bottom: 10px
 *     #specs            margin-top: 70px      (a different KIND of block)
 *
 * So the site's answer is a RATIO, not a number: sections are four times the
 * intra-list gap (40 against 10), and a different kind of block is bigger again.
 * What was here before had ONE gap — 8px — between all five rows, which is
 * precisely "it doesn't read as sections".
 *
 * ─── WHY 18 AND NOT 40 ──────────────────────────────────────────────────────
 *
 * The legacy 40 is 2.5x its 16px body, set in a 900px detail column. Scaling
 * that ratio to this card's 13.5px body gives ~34px, which is more air than the
 * card's own 18px padding — a card whose internal gaps exceed its inset reads as
 * four cards, not one. So the section step is THE CARD'S OWN INSET,
 * `--sf-flow-acc-pad`: one number, already declared, and the internal rhythm
 * then agrees with the frame around it by construction. Against the 8px that
 * stays INSIDE a section that is a 2.25:1 contrast — the legacy's own tight pair
 * is 20 against 10, which is 2:1.
 */
.sf-flow-acc__main {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--sf-flow-acc-pad);
  min-width: 0;
}

/*
 * SECTION FOUR IS ONE SECTION. The paragraph and the bullets are the same claim
 * at two grains — the legacy site authors them as one block — so they share a
 * wrapper, the wrapper takes the section step above, and inside it the pair
 * keeps the tight 8 that used to separate everything.
 */
/*
 * A BLOCK, NOT A FLEX COLUMN (W185). It was a flex column so its two children
 * could share one `gap`; a FLOAT does not apply to a flex item and text does
 * not wrap around one, so the film player Ryan asked to "right align in the
 * description and bullet section" could not have worked inside it. The 8px is
 * a margin now — one number, moved, not lost.
 */
.sf-flow-acc__about {
  display: block;
}

/* THE TIGHT STEP W106 PUT BETWEEN THE PARAGRAPH AND ITS BULLETS, restated as a
 * margin now that the section is a block. `:first-child` because the paragraph
 * is absent on a document that writes none, and the list must not then push
 * itself away from the section's own top. */
.sf-flow-acc__about > .sf-flow-bullets:not(:first-child) {
  margin-top: 8px;
}

.sf-flow-acc__head {
  align-items: baseline;
  display: flex;
  gap: 12px;
  justify-content: space-between;
}

/* The board's `.h3` at its card size. globals.css styles every h1-h3 with the
 * heading face at weight 500 and a negative track; the board draws the family's
 * own 700 at 16, so both are restated. */
/*
 * ═══ W105 — THE NAME MATCHES THE PRICE (Ryan, 2026-09-04) ═══
 *
 * "Accessory NAME matches the PRICE's size (bump up)." Measured: the name drew
 * `--text-emphasis` (16) beside a `--text-product` (21) price, so the figure was
 * the loudest thing on a card whose subject is the product. They are the two
 * halves of one row and they now take one size — `--text-product`, the price's
 * own token, read rather than restated so the pair cannot drift apart.
 *
 * The board's own 700 and its `letter-spacing: normal` are unchanged (globals
 * styles every h1-h3 with the heading face at weight 500 and a negative track;
 * the board draws the family's own 700, so both are still restated here).
 */
.sf-flow-acc__name {
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-product);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  margin: 0;
  text-wrap: initial;
}

/* Money is a mono duty (tokens §2). */
.sf-flow-acc__price {
  flex: none;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-product);
  font-weight: 700;
}

/*
 * ═══ W110 — THE MARKDOWN (Ryan, 2026-09-04: "check your data") ═══
 *
 * The data was right and this step was asking it the wrong question — see
 * `order/accessories.ts`'s `packageIdOf`. What is drawn is the product page's
 * own two-span shape (`.sf-pdp-buy__total-money`: muted struck figure, then the
 * price it sells at), which is ALSO what the live accessory page draws in
 * `#order_now_top` — `span.retail { text-decoration: line-through; color:
 * light-gray; font-weight: normal }` beside `span.total`. Two independent
 * precedents, one shape; nothing here is invented.
 *
 * THE STRUCK FIGURE IS ONE STEP DOWN in size as well as in colour. The product
 * page draws its strike at `--text-body` against a `--text-page-title` price;
 * the same relation here is `--text-small` against `--text-product`, so the
 * pair reads as one price with a history rather than as two prices.
 */
.sf-flow-acc__money {
  align-items: baseline;
  display: inline-flex;
  flex: none;
  gap: 8px;
}

.sf-flow-acc__strike {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-small);
  font-weight: 400;
  text-decoration: line-through;
}

/*
 * ═══ W85 — THE REVIEW LINE, SECOND IN THE CARD ═══
 * (Ryan, 2026-09-03: "product name, reviews, stocking, description, bullets")
 *
 * The board drew "· 306 reviews" beside the name and this card never drew it,
 * because when it was built nothing joined an accessory to the legacy review
 * corpus. It does now (see the route's `page.tsx`), and 306 is the real figure.
 *
 * IT IS ITS OWN ROW rather than a run inside the head, which is Ryan's order
 * read literally — and it is also what keeps the price on the head row's right
 * edge where the board put it, instead of pushing it past a star row.
 */
.sf-flow-acc__rating {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

/* The stars, in the accent — the site's own review colour, and the same glyph
 * the product page and the installer card draw. `letter-spacing` because five
 * `★` at a text face's default tracking read as a smear at this size. */
.sf-flow-acc__stars {
  color: var(--brand-color-accent, #c4530a);
  font-size: var(--text-small);
  letter-spacing: 1px;
  line-height: 1;
}

/* The tail a corpus has not earned. Drawn rather than dropped, so five cards
 * with different averages keep one star-row width and the counts under them
 * line up. */
.sf-flow-acc__stars-rest {
  color: var(--brand-color-line-mid, #d8d6d0);
}

/* The score itself, in mono — a measured figure is a mono duty (tokens §2), the
 * same call the money and the quantity make on this card. */
.sf-flow-acc__score {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-small);
  font-weight: 700;
}

/*
 * W86: THE COUNT IS THE DOOR. It opens the reviews in place, so it is a BUTTON
 * wearing the quiet link's treatment — the same call `.sf-flow-rail__edit` used
 * to make and the same one the installer card's "203 Reviews" makes.
 *
 * DELIBERATELY NOT IN `btngate`'s FAMILY: underlined text is not a button and
 * must not grow a box (the gate's own "QUIET LINKS" exclusion, which already
 * names `sf-flow-quiet` and `sf-pdp-quiet`). Its target is the line's height
 * plus the row's padding, which is the same floor the installer card's is.
 */
.sf-flow-acc__reviews {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-small);
  padding: 0;
  text-decoration: underline;
}

.sf-flow-acc__reviews:hover {
  color: var(--brand-color-ink, #17181a);
}

/*
 * W106, THE SECOND HALF: "In stock" and the delivery note are ONE THOUGHT.
 *
 * 12 -> 8. They are two runs on one line, and at 12px apart — with the section
 * step above and below them now at 18 — they read as two facts that happen to
 * share a row. 8 is the same tight value section four uses INSIDE itself, so
 * the card has exactly two spacings: a section step and an intra-section step.
 * That is the whole of what makes four sections legible, and it is the legacy
 * page's own 40/10 discipline at this card's scale.
 */
.sf-flow-acc__stock {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

/*
 * W83 — THE DESCRIPTION, between the stocking line and the bullets.
 *
 * IT WEARS `.sf-flow-bullets`' OWN VOICE — `ink-soft`, `--text-small`, 1.6 —
 * and that is deliberate rather than lazy: on the legacy site the paragraph and
 * the bullets are ONE authored block (`description` and `description_bullets`
 * on the same config), and drawing the prose half a size up would present them
 * as two claims of different rank. A `max-width` because a paragraph running
 * the full body column at this size is past the comfortable measure, where a
 * bulleted line is not.
 */
.sf-flow-acc__desc {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  line-height: 1.6;
  margin: 0;
  max-width: 64ch;
}

.sf-flow-acc__good {
  align-items: center;
  color: var(--brand-color-good, #1f7a4d);
  display: inline-flex;
  font-size: var(--text-small);
  font-weight: 700;
  gap: 6px;
}

/* The board types a ● glyph in the run; drawn, for the reason the stock card's
 * own dot is (a glyph's optical centring is the text font's, not ours). */
.sf-flow-acc__dot {
  background: var(--brand-color-good, #1f7a4d);
  border-radius: 50%;
  display: inline-block;
  height: 7px;
  width: 7px;
}

.sf-flow-acc__note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
}

/* ---- the personalisation simulator ---- */

/*
 * The board's own module, inside the description: a 260px pad photo beside the
 * field, on `paper` inside a hairline box.
 */
/*
 * `overflow: hidden` SINCE W186: the photograph bleeds to this box's edges now,
 * and the box has a radius, so it has to clip its own corners.
 */
.sf-flow-acc__sim {
  align-items: center;
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: grid;
  gap: 16px;
  grid-template-columns: 260px minmax(0, 1fr);
  overflow: hidden;
  padding: var(--sf-flow-acc-sim-pad);

  /* THE MODULE'S OWN INSET, NAMED — the installer card's `--sf-inst-card-pad`
   * move, for the same reason. W186's bleed cancels this number and the padding
   * pays it, so the two have to BE one number or the photograph lands a pixel
   * off the module's edge. */
  --sf-flow-acc-sim-pad: 14px;
}

/*
 * ═══ W178 — THE LEGACY COMPOSITE (Ryan, 2026-09-04) ═══
 *
 * "Look very closely at the existing website... we need to copy that."
 *
 * WHAT THIS REPLACES: a 190px `well` with a mono word in it, invented in batch
 * 9 because no pad photograph had been mirrored. The legacy site has had the
 * real thing since 2022 — `images/order/pole_pad_lettering.png`, a 338 x 505
 * cut-out of an in-ground adjustable hoop with its pole pad — and it is
 * mirrored now beside the accessory stills.
 *
 * NO DECLARED HEIGHT ANY MORE. The box is the photograph's, so its aspect comes
 * from the `<img>`'s own intrinsic size and the composite cannot letterbox or
 * crop at any width. That is also what lets the strip's percentages be true:
 * they are fractions of a box that is exactly the image.
 *
 * `container-type: inline-size` IS LOAD-BEARING, not decoration — it is what
 * `--sf-pad-font`'s `cqw` unit measures against, and it is why the lettering
 * keeps the legacy's 13-on-338 RATIO instead of its 13 literal pixels. The
 * legacy has no media query for this simulator at all, so its own fixed offsets
 * drift off the pad on a phone; holding the ratio is the one place this
 * transplant deliberately improves on its source, and it is flagged as such.
 */
/*
 * ═══ W186 — THE HOOP GOES FULL BLEED (Ryan, 2026-09-04) ═══
 *
 * "The lettering simulator's hoop photo goes full bleed to the section — the
 * current gap looks odd."
 *
 * It did look odd, and the reason is that the composite is a CUT-OUT on
 * transparency: the module's 14px padding put a band of `paper` above a
 * photograph whose own top edge is already empty sky, so the hoop appeared to
 * float in a margin rather than to sit in the panel. Cancelling the padding on
 * three sides — top, left and bottom — lands the photograph on the module's own
 * edges; the RIGHT side keeps the grid's 16px gap, because that is the gutter
 * between the picture and the field beside it, not padding.
 *
 * THE STRIP DOES NOT MOVE, and that is by construction rather than by luck: its
 * four offsets are FRACTIONS of the image box (W178), so widening the image by
 * the 14px it just reclaimed scales the lettering with it. Asserted after the
 * bleed by `accgate`'s six measured ratios and its driven typing check, which
 * is the whole reason those were written as ratios in the first place.
 */
.sf-flow-acc__sim-pad {
  container-type: inline-size;
  margin: calc(-1 * var(--sf-flow-acc-sim-pad, 14px)) 0
    calc(-1 * var(--sf-flow-acc-sim-pad, 14px))
    calc(-1 * var(--sf-flow-acc-sim-pad, 14px));
  position: relative;
}

.sf-flow-acc__sim-pad-img {
  display: block;
  height: auto;
  width: 100%;
}

/*
 * THE LETTERS STAND ON THE PAD, one glyph per line — the legacy's own
 * `#ppl_preview`, transplanted (W178).
 *
 *     #ppl_preview{ position:absolute; left:30px; top:232px; width:25px;
 *                   height:210px; color:white; font-size:13px;
 *                   text-align:center; font-weight:bold; }
 *
 * The four offsets arrive as custom properties because they are FRACTIONS of
 * the photograph, computed from the legacy's integers in the component — see
 * `accessories.ts` for the numbers and `flow-content.ts` for the one derivation
 * (the legacy measures `left` from its own box, not from the image).
 *
 * THE SCRIM IS GONE, and that is a correction rather than a loss: batch 9 drew
 * a 55% black panel behind the glyphs to keep white vinyl legible over "a
 * photograph that does not exist yet and will not be uniformly dark when it
 * does". The photograph exists now and it IS uniformly dark — the strip lands
 * on the black pad, which is why the legacy needs no scrim either.
 *
 * `white-space: pre-line` IS THE LEGACY'S `<br>`s. It writes one character per
 * line into a single element; this holds them in a single text-shaping run,
 * which `pre-line` breaks on the newlines the component joined with. A blank
 * line is a SPACE, which the whitelist allows and the legacy renders the same
 * way.
 *
 * 1.15 IS ARIAL'S OWN `normal` AT 13px, MEASURED (~15px), pinned so the stack
 * does not depend on whatever `normal` our body face computes to. Thirteen
 * characters then occupy ~195 of the strip's 210 legacy pixels, which is the
 * fit the legacy ships.
 */
.sf-flow-acc__sim-letters {
  color: var(--brand-color-surface, #ffffff);
  /*
   * ═══ W188 — THE COLLEGIATE FACE (Ryan, 2026-09-04) ═══
   *
   * "See if you can find a blocky collegiate font for the pole pad lettering."
   *
   * THIS IS THE ONLY RULE ON THE SITE THAT NAMES IT, deliberately: the face is
   * part of a PICTURE of cut vinyl, not part of the storefront's typography, and
   * `flow-lettering-font.tsx` (which carries the whole comparison against the
   * real product) explains why it is not the brand's own `googleFontsUrl`.
   *
   * 400, NOT 700. Graduate ships one weight; asking for bold makes the browser
   * synthesise it and smears the thin slab serifs that made it the right face.
   * The legacy's own preview is Arial BOLD, so this is a deliberate departure
   * from the transplant — and the one W178 flagged when it shipped.
   *
   * THE FALLBACK IS A REAL SLAB STACK, not `serif` alone: Rockwell ships with
   * Office on Windows and Roboto Slab is common on Android, so a reader who
   * never gets the webfont still sees slab lettering rather than a book face.
   */
  font-family: "Graduate", Rockwell, "Roboto Slab", Georgia, serif;
  font-size: var(--sf-pad-font, 3.846cqw);
  font-weight: 400;
  height: var(--sf-pad-h, 41.584%);
  left: var(--sf-pad-left, 47.337%);
  line-height: 1.15;
  position: absolute;
  text-align: center;
  top: var(--sf-pad-top, 45.941%);
  white-space: pre-line;
  width: var(--sf-pad-w, 7.396%);
}

/*
 * `.sf-flow-acc__sim-letters:empty` AND `.sf-flow-acc__sim-letter` STOOD HERE
 * AND ARE GONE (W178).
 *
 * The first hid an empty SCRIM, and there is no scrim any more — an empty run
 * of white type on a black pad draws nothing on its own, which is exactly what
 * the legacy does when the field is cleared.
 *
 * The second dressed thirteen per-character boxes at a fixed 22px. The legacy
 * has no per-character element at all: it is one div whose `innerHTML` is the
 * characters joined by `<br>`, and its glyphs are sized by one `font-size`.
 * Thirteen fixed 22px boxes could not scale with the photograph in any case.
 */

.sf-flow-acc__sim-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sf-flow-acc__sim-label {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  font-weight: 600;
}

/* The board's `.input`, at the platform's control height. */
.sf-flow-acc__sim-input {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-emphasis);
  max-width: 260px;
  min-height: var(--control-height);
  padding: 0 14px;
  width: 100%;
}

.sf-flow-acc__sim-counter {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
}

.sf-flow-acc__sim-note {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  margin: 6px 0 0;
}

/* ---- the control row ---- */

/* `margin-top: auto` pins it to the bottom of the body column, so two cards of
 * different description lengths still line their actions up. */
.sf-flow-acc__controls {
  align-items: center;
  display: flex;
  gap: 12px;
  margin-top: auto;
  padding-top: 8px;
}

/* The board's quiet ghost, at the row's 40px. Not `.sf-flow-ghost`: that one is
 * the INK-outlined wayfinding button, and this is the quieter `line-mid` box
 * the board draws for a control that opens a modal rather than moving anyone.
 *
 * ITS TYPE IS THE RUNG'S NOW (Ryan's button audit, 2026-09-01): it drew 13.5/500
 * inside a 40px box whose two neighbours in the same control row — the stepper
 * and "Add to order" — draw 14.5/600, which is two of the five type sizes Ryan
 * counted. THE BORDER STAYS `line-mid`: quiet-vs-ink is a treatment the board
 * drew on purpose, and the ruling was about SIZE, not about flattening every
 * button to one colour. */
.sf-flow-acc__details {
  align-items: center;
  background: transparent;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  min-height: var(--control-height-sm, 40px);
  padding: 0 16px;
}

.sf-flow-acc__actions {
  align-items: center;
  display: flex;
  gap: 10px;
  margin-left: auto;
}

.sf-flow-acc__qty {
  align-items: center;
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: inline-flex;
  min-height: var(--control-height-sm);
}

/* One rule for both shapes the stepper's halves take — a button while the card
 * is composing a quantity, a link once it is editing the order (see the
 * component). They have to measure the same, or a card would change size when
 * it was added. */
.sf-flow-acc__step {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-small);
  padding: 0 12px;
  text-decoration: none;
}

.sf-flow-acc__step:disabled,
.sf-flow-acc__step--off {
  cursor: default;
  opacity: 0.45;
}

.sf-flow-acc__value {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-small);
  padding: 0 6px;
}

/* ---- W185: the film player ---- */

/*
 * ═══ THE FILM IS AN EMBED-EXPAND PLAYER (Ryan, 2026-09-04) ═══
 *
 * "I'm wondering if we should use the embed and expand functionality we made…
 * let's try it. Right align it in the description and bullet section. Allow it
 * to grow pushing description and bullets down."
 *
 * `.sf-flow-bullets__film`, `.sf-flow-acc__film` (the button) AND
 * `.sf-flow-acc__film-mark` STOOD HERE AND ARE GONE — W177's film BULLET, which
 * lived for one walkthrough. The class name `.sf-flow-acc__film` survives as the
 * SLOT class the shared frame wears, which is why the sweep still finds it.
 *
 * ─── THE TWO WIDTHS ARE PERCENTAGES, AND THAT IS THE WHOLE MECHANISM ────────
 *
 * The shared frame reserves its rectangle with `.sf-xp::before { width:
 * var(--sf-xp-w) }` and draws the box over it, so a caller's only job is to say
 * how wide the slot is at each state. The Rust Armor film says `296px` and
 * `calc(100% + 218px)` because it sits in a pop-up whose panel is a fixed 780;
 * this one sits in a card column that is 622px at 1440 and narrower at every
 * rung below, so PIXELS WOULD BE RIGHT AT ONE WIDTH AND WRONG EVERYWHERE ELSE.
 *
 * 40% and 100% of the section keep the ratio exact at every width, which is
 * also what makes `FILM_EXPAND_FACTOR` a constant the hook can trust — see the
 * card, where the number is derived from these two rather than typed.
 *
 * ─── WHY A FLOAT ───────────────────────────────────────────────────────────
 *
 * It is the one mechanism that does BOTH halves of the ruling: at 40% the
 * description and bullets wrap beside it, and at 100% the same float forces
 * every following line below it — "pushing description and bullets down" —
 * with no state to toggle and nothing to re-flow. A grid column would have
 * needed a second layout for the grown state and could not wrap text at all.
 *
 * IT STAYS FLOATED WHEN GROWN, deliberately: a 100%-wide float already clears
 * everything after it, so switching `float: none` at the same moment the width
 * animates would be two changes racing to describe one state.
 */
/*
 * ─── THE TWO WIDTHS LIVE ON A WRAPPER, AND THAT IS A REAL CSS CONSTRAINT ────
 *
 * MEASURED, by `accgate`, when they did not: a floated `.sf-xp` reported 1280px
 * wide inside a 622px section. A FLOAT IS SHRINK-TO-FIT — its width comes from
 * its contents — and the shared frame sizes itself from `.sf-xp::before { width:
 * var(--sf-xp-w) }`. A percentage there resolves against the float's own width,
 * which is the thing it is being used to compute: circular, and browsers resolve
 * it to nonsense rather than to an error.
 *
 * So the FLOAT carries a definite width and the slot inside it fills that. The
 * shared frame's `--sf-xp-w` is therefore 100% on this consumer — it is already
 * as wide as it is allowed to be — and the 40/100 pair moves one element out.
 * The parity roster asserts the pair WHEREVER it lives against the caller's
 * factor; the invariant is that the CSS's growth ratio and the hook's predicted
 * one agree, not which selector spells it.
 *
 * `:has()` IS WHAT LETS THE WRAPPER FOLLOW THE STATE. `data-big` is written on
 * the slot by the shared frame, so the wrapper reads its own child rather than
 * needing a second piece of state — the same move `.sf-flow-rail:has(...)`
 * already makes in this sheet.
 */
.sf-flow-acc__film-wrap {
  float: right;
  /* The section's own tight step to the left of it and under it, so wrapped
   * text keeps W106's rhythm rather than touching the frame. */
  margin: 0 0 8px 14px;
  width: 40%;
}

.sf-flow-acc__film-wrap:has(> .sf-xp[data-big="true"]) {
  margin: 0 0 12px;
  width: 100%;
}

/* THE SLOT FILLS ITS FLOAT. See the wrapper's note for why the growth is not
 * spelled here. */
.sf-flow-acc__film {
  --sf-xp-w: 100%;
}

/* ---- W124: the per-hoop offer ---- */

/*
 * ═══ THE OFFER IS THE RAIL'S NUDGE, ONE COMPONENT OVER (W124) ═══
 *
 * Ryan's ruling is behavioural — offer, never act — and the storefront already
 * has a shape that means exactly "this is addressed to you, and you may ignore
 * it": `.sf-flow-nudge`, the framed inset the rail draws when an install is
 * offered and not taken. Its values are transplanted here rather than
 * re-invented, for the reason the card standard was written for: a second
 * quiet-suggestion treatment would be a second thing to sweep.
 *
 * WHAT IS THE SAME: the `paper` ground on `surface`, the 2px accent left rule,
 * the radius flattened on that edge so the rule reads as a mark rather than as
 * a border, the caption type in `ink-soft`.
 *
 * WHAT DIFFERS, AND WHY: the rail's nudge indents 26px past the thumbnail
 * column it sits beside, and carries its dismiss as an absolute X in the
 * corner. This one spans the card's body column, which has no thumbnail to
 * clear, and its two answers are BOTH words — accepting is a real destination
 * ("Set to 3") and declining is a real refusal ("No thanks"), so neither is a
 * glyph a reader has to interpret. A question with two named answers is the
 * shape this needed and the corner X was not it.
 */
.sf-flow-acc__offer {
  align-items: baseline;
  background: var(--brand-color-paper, #fbfbfa);
  border-left: 2px solid var(--brand-color-accent, #c4530a);
  border-radius: 0 var(--radius-inner) var(--radius-inner) 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  margin: 0;
  padding: 10px 12px;
}

.sf-flow-acc__offer-body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-caption);
  line-height: 1.45;
  /* The question takes the row and pushes its two answers to the far end, so a
   * wrapped offer reads sentence-then-answers rather than interleaved. */
  margin-right: auto;
}

/*
 * ═══ THE QUESTION TAKES ITS OWN LINE, SO TWO BANDS ARE ONE SHAPE ═══
 *
 * Caught on the fix round's own screenshots rather than in the review: with the
 * body merely greedy (`margin-right: auto`), whether an answer fits beside the
 * sentence depends on how long the PRODUCT'S NAME is. So the goal light's band
 * drew its accept beside the question and its decline below, while the pole
 * pad's — two characters longer — pushed both answers onto their own row. Two
 * adjacent instances of one control in two different shapes, which is precisely
 * the inconsistency W171 was called for.
 *
 * A FULL-WIDTH BASIS MAKES THE SHAPE INDEPENDENT OF THE CONTENT: the question
 * always takes its own line(s), the two answers always sit together on one row
 * beneath it. That also keeps the pair adjacent, which is what the 44px targets
 * (F6) are for — a decline that lands on its own line half a band away from the
 * accept is the harder thing to hit, not the easier one.
 */
.sf-flow-nudge--offer .sf-flow-nudge__body {
  flex: 1 0 100%;
}

/*
 * ACCEPTING WEARS THE NUDGE'S OWN DOOR — `accent-strong`, 600, underlined.
 *
 * DELIBERATELY NOT A BUTTON'S BOX, and therefore deliberately not in
 * `btngate`'s family: this is the gate's own "QUIET LINKS" exclusion (underlined
 * text is not a button and must not grow a box). A 40px accent button inside a
 * suggestion band would out-shout "Add to order" two rows below it — which is
 * the control that actually closes the sale, and the one the eye must land on.
 */
.sf-flow-acc__offer-yes {
  color: var(--brand-color-accent-strong, #9c420a);
  font-size: var(--text-caption);
  font-weight: 600;
  text-decoration: underline;
  white-space: nowrap;
}

/* AND DECLINING IS QUIETER STILL. It is the answer the shopper already gave by
 * doing nothing, so it is drawn as the smallest thing that is unmistakably a
 * control. */
.sf-flow-acc__offer-no {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-caption);
  padding: 0;
  text-decoration: underline;
  white-space: nowrap;
}

.sf-flow-acc__offer-no:hover {
  color: var(--brand-color-ink, #17181a);
}

/* ---- W86: the in-line reviews the count opens ---- */

/*
 * ═══ THE ACCESSORY CARD'S REVIEW VIEW (Ryan's W86, 2026-09-03) ═══
 *
 * "Clicking the review total opens IN-LINE reviews, the installer-card pattern
 * (in-place content swap)." So these rules are `installers.css`'s
 * `.sf-inst-rev__*` block, transplanted value-for-value, which is itself
 * `pdp.css`'s `.sf-pdp-rev__item*` block transplanted value-for-value. W38's
 * ruling — "a review is a review" — is honoured in the MARKUP by
 * `review-card.tsx`, which all three surfaces share; what each sheet carries is
 * the ground its own surface stands on.
 *
 * WHY NOT ONE SHARED SHEET YET: installers.css records the same flag and the
 * same reason, and it applies unchanged here — converging three review blocks
 * into an `sf-rev-` block in sections.css would re-open `pdpgate`'s recorded
 * pixel ceilings to buy a tidiness nobody has asked for. FLAGGED, twice now,
 * which is the point at which the next pass should take it.
 *
 * WHAT THIS SURFACE DOES NOT TRANSPLANT, and why: W49/W61's measured box (that
 * arithmetic is against an installer pop-up's next profile photo, which does
 * not exist here), W50's phone carousel (a different answer to a complaint this
 * surface has not had), and W65b's fade (which marks an INTERNALLY scrolling
 * review; these reviews are as tall as they are).
 */

/*
 * THE PANEL BLEEDS TO THE CARD'S EDGES, then pays the padding back on each row
 * — the installer card's own W35 move, at this card's 18px. It is what lets the
 * control bar's ground and the rows' hairlines run the full width of the body
 * column instead of stopping short of it.
 *
 * NOTE IT BLEEDS ONLY THE INLINE AXIS: the card's flex row puts the media
 * column to the left of this, so a block-axis bleed would climb over the name.
 */
.sf-flow-rev {
  margin-block: 6px 0;
  margin-inline: calc(-1 * var(--sf-flow-acc-pad));
}

/*
 * THE CONTROL BAR — W46's shape and W46's reasoning, unchanged.
 *
 * `position: sticky` rather than a scroll listener: the browser owns the
 * arithmetic and it behaves correctly against whichever ancestor is actually
 * scrolling — here the review list below it. `top: 0` because its job is to
 * stay reachable within THAT scroller.
 *
 * THE GROUND IS `paper` ON `surface`, a four-per-channel delta: deliberately
 * quiet, because the bar has to read as chrome the reviews slide under rather
 * than as a second card. The hairline under it is what actually separates it
 * from the first review (playbook §4's tint trap, from the other side — this
 * one is a tint that is MEANT to be nearly invisible, and it does not carry the
 * separation on its own).
 */
.sf-flow-rev__bar {
  align-items: center;
  background: var(--brand-color-paper, #fbfbfa);
  border-bottom: 1px solid var(--brand-color-line, #e8e7e3);
  display: flex;
  gap: 14px;
  padding: 10px var(--sf-flow-acc-pad);
  position: sticky;
  top: 0;
  /* ABOVE THE ROWS THAT SLIDE UNDER IT. The reviews draw hairlines and
   * photographs; without a stacking order the bar's own ground would be painted
   * under a tile that happens to come later in the document (W71a's lesson: a
   * positioned element paints in a later step than static flow content, so the
   * bar has to be positioned AND ordered). */
  z-index: 2;
}

/* W59's shape: the compact rung, the arrow AND the word. A bare arrow at the
 * head of a review list reads as chrome rather than as the way out. */
.sf-flow-rev__back {
  align-items: center;
  appearance: none;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  font-family: inherit;
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  gap: 7px;
  justify-content: center;
  min-height: var(--control-height-sm, 40px);
  padding: 0 12px;
}

/* FLEX, so the inline SVG cannot pick up its line box's descender and push the
 * word off the button's optical centre (playbook §5, the glyph trap). */
.sf-flow-rev__back-mark {
  display: flex;
}

/* A STATEMENT, NOT A CONTROL — muted, because a fact about the list drawn in a
 * control's ink reads as a control. `auto` holds the far end of the bar. */
.sf-flow-rev__sort {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0 0 0 auto;
}

/*
 * ═══ THE LIST IS A SCROLLER WITH A DECLARED CEILING ═══
 *
 * W49's conclusion by the short route. The installer pop-up MEASURES its box,
 * because it has to leave the next installer's photograph peeking; this card is
 * in a page's own scroll with nothing below it whose position must be
 * preserved, so a CSS maximum does the same job with no layout effect, no
 * `ResizeObserver` and none of W67's stale-measurement class of bug.
 *
 * 520px because that is a little over three reviews at this measure — enough
 * that the list obviously continues, and short enough that the card does not
 * take a whole viewport when a shopper opens it out of curiosity. Infinite
 * scroll happens INSIDE it, so the card's own height stops changing after the
 * first batch lands.
 *
 * `position: relative` because the list hosts the rows' photo strips: every
 * `overflow != visible` container that can host an absolutely-positioned
 * descendant gets one, unconditionally, so the sr-span overflow trap (playbook
 * §5) cannot be reintroduced by a later addition.
 *
 * `overscroll-behavior: contain` so reaching the end of the reviews does not
 * scroll the page out from under the card the reader is reading.
 *
 * A REAL SCROLLBAR, thin and low-contrast rather than hidden — the same call
 * the pop-up shell's content row makes, and for the research-backed reason
 * recorded there: a fully hidden scrollbar is a mouse/trackpad hazard when
 * nothing else gives a position cue.
 */
.sf-flow-rev__list {
  max-height: 520px;
  overflow-y: auto;
  overscroll-behavior: contain;
  position: relative;
  scrollbar-color: var(--brand-color-line-mid, #d8d6d0) transparent;
  scrollbar-width: thin;
}

/* ─── one review, transplanted from installers.css's `.sf-inst-rev__item*` ── */

.sf-flow-rev__item {
  border-top: 1px solid var(--brand-color-line, #e8e7e3);
  display: flex;
  gap: 20px;
  padding: 22px var(--sf-flow-acc-pad);
}

/* THE FIRST REVIEW DRAWS NO RULE OF ITS OWN. The control bar above it already
 * closes with a hairline, and two lines a pixel apart is the seam a sticky bar
 * makes visible the moment it lifts off the first row. */
.sf-flow-rev__item:first-child {
  border-top: 0;
}

.sf-flow-rev__item-main {
  flex: 1;
  min-width: 0;
}

/* W51's column: stars on their own line, title under them. The measure here is
 * the accessory card's body column less the photo strip — narrower than the
 * product page's 565 — so a real title wrapping beside the stars would start
 * its second line under them, which is the ragged shape W51 named. */
.sf-flow-rev__item-head {
  align-items: flex-start;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* THE CARD'S OWN STAR GLYPH — the `★` text run this sheet already draws in
 * `.sf-flow-acc__stars` four lines above, not the installer widget's SVG. A
 * review row wearing a different star from the rating on the same card would be
 * the visible seam. */
.sf-flow-rev__stars {
  color: var(--brand-color-accent, #c4530a);
  flex: none;
  font-size: var(--text-small);
  letter-spacing: 1px;
  line-height: 1;
}

.sf-flow-rev__stars-rest {
  color: var(--brand-color-line-mid, #d8d6d0);
}

.sf-flow-rev__item-title {
  /* NO TRACKING. globals.css gives every h1/h2/h3 `letter-spacing: -0.015em`
   * and the review title is drawn with none — the same declaration, and the
   * same measured reason, as `.sf-pdp-rev__item-title`'s. */
  letter-spacing: normal;

  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-emphasis);
  font-weight: 700;
  line-height: normal;
  margin: 0;
  text-wrap: wrap;
}

.sf-flow-rev__item-meta {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin: 4px 0 0;
}

/* `pre-line` for the reason installers.css records: the ERP turns the rows that
 * carry `<br>` into NEWLINES rather than dropping them, which is only worth
 * doing if something draws them. It collapses runs of spaces the way normal
 * text does and keeps the line breaks — the pair of behaviours a pasted review
 * wants. */
.sf-flow-rev__item-body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  line-height: 1.6;
  margin: 10px 0 0;
  white-space: pre-line;
}

.sf-flow-rev__strong {
  color: var(--brand-color-ink, #17181a);
  font-weight: 700;
}

/* W47's measured answer: the product page draws review photographs as a 120px
 * RIGHT-HAND COLUMN at the desktop rung. This conforms rather than improves. */
.sf-flow-rev__item-photos {
  display: flex;
  flex: none;
  gap: 8px;
  overflow-x: auto;
  /* A clipping box with no positioned ancestor is how an abs-positioned
   * descendant escapes into the DOCUMENT's overflow (playbook §5). */
  position: relative;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  width: 120px;
}

.sf-flow-rev__item-photos::-webkit-scrollbar {
  display: none;
}

.sf-flow-rev__item-photo {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border: 0;
  border-radius: var(--radius-inner);
  cursor: pointer;
  display: flex;
  flex: none;
  height: 120px;
  justify-content: center;
  padding: 0;
  /* THE PHOTOGRAPH'S CONTAINING BLOCK. The `<img>` is absolutely positioned so
   * the well stays underneath it unconditionally, and a strip whose files have
   * not loaded draws the ground rather than white-on-white. */
  position: relative;
  scroll-snap-align: start;
  width: 120px;
}

.sf-flow-rev__item-photo-img {
  border-radius: inherit;
  display: block;
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

.sf-flow-rev__well-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  /* The placeholder-caption exemption (tokens §2). */
  font-size: 9px;
  /* THE LABEL IS THE FALLBACK, and it must not push the tile open when a long
   * one lands in the box. */
  overflow: hidden;
  padding: 0 4px;
  text-align: center;
}

/* THE STATUS LINES SIT WHERE THE FIRST REVIEW WOULD, so a panel that is
 * loading, empty or failed is the same shape as one that is full — nothing
 * jumps when the answer lands. */
.sf-flow-rev__status {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
  padding: 22px var(--sf-flow-acc-pad);
}

/*
 * THE SENTINEL, WHICH IS ALSO THE FALLBACK (W45's shape).
 *
 * A REAL BUTTON the observer watches, so the enhancement and the keyboard floor
 * can never drift apart. It has no border and no ground — the quiet treatment
 * `btngate` deliberately does not sweep — and takes the compact rung's height
 * so a row of chrome in a list of content matches everything around it.
 */
.sf-flow-rev__more {
  background: none;
  border: 0;
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  display: block;
  font-family: inherit;
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  min-height: var(--control-height-sm, 40px);
  padding: 0 var(--sf-flow-acc-pad);
  text-align: left;
  width: 100%;
}

.sf-flow-rev__more:disabled {
  color: var(--brand-color-muted, #6c6a62);
  cursor: default;
}

/* --------------------------------------------------- step 3: the checkout -- */

/*
 * STEP 3 — CHECKOUT, transplanted from design/produnk/FlowCheckout.dc.html
 * (drawn at 1440).
 *
 * The board's own numbers, verbatim: cards at `26px 28px` with 24px between
 * them, a 36px tinted section-header circle with 12px to its heading and 22px
 * under the header, 16px between field rows, 14px between the fields in a row,
 * a 13.5/600 label with 6px under it, and the board's `.input` at 48 tall with
 * 14px of side padding.
 *
 * THREE PLACES THE BOARD IS DELIBERATELY SUPERSEDED, each by something that
 * postdates it or outranks it. All three are declared in `flowgate.mjs` beside
 * the ceilings they explain, so none of them is a silent difference:
 *
 *   THE GRID — NO LONGER SUPERSEDED, AND THIS BOARD IS WHY. It draws
 *   `1fr 380px` with a 32px gap; the shell drew `minmax(0,1fr) 360px` with 28
 *   until Ryan ruled the rail wider on 2026-08-27, and the shell now draws THIS
 *   board's grid on all three steps (see `.sf-flow__grid`). The supersession
 *   moved to the other end: steps 1 and 2's boards are the ones the build no
 *   longer follows. Every `a3-*` content region in `flowgate.mjs` therefore
 *   dropped its +24 and the rail crop dropped its −20.
 *
 *   THE CONTROL TYPE SIZE. The board's `.input` is 15px; Ryan normalised this
 *   flow's controls to the platform standard on 2026-08-26 (the zip field came
 *   down from 18 to `--text-emphasis`, its button from 50 to 48) and the
 *   accessory simulator's field followed. Fourteen fields one rung below every
 *   other control on the flow would be the same defect the ruling was about,
 *   pointing the other way.
 *
 *   THE FIGURES. The board draws a $253.47 tax, a $214 freight overage and a
 *   $3,500.47 total. Nothing computes any of them (`quote-totals.ts` declines),
 *   so the SHAPE ships and the slots carry the pending mark. Not a style
 *   decision — see `flow-step3.tsx` and the rail's `Totals`.
 *
 * ONE PLACE THE CAPTION AND THE DRAWING DISAGREE, and the drawing wins
 * (playbook §3): the caption calls the terms block "its own BOUNDED
 * sub-section (well bg, generous padding)"; the artboard draws two plain
 * checkbox rows on the card's white, 28px apart. Shipped as DRAWN, flagged.
 */

.sf-flow-step3__title {
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-product);
  font-weight: 700;
  letter-spacing: normal;
  line-height: normal;
  margin: 0;
  text-wrap: initial;
}

/*
 * `.sf-flow-step3__sub` STOOD HERE AND IS GONE (W73, Ryan 2026-09-03). It set
 * the muted line under the title — "One page. Guest checkout — no account
 * needed." — one of four section sub-headers he struck in a single ruling.
 *
 * ITS SPACING RULE MOVED RATHER THAN GOING WITH IT. The board leads the first
 * card at 20px and the rest at 24; that 20 was expressed as
 * `.sf-flow-step3__sub + .sf-flow-ck`, so deleting the element would have
 * silently promoted the first card to 24 as a side effect of a copy change.
 * The adjacency now hangs off the title, which is the element the card
 * genuinely follows.
 */

/* ---- the cards ---- */

/* The board's `padding: 26px 28px`, and 20px under the head for the first card,
 * 24 between the rest. `:first-of-type` is deliberately NOT used — it counts
 * every element sibling of the same TAG, and the step's first `<section>` is
 * what is wanted (the same trap that silently removed the rail's install
 * hairline on 2026-08-26). */
.sf-flow-ck {
  margin-top: 24px;
  padding: 26px 28px;
}

.sf-flow-step3__title + .sf-flow-ck {
  margin-top: 20px;
}

/* The PDP's section-header pattern, at the board's checkout size: a 36px
 * `nav-text` circle carrying an 18px mark in `nav`, 12px to the heading. */
.sf-flow-ck__head {
  align-items: center;
  display: flex;
  gap: 12px;
  margin-bottom: 22px;
}

.sf-flow-ck__mark {
  align-items: center;
  background: var(--brand-color-nav-text, #d9e8ff);
  border-radius: 50%;
  color: var(--brand-color-nav, #192950);
  display: flex;
  flex: none;
  height: 36px;
  justify-content: center;
  width: 36px;
}

/*
 * THE HEADING IS A CONTAINER, AND ITS NAME IS A RUN INSIDE IT — which looks
 * like indirection and is the board's own structure, verbatim. The drawing puts
 * an 18/700 span and a 13.5/400 span side by side inside a plain div, so the
 * SPACE BETWEEN THEM is set at the div's own metrics (the browser default 16 at
 * weight 400), not at the heading's. Setting the h2 itself to 18/700 makes that
 * space ~1px wider and pushes the whole context run sideways — measured, at
 * threshold 24, on the `a3-contact` crop. So the element carries the board's
 * container metrics and the name carries the board's name metrics.
 *
 * globals.css styles every h2 with the heading face at weight 500, a -0.015em
 * track, 1.12 leading and `text-wrap: balance`; all four are restated here,
 * which is what every heading on this flow does.
 */
.sf-flow-ck__heading {
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-emphasis);
  font-weight: 400;
  letter-spacing: normal;
  line-height: normal;
  margin: 0;
  text-wrap: initial;
}

.sf-flow-ck__heading-name {
  font-size: var(--text-card-header);
  font-weight: 700;
}

/*
 * `.sf-flow-ck__context` STOOD HERE AND IS GONE (W73, Ryan 2026-09-03).
 *
 * It drew the 13.5 muted run on each card's heading line — "· who we call to
 * schedule delivery", "· tax confirms as you type", "· card, Apple Pay or
 * Affirm". All three are struck, along with the page's own subtitle above.
 *
 * `.sf-flow-ck__heading-name` STAYS, and it is not now redundant: the heading
 * element carries the BOARD's container metrics (16/400, the browser default)
 * and the name carries the board's name metrics (18/700), which is the split
 * the block above this one explains at length. Folding the name back into the
 * `<h2>` would move the type by the amount that comment was written about.
 */

/* The board's `flex-direction: column; gap: 16px` under every header.
 *
 * `position: relative` IS AN ANCHOR AND NOTHING ELSE (2026-09-01, address
 * autocomplete). The suggestion listbox is absolutely positioned against this
 * card so it can sit under the street field, and a positioned ancestor with no
 * offsets of its own moves nothing: the flex column lays out identically with
 * and without it, which is what keeps the recorded flow gates recorded. */
.sf-flow-ck__stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
  position: relative;
}

/*
 * THE AUTOFILL HOOK — a zero-effect animation whose only job is to fire an
 * event.
 *
 * There is NO event for "the browser just autofilled this field". The one
 * documented signal is that Chrome and Safari apply `:-webkit-autofill`, so a
 * keyframe attached to that pseudo-class produces an `animationstart` a script
 * can listen for. `flow-address-suggest.tsx` listens for this exact name and
 * suppresses its suggestion list for a beat — the WooCommerce #63423 failure
 * mode is a Google dropdown appearing over a form the browser has just
 * completed, where one stray click replaces good state with a differently
 * parsed value.
 *
 * It animates a property that is already its own value, so nothing paints,
 * nothing moves and no pixel gate sees it.
 */
@keyframes sf-autofill-start {
  from {
    opacity: 1;
  }
  to {
    opacity: 1;
  }
}

.sf-flow-ck__input:-webkit-autofill {
  animation-duration: 1ms;
  animation-name: sf-autofill-start;
}

/* ---- the street field's suggestion list ---- */

/*
 * DRAWN OVER THE CARD, ANCHORED TO THE STREET FIELD, and it exists only while
 * there are suggestions — see `flow-address-suggest.tsx` for why this is a
 * headless list over our own input rather than Google's own web component.
 *
 * `top`/`left`/`width` are written INLINE by the component from the field's own
 * box, because the field is one of three in a grid row whose tracks are data:
 * the sheet cannot know where it is, and hard-coding a track weight here would
 * put the row's arrangement in two places.
 */
.sf-flow-ck__suggest {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  box-shadow: 0 6px 18px rgb(0 0 0 / 12%);
  list-style: none;
  margin: 0;
  max-height: 264px;
  overflow-y: auto;
  padding: 4px 0;
  position: absolute;
  z-index: 20;
}

/* 44 tall: a touch target, not a menu row. The board draws no phone state for
 * this list, so the size is the platform's own minimum rather than a guess. */
.sf-flow-ck__suggest-row {
  align-items: baseline;
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-support);
  gap: 0 8px;
  line-height: 1.35;
  min-height: 44px;
  padding: 9px 14px;
}

.sf-flow-ck__suggest-row--active,
.sf-flow-ck__suggest-row:hover {
  background: var(--brand-color-well, #f4f3ef);
}

.sf-flow-ck__suggest-main {
  font-weight: 600;
}

.sf-flow-ck__suggest-rest {
  color: var(--brand-color-muted, #6c6a62);
}

/* THE PAYMENT CARD'S OWN RHYTHM. Its board draws no uniform gap — 6px above the
 * billing row, then 28 to the terms and 24 to each block after it — so the
 * column carries no gap of its own and each block declares the air above it. */
.sf-flow-ck__pay {
  display: flex;
  flex-direction: column;
}

/* ---- fields ---- */

/* The board's grids: `1fr 1fr`, `2fr 1fr`, `2fr 1fr 1fr`. The tracks come from
 * the DATA (a field's `span`), so the row's arrangement lives in one place —
 * see `CheckoutRow`. */
.sf-flow-ck__row {
  display: grid;
  /* THE TRACKS COME FROM THE ROW's OWN FIELDS, as a custom property the
   * component sets (see `CheckoutRow`). Read here rather than written inline as
   * `grid-template-columns`, because an inline declaration beats every rule in
   * this sheet and the phone rung below would be dead. */
  grid-template-columns: var(--sf-ck-cols, 1fr);
  gap: 14px;
}

.sf-flow-ck__field {
  display: flex;
  flex-direction: column;
  /* `min-width: 0` so a grid track may be narrower than its input's intrinsic
   * width — without it a 2fr/1fr/1fr row is floored by three controls and their
   * padding, and the row stops honouring its weights before the mobile cut. */
  min-width: 0;
}

/* The board's `.label`, at the 13.5 its own inline override sets. */
.sf-flow-ck__label {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  font-weight: 600;
  margin-bottom: 6px;
}

/* "(optional)" — the board sets it at weight 400 in `muted` inside a 600
 * label. */
.sf-flow-ck__optional {
  color: var(--brand-color-muted, #6c6a62);
  font-weight: 400;
}

/*
 * The board's `.input`. Its own rule declares `box-sizing: border-box`, so its
 * 48 is already a border box and ships unchanged — the one place this board is
 * not content-box, exactly as `FlowStep1.dc.html`'s was.
 *
 * The TYPE is the platform's 16 rather than the board's 15 — see this block's
 * header for the ruling.
 */
.sf-flow-ck__input {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-emphasis);
  min-height: var(--control-height);
  padding: 0 14px;
  width: 100%;
}

.sf-flow-ck__input::placeholder {
  color: var(--brand-color-muted, #6c6a62);
  opacity: 1;
}

/* THE STATE SELECT keeps the UA's own indicator and its own picker — see
 * `CheckoutStateField` for why the board's typed chevron is not reproduced. All
 * it needs from here is the same box every other field wears; a select's
 * vertical padding is a UA affair, so the height is declared and the side
 * padding steps back to leave room for the indicator. */
.sf-flow-ck__select {
  appearance: auto;
  height: var(--control-height);
  padding: 0 10px;
}

/*
 * `.sf-flow-ck__note` STOOD HERE AND IS GONE (W77, Ryan 2026-09-03). It drew
 * the muted line under a card's fields, and its only two instances were the
 * receipt-email explainer and the backup-number explainer, both struck.
 */

/* ---- W78: what a field says when it is wrong ---- */

/*
 * THE ERROR LINE, BESIDE THE FIELD IT IS ABOUT.
 *
 * NN/g's first rule for form errors is that the message is TEXT and not only a
 * colour, and its second is that the text sits next to the control — "users can
 * see the error message while fixing the error instead of having to remember
 * it". Both are structural here rather than stylistic: the paragraph is a child
 * of `.sf-flow-ck__field`, so it is in the same flex column as the label and
 * the input and cannot drift away from them at any width.
 *
 * `danger`, AT THE SUPPORTING SIZE, and the field's own 6px rhythm inverted —
 * the label leads the control by 6 and the message follows it by 6.
 *
 * IT IS RENDERED ONLY WHEN THERE IS ONE. No empty slot, no reserved height:
 * with nothing typed and nothing blurred this page's markup is what the
 * recorded flow gates measured, which is the property the whole of W78 was
 * built to keep.
 */
.sf-flow-ck__error {
  color: var(--brand-color-danger, #a3261a);
  font-size: var(--text-small);
  line-height: 1.4;
  margin: 6px 0 0;
}

/*
 * AND THE CONTROL SAYS SO TOO — the second signal, never the only one. The
 * board's own `line-mid` hairline becomes a `danger` one at the same width, so
 * the box does not move by a pixel when it goes wrong; a border that thickened
 * on error would reflow the row it is in.
 *
 * THE RING IS UNTOUCHED. Focus is a different state from invalid and the two
 * are drawn by different properties (`outline` vs `border-color`), so a field
 * that is both wears both — which is exactly what a reader who has come back to
 * fix it should see.
 */
.sf-flow-ck__input--invalid {
  border-color: var(--brand-color-danger, #a3261a);
}

/* ---- the advisory bands ---- */

/* The board's own stack: 8px between the bands. */
.sf-flow-ck__bands {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/*
 * The board's left-ruled band: white on the card's own white with a hairline
 * all round, a 3px coloured rule at the left, and the radius squared off on
 * that side so the rule reads as an edge rather than a stripe.
 */
.sf-flow-ck__band {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-left: 3px solid var(--brand-color-faint, #9a988f);
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-support);
  gap: 0 10px;
  line-height: 1.45;
  margin: 0;
  padding: 12px 16px;
}

/* THE CONFIRMED BAND — green rule, green tick. It is the branch the tax service
 * takes the day it answers; today the quiet one below renders instead, and both
 * are written so that day is a data change and not a build. */
.sf-flow-ck__band--good {
  border-left-color: var(--brand-color-good, #1f7a4d);
}

.sf-flow-ck__band--quiet {
  border-left-color: var(--brand-color-faint, #9a988f);
}

/*
 * THE PUSH-THROUGH BAND — accent rule, and a column rather than a wrapped line.
 *
 * ACCENT AND NOT DANGER, deliberately. Ryan's whole framing is that the
 * validator "is not always right", so a red band over a shopper's own correct
 * address would be the site telling them they are wrong about where they live.
 * Accent is the site's "look at this", which is what this is.
 *
 * It overrides the shared band's `align-items: center` because it stacks a
 * sentence, an optional suggested address and a row of actions — three blocks,
 * left-aligned, rather than one line of text with a figure in it.
 */
.sf-flow-ck__band--warn {
  align-items: flex-start;
  border-left-color: var(--brand-color-accent, #e2711d);
  flex-direction: column;
  gap: 8px;
}

.sf-flow-ck__band-body {
  color: var(--brand-color-ink-soft, #44464a);
}

/* The postal service's own spelling, set apart from the sentence offering it —
 * a shopper has to be able to READ the address before they accept it. */
.sf-flow-ck__band-suggest {
  color: var(--brand-color-ink, #17181a);
  font-weight: 600;
}

.sf-flow-ck__band-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
}

/*
 * PLAIN TEXT BUTTONS, and both of them equally plain.
 *
 * "Keep what I typed" is not the lesser option — Baymard measured 69% of
 * shoppers completing manually when suggestions did not match them — so drawing
 * the accept as a solid button and the keep as a hint would be steering people
 * away from the answer that is right for two thirds of them. Same size, same
 * weight, one carries the accent.
 */
.sf-flow-ck__band-action {
  background: none;
  border: 0;
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  font: inherit;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.sf-flow-ck__band-action--accept {
  color: var(--brand-color-accent, #e2711d);
  font-weight: 600;
}

.sf-flow-ck__band-tick {
  color: var(--brand-color-good, #1f7a4d);
  font-weight: 700;
}

.sf-flow-ck__band-money {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
}

/*
 * W145's SECOND DOOR, in the address card's tax band.
 *
 * THE SAME QUIET TREATMENT AS THE RAIL'S RUNG, deliberately: it is the same
 * offer and pressing either opens the same pop-up, so two different-looking
 * controls for one action would be two things to a reader rather than one.
 * Muted, underlined, at the band's own size — the storefront's quiet-link
 * idiom (tokens §1).
 */
.sf-flow-ck__band-exempt {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: inherit;
  margin-left: 8px;
  padding: 0;
  text-decoration: underline;
  white-space: nowrap;
}

.sf-flow-ck__band-exempt:hover {
  color: var(--brand-color-ink, #17181a);
}

.sf-flow-ck__band-trailer {
  color: var(--brand-color-muted, #6c6a62);
}

/* ---- the terms gate, and the veil ---- */

/* The board's checkbox rows: 15px ink, 12px to the words. */
.sf-flow-ck__check {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: flex;
  font-size: var(--text-body);
  gap: 12px;
  margin-top: 6px;
  padding: 2px 0;
}

.sf-flow-ck__check--terms {
  margin-top: 28px;
}

/*
 * THE REAL CHECKBOX, CLIPPED INSIDE THE DRAWN BOX.
 *
 * Clipped rather than `display: none`, because a hidden control is not a
 * control — it leaves the tab order, and this one is the page's only gate. It
 * is `position: absolute` inside a `position: relative` box for the reason the
 * playbook's sr-span bug records: an absolutely-positioned descendant whose
 * ancestors are all static takes the PAGE as its containing block and extends
 * the document's scrollable overflow.
 */
.sf-flow-ck__box {
  height: 100%;
  inset: 0;
  margin: 0;
  opacity: 0;
  position: absolute;
  width: 100%;
}

/* The drawn box: 20px, 1.5px accent border, radius 4 — the platform's selection
 * language at the board's own weight. */
.sf-flow-ck__box-mark {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-selected);
  border-radius: var(--radius-inner);
  color: var(--brand-color-surface, #ffffff);
  display: inline-flex;
  flex: none;
  height: 20px;
  justify-content: center;
  position: relative;
  width: 20px;
}

/* Checked: the box FILLS and its mark shows. Unchecked, the check is still in
 * the DOM at the same metrics — so nothing reflows when it is ticked. */
.sf-flow-ck__box-mark > svg {
  visibility: hidden;
}

.sf-flow-ck__box-mark:has(> .sf-flow-ck__box:checked) {
  background: var(--brand-color-accent, #c4530a);
}

.sf-flow-ck__box-mark:has(> .sf-flow-ck__box:checked) > svg {
  visibility: visible;
}

/* THE RING MOVES TO THE DRAWN BOX — tokens §3's one sanctioned case, in its own
 * words: "a bare input inside a drawn field, where the ring moves to the FIELD
 * (`:has(… :focus-visible)` on the wrapper) because that is the box the reader
 * is looking at". The clipped input's own outline would be a ring around a 20px
 * transparent square nobody can see. */
.sf-flow-ck__box-mark:has(> .sf-flow-ck__box:focus-visible) {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

.sf-flow-ck__box:focus-visible {
  outline: none;
}

/*
 * The board's legal links: PLAIN TEXT — same colour, same size, no underline
 * (its caption says so in as many words).
 *
 * THEY ARE `<button>`s NOW (W80, Ryan 2026-09-03): they open the terms pop-up
 * rather than navigating away from a checkout that keeps none of its state in
 * the URL. So the rule additionally has to UNDO a button — a UA draws one with
 * its own ground, border, padding and font — while changing nothing a reader
 * can see. It is deliberately not `all: unset`, which would also drop the
 * cursor and the focus behaviour this control needs.
 */
.sf-flow-ck__legal {
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  padding: 0;
  text-decoration: none;
}

.sf-flow-ck__legal:hover {
  text-decoration: underline;
}

/* THE MESSAGE'S SLOT is a live region and is always in the DOM, empty; the
 * board's own ruling is that there is NO permanent veil message, so what is
 * always present is the announcement point, not the announcement. It reserves
 * no height while it is empty. */
.sf-flow-ck__toast-slot {
  margin: 0;
}

.sf-flow-ck__toast-slot:not(:empty) {
  margin-top: 24px;
}

/* The board's toast: a left-ruled accent band with the drawn shadow. */
.sf-flow-ck__toast {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-left: 3px solid var(--brand-color-accent, #c4530a);
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
  box-shadow: 0 6px 18px rgb(23 24 26 / 12%);
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  font-size: var(--text-small);
  padding: 10px 16px;
}

/* The board's `position: relative` wrapper with the veil on `inset: 0`. */
.sf-flow-ck__methods-wrap {
  margin-top: 24px;
  position: relative;
}

.sf-flow-ck__methods {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/*
 * THE BOARD'S 0.45 — the greyed area, exactly as drawn. `inert` is what makes
 * it true rather than only visible (see the component); this is the half a
 * stylesheet can do.
 *
 * W74 (Ryan, 2026-09-03) DID NOT CHANGE THIS RULE, and that is worth saying
 * out loud: the ruling widened WHEN the block is deactivated — the quote's
 * freight and tax now gate it as well as the terms — and the selector is
 * `[inert]` rather than a class the component sets, so the state and the
 * drawing cannot come apart. One attribute means both "a keyboard cannot reach
 * this" and "this looks unreachable", which is precisely the pairing the
 * ruling is about.
 */
.sf-flow-ck__methods[inert] {
  opacity: 0.45;
}

.sf-flow-ck__veil {
  inset: 0;
  position: absolute;
}

/* ---- the wallets and the card fields ---- */

.sf-flow-ck__wallets {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/*
 * The board's three 48px tenders, `flex: 1` each.
 */
.sf-flow-ck__wallet {
  align-items: center;
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
  display: flex;
  flex: 1;
  font-family: inherit;
  font-size: var(--text-body);
  font-weight: 600;
  gap: 6px;
  justify-content: center;
  min-height: var(--control-height);
  /*
   * GOOGLE'S FLOOR, APPLIED TO ALL THREE — 152px, which is the minimum width
   * their dark button may be drawn at. Two reasons it is not scoped to the
   * Google one: Apple's guidance says its button must not be SMALLER than the
   * other payment buttons beside it, so the strictest floor is the row's floor;
   * and a floor on one button of a three-up flex row does not wrap the row, it
   * squeezes the other two — which is exactly what happened at 390 before this
   * was measured (Apple at ~78px and Affirm spilling out of the card).
   *
   * With all three floored the row WRAPS instead, one tender per line on a
   * phone, and nothing changes at 1440 where each track is ~281px.
   */
  min-width: 152px;
  padding: 0 12px;
}

.sf-flow-ck__wallet--apple {
  background: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
}

/*
 * `.sf-flow-ck__wallet--google` STOOD HERE AND IS GONE (Ryan, 2026-09-01:
 * "let's launch without Google pay but the others need to launch"). The row's
 * own 152px floor stays exactly where it is — the note above already explains
 * that it is the ROW's floor and why, and Apple's guidance ("must not be
 * smaller than the other payment buttons beside it") is what keeps it binding
 * with one fewer tender in the row.
 */

/*
 * THE APPLE PAY HOST — where the real `<apple-pay-button>` is mounted.
 *
 * A HOST RATHER THAN A BUTTON, and that is forced rather than chosen: Apple's
 * button is a web component that draws its own everything, and iOS's
 * anti-clickjacking rules keep it above any overlay and route taps to it
 * regardless of z-index. So it cannot be styled and cannot be covered — it can
 * only be PRESENT or ABSENT, and this box is the slot it occupies in the flex
 * row when it is present. The component is told its own width and height
 * through Apple's own custom properties (see `flow-checkout-wallets.tsx`).
 */
.sf-flow-ck__wallet-host {
  display: block;
  flex: 1;
  min-height: var(--control-height);
  min-width: 152px;
}

.sf-flow-ck__wallet--affirm {
  background: var(--brand-color-surface, #ffffff);
  border: 1px solid var(--brand-color-ink, #17181a);
  color: var(--brand-color-ink, #17181a);
  font-weight: 700;
}

.sf-flow-ck__wallet-note {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  font-weight: 400;
}

.sf-flow-ck__wallet-help {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
}

/*
 * WHAT THE EXPRESS TENDERS HAVE TO SAY, said where they are.
 *
 * The place-order bar has a slot of exactly this shape three hundred pixels
 * further down; a shopper who has just tapped Apple Pay and had their card
 * declined has to be told beside the button they pressed. It is always in the
 * DOM (a `role="status"` that appears with its own text announces nothing) and
 * collapses to nothing when empty, so an unpressed row keeps the board's
 * spacing.
 */
.sf-flow-ck__wallet-status {
  margin: 0;
}

.sf-flow-ck__wallet-status:empty {
  display: none;
}

/* The board's "or pay with card" rule: two hairlines and a label between them. */
.sf-flow-ck__divider {
  align-items: center;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  font-size: var(--text-small);
  gap: 12px;
  margin: 0;
}

.sf-flow-ck__divider::before,
.sf-flow-ck__divider::after {
  background: var(--brand-color-line, #e8e7e3);
  content: "";
  flex: 1;
  height: 1px;
}

.sf-flow-ck__divider-label {
  flex: none;
}

/* ---- the billing form ---- */

/* The rail's derived-child treatment: a 2px `line-mid` rule, inset so it lines
 * up under the checkbox it is a consequence of (the board's own 9px margin puts
 * it on the box's centre line). */
/*
 * `position: relative` IS AN ANCHOR AND NOTHING ELSE (W95, 2026-09-03) — the
 * same one line, for the same reason, that `.sf-flow-ck__stack` carries: the
 * billing study now mounts its own suggestion listbox, which is absolutely
 * positioned against this form so it can sit under the billing street field.
 *
 * A positioned ancestor with no offsets of its own moves nothing: the flex
 * column lays out identically with and without it, which is what keeps
 * `a3-billing` recorded. It is also the playbook's own standing rule — every
 * `overflow`/abs-positioning host is `position: relative`, or the out-of-flow
 * box measures against the page and grows the DOCUMENT's scrollable width.
 */
.sf-flow-ck__billing {
  border-left: var(--border-inset);
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin: 16px 0 0 9px;
  padding-left: 18px;
  position: relative;
}

.sf-flow-ck__billing-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin: 16px 0 0;
}

/* ---- place order ---- */

.sf-flow-ck__place {
  margin-top: 24px;
}

/*
 * THE BOARD DRAWS THIS BAR DISABLED and so does the page — because it is. The
 * grey is `line-mid` on `muted`, which is the drawing; what a stylesheet cannot
 * say is WHY, so the sentence under it does (`.sf-flow-ck__unavailable`).
 *
 * FULL WIDTH AT THE BOARD'S OWN 54 (16px of padding either side of a 16/normal
 * line, declared rather than inherited so the bar cannot change size with a
 * leading), and it keeps that box when it can act — only the ground and the ink
 * change, which is the house rule for a control that cannot act yet.
 */
.sf-flow-ck__submit {
  align-items: center;
  background: var(--brand-color-accent, #c4530a);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: flex;
  font-family: inherit;
  /* THE PRIMARY RUNG (Ryan's button audit, 2026-09-01). It was 54 — a height
   * nothing else on the site draws, six px above the platform primary and the
   * tallest button in the flow. Place Order is the most important button on the
   * storefront, and it is now the same box as every other primary; being the
   * most important is what the ACCENT and the full width already say. */
  font-size: var(--button-text, 16px);
  font-weight: var(--button-weight, 700);
  justify-content: center;
  min-height: var(--control-height, 48px);
  padding: 0;
  text-align: center;
  width: 100%;
}

.sf-flow-ck__submit[aria-disabled="true"] {
  background: var(--brand-color-line-mid, #d8d6d0);
  color: var(--brand-color-muted, #6c6a62);
  cursor: default;
}

.sf-flow-ck__unavailable {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 10px 0 0;
  text-align: center;
}

/*
 * WHAT THE BAR HAS TO SAY AFTER IT WAS PRESSED (checkout P2).
 *
 * A RESERVED SLOT, not a conditional row — the same shape the payment card's
 * toast uses, and for the same two reasons: a `role="status"` that appears only
 * when it has content is a live region a screen reader may not have been
 * watching, and a row that materialises pushes the trust line down under the
 * reader's cursor at the exact moment they are reading a decline.
 *
 * The slot is EMPTY at rest and collapses to nothing (`min-height: 0`), so a
 * page with no message is byte-identical in layout to the page before P2.
 */
/*
 * THE SLOT ITSELF CONTRIBUTES NOTHING WHEN IT IS EMPTY — zero height, zero
 * margins — and that is a MEASURED requirement rather than tidiness. The
 * recorded pixel ceilings for the place-order block (`flowgate.mjs`'s
 * `a3-place`) were taken before this element existed; a 10px top margin here
 * would collapse through an empty paragraph and push the trust row down, which
 * is a real layout change on a page that is supposed to render exactly as it
 * did while the seam was a stub. So the spacing lives on the TEXT, which only
 * exists when there is something to say.
 */
.sf-flow-ck__result {
  margin: 0;
  min-height: 0;
  text-align: center;
}

.sf-flow-ck__result-text {
  display: block;
  font-size: var(--text-small);
  line-height: 1.5;
  margin: 10px auto 0;
  max-width: 46ch;
}

/*
 * THREE TONES FOR FOUR OUTCOMES, and the pairing is deliberate.
 *
 * A DECLINE IS NOT AN ERROR OF OURS and a REPRICE IS NOT AN ERROR AT ALL, so
 * neither wears the alarm colour: a declined card is a thing to fix (accent,
 * the same ink the sale row uses to mean "look here"), and a reprice is
 * ordinary information (muted). What DOES go red is the pair a shopper must not
 * skim past — `unavailable`, where an order did not happen, and `unknown`,
 * where we cannot say whether a card was charged.
 */
.sf-flow-ck__result-text--declined,
.sf-flow-ck__result-text--repriced {
  color: var(--brand-color-ink-soft, #3c3a35);
}

.sf-flow-ck__result-text--unavailable,
.sf-flow-ck__result-text--unknown {
  color: var(--brand-color-danger, #a3261a);
  font-weight: 600;
}

/*
 * The board's trust row: centred, 22px apart, wrapping.
 *
 * W82 (Ryan, 2026-09-03): "More air above the trust-indicator row (SSL /
 * warranty / phone)." 28 -> 40. It is the LAST thing on the page and the one
 * thing on it that is not part of the transaction — three reassurances under
 * the button that spends the money — and at 28 it read as a fourth line of the
 * place-order block rather than as a footer to it.
 *
 * 40 IS THE PAGE'S OWN NEXT RUNG UP, not a number picked to look right: it is
 * what separates the message slot from the block above it at every other seam
 * on this step, so the row now sits at a distance the page already uses.
 */
.sf-flow-ck__trust {
  align-items: center;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-small);
  gap: 12px 22px;
  justify-content: center;
  margin: 40px 0 0;
}

.sf-flow-ck__trust-item {
  align-items: center;
  display: inline-flex;
  gap: 7px;
}

.sf-flow-ck__trust-rule {
  color: var(--brand-color-line-mid, #d8d6d0);
}

/* The number is a `tel:` link and the board sets it 700 in ink — a phone number
 * on a phone is a control, which the drawing could not know. */
.sf-flow-ck__phone {
  color: var(--brand-color-ink, #17181a);
  font-weight: 700;
  text-decoration: none;
}

.sf-flow-ck__phone:hover {
  text-decoration: underline;
}

/* ---------------------------------------------------------------- the rail -- */

.sf-flow-rail {
  align-self: start;
  position: sticky;
  /* The board sticks the rail 20px from the top of an artboard that draws no
   * real chrome. On the page the pinned nav row is what it clears, so the offset
   * is the chrome's own rung plus the board's 20. */
  top: calc(var(--sf-pinned-offset, 54px) + 20px);
}

/*
 * THE FRAME IS `.sf-card`'s NOW (W89). It declared `surface`, `--border-hairline`
 * and `--radius-control`, which is the shared card look to the computed pixel —
 * the rail always WAS a card, it just said so itself. What is left here is its
 * padding. See card.css's header for why the two classes may never declare the
 * same property.
 *
 * AND THAT IS ALSO W90's SHADOW, arriving for free: Ryan asked for the lifted
 * rail to carry a drop shadow, and the shared definition already carries the
 * only drop shadow this house draws on a card.
 */
.sf-flow-rail__body {
  padding: 20px 22px;
}

/*
 * ═══ W90a — THE RAIL RIDES UP OVER THE PROGRESSION BAND (Ryan, 2026-09-03) ═══
 *
 * "Desktop flourish: 'Your Order' pushes up to OVERLAY the progression band,
 * with drop shadow."
 *
 * ─── THE NUMBER, AND WHY IT IS THIS ONE ─────────────────────────────────────
 *
 * Two measured facts settle it. The stepper band is 64px tall (`padding-block:
 * 18px` around a 28px dot row, declared at `.sf-flow-steps`), and the grid
 * below it carries `margin-top: 28px`. So a lift of 28 lands the rail flush
 * with the band's bottom edge — touching, not overlaying — and a lift of 92
 * lands it flush with the band's TOP, which is a rail hanging off the title
 * band rather than a rail overlapping the stepper.
 *
 * 60px is 28 + half the band: the rail's top edge lands on the band's own
 * horizontal centre line, so the overlap is unmistakably deliberate and the
 * step dots beside it are still whole. Between the two extremes, at the only
 * position either edge of the band can be read from.
 *
 * ─── IT NEEDS NO `z-index`, AND THAT IS WORTH SAYING OUT LOUD ───────────────
 *
 * W71a's lesson from the other direction: a POSITIONED element — even at the
 * default `z-index: auto` — paints in a later step than static flow content,
 * including that content's background. `.sf-flow-steps` is a static band with a
 * `well` ground; this rail is `position: sticky`, which is positioned. So the
 * rail paints over the band by the stacking rules alone, and a z-index here
 * would be a number nothing needed that a later reader would have to disprove.
 *
 * ─── DESKTOP ONLY ───────────────────────────────────────────────────────────
 *
 * Below 919.98 the rail is not a column at all — it is the full-bleed collapsed
 * band that sits FLUSH under the stepper by Ryan's 2026-08-27 ruling, and there
 * is nothing to overlay. This rule lives at the rung where the rail is a rail.
 *
 * ─── THE SHADOW IS THE HOUSE'S CARD LIFT, AND IT IS NOT DECLARED HERE ───────
 *
 * Ryan asked for the lifted rail to carry a drop shadow. It does, and it does
 * so through W89 rather than through a rule of its own: `.sf-flow-rail__body`
 * wears `sf-card` now, so the rail draws exactly the shadow the cards beside it
 * draw. A `box-shadow` in this block would have been a second definition of the
 * one thing W89 exists to have one of.
 *
 * FLAGGED FOR RYAN: this is the SHARED rail, so the lift and the overlap appear
 * on all three steps, not only on the accessories one he walked. That is the
 * consistency reading — one rail, one look — and it is a one-line change to
 * scope it to `.sf-flow[data-step="2"] .sf-flow-rail` if he wants it kept to
 * this page.
 */
@media (min-width: 920px) {
  .sf-flow-rail {
    margin-top: -60px;
  }
}

.sf-flow-rail__head {
  align-items: center;
  display: flex;
  justify-content: space-between;
  margin-bottom: 14px;
}

.sf-flow-rail__heading {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  font-size: var(--text-card-header);
  font-weight: 700;
}

/* NO COUNT BADGE on the rail's cart (tokens §4) — the nav's cart carries the
 * count; in here the order is the whole column. */
.sf-flow-rail__cart {
  align-items: center;
  display: inline-flex;
  margin-right: 10px;
}

/*
 * ═══ W90b — EDIT / DONE IS A GHOST BUTTON (Ryan, 2026-09-03) ═══
 *
 * "Edit/Done links become GHOST buttons."
 *
 * WHAT IT WAS AND WHY IT CHANGED: the board drew Edit as one of its quiet
 * links, and this rule dressed a `<button>` as one — muted, underlined, no box.
 * It is the control that puts the whole order into an editable state, sitting
 * at the top of the panel a shopper does that from, and it read as a footnote.
 * The rail's other real control (`.sf-flow-rail__disclose`) has been a ghost on
 * the compact rung since BTN-UNIFY; this brings the pair into one family.
 *
 * THE COMPACT RUNG, WHOLE — 40px, 14.5/600, `--radius-control`, transparent
 * ground with a 1px ink border. That is `.sf-flow-ghost`'s own treatment,
 * restated rather than composed from it in the markup for one reason: this
 * button lives in a `space-between` header row opposite a card heading, and
 * `.sf-flow-ghost` carries `--button-pad-x-sm: 18px` side padding sized for a
 * standalone action. 14 keeps a two-letter word from drawing a 60px box beside
 * "Your order".
 *
 * IT JOINS `btngate`'s FAMILY with this change — a bordered box with a word in
 * it is exactly what that gate sweeps, and a ghost is one of its three
 * sanctioned treatments.
 *
 * THE WEIGHT NO LONGER MOVES ON `[data-editing]`. It used to step 500 -> 600 to
 * mark the editing state; at the rung both states are 600, and a control that
 * changed width when it changed word ("Edit" -> "Done") would shift the heading
 * beside it. The word IS the state.
 */
/*
 * TWO SELECTORS, ONE DECLARATION (W198 item 6). The cart drawer's Edit/Done
 * moved into the `ModalShell` header row, and it must be the SAME control the
 * rail draws — same 40px rung, same border, same type — because it is the same
 * act on the same order.
 *
 * IT MAY NOT SHARE THE CLASS NAME, THOUGH, and that is not fussiness: six gate
 * tools reach for `.sf-flow-rail__edit` with `document.querySelector`, and the
 * drawer is mounted by the CHROME — earlier in the document than any flow page
 * — so a second element wearing the name became the first match and every one
 * of those gates pressed the wrong button. `railgate` failed six assertions the
 * first time this was tried, which is the whole reason that gate exists.
 *
 * A SHARED RULE IS WHAT SHARING SHOULD HAVE MEANT ANYWAY. The declaration lives
 * once, here, beside the rail it was written for; the drawer's own name is
 * added to the selector rather than to the element.
 */
.sf-flow-rail__edit,
.sf-cart-drawer__edit-toggle {
  align-items: center;
  background: transparent;
  border: 1px solid var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  font-family: inherit;
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  justify-content: center;
  min-height: var(--control-height-sm, 40px);
  padding: 0 14px;
}
/*
 * `.sf-flow-rail__head > .sf-flow-rail__edit:only-child` IS RETIRED BY NAME
 * (W198 item 6).
 *
 * It pushed a LONE Edit control to the right of the head row, because the cart
 * drawer drew the head with no heading in it (the shell's band already named
 * the panel) and a single child of a `space-between` row lands on the left.
 * Ryan struck that row entirely — "the edit button must not take a whole row" —
 * so the drawer draws no head at all and the Edit lives in the shell's header
 * beside the cross. The rail's head always has two children, so the selector
 * now matches nothing anywhere.
 */


/* ---- lines ---- */

.sf-flow-line {
  align-items: flex-start;
  display: flex;
  gap: 8px;
  padding: 12px 0 6px;
}

/* THE QTY COLUMN IS ALWAYS PRESENT (ruled — the board's qty model), on every
 * row of the rail, at the same 18px measure so the names line up. */
.sf-flow-line__qty {
  color: var(--brand-color-muted, #6c6a62);
  flex: none;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  text-align: right;
  width: 18px;
}

.sf-flow-line__main {
  flex: 1;
}

.sf-flow-line__row {
  display: flex;
  gap: 10px;
  justify-content: space-between;
}

.sf-flow-line__name {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 600;
}

.sf-flow-line__caption {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-small);
  margin-top: 3px;
}

/*
 * `.sf-flow-line__caption--aside` STOOD HERE AND IS RETIRED BY NAME (W196).
 *
 * W195 added it to pull an INACTIVE hoop's option caption over by 78px, because
 * that hoop drew its caption as a SIBLING of its row while the focused hoop drew
 * one inside `.sf-flow-line__main` — same words, same job, two indents, and Ryan
 * saw it.
 *
 * THE EXTRACTION DELETED THE CAUSE. There is no inactive-hoop row any more:
 * `order-view.tsx` draws every hoop the same way, caption inside the row's main
 * column, so the alignment is structural and the patch that bought it by hand
 * has nothing left to correct. This is the shape of the whole W196 lesson in one
 * rule — two implementations needed a rule to agree; one implementation does not.
 */

.sf-flow-line__amount {
  color: var(--brand-color-ink, #17181a);
  flex: none;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-support);
}

.sf-flow-line__amount--sale {
  color: var(--brand-color-accent, #c4530a);
}

.sf-flow-line__amount--free {
  color: var(--brand-color-good, #1f7a4d);
  font-weight: 600;
}

/*
 * ═══ W200 ITEM 1 (Ryan, 2026-09-06): TWO HOOPS SEPARATE FROM EACH OTHER ═══
 *
 *   "Missing horizontal hairline BETWEEN the two system rows (the accessory has
 *    its line; systems don't separate from each other)."
 *
 * ─── THE ROOT CAUSE IS THAT THE HAIRLINE WAS KEYED OFF THE ROW'S KIND ───────
 *
 * There has never been a rule that separates one LINE from the next. What this
 * panel had was `.sf-flow-line--service`'s own `border-top` — a rule about the
 * per-order block, written when the only thing above it was a single hoop, so
 * "the rule under the hoops" and "the rule above the services" were the same
 * pixel and nobody had to decide which one it was. W194 made an order a flat
 * collection of lines and a second hoop became possible; the moment it did, the
 * one rule on the panel was drawn by the ACCESSORY, and two hoops ran together.
 * Measured at 390 on Ryan's own cart (Diamond, Gold, goal light):
 *
 *     .sf-flow-line[data-system="sys-0"]   border-top: 0px
 *     .sf-flow-line[data-system="sys-1"]   border-top: 0px
 *     .sf-flow-line--service (Goal Light)  border-top: 1px
 *
 * SO THE RULE IS STATED ABOUT THE BOUNDARY, not about the kind: a hoop's row
 * that FOLLOWS another line carries the hairline, and the first one on the panel
 * does not (the head row above it, or the panel's own edge, already separates
 * it). That is `.sf-flow-line--service`'s own `+` discipline generalised, and it
 * needs no `:first-child` exception to walk back — which matters here because
 * the two surfaces put a different number of elements above the first hoop.
 *
 * TWO SELECTORS BECAUSE A HOOP IS TWO ELEMENTS. The block is the row PLUS its
 * framed inset when the expander is open, so the sibling directly above hoop
 * two is `.sf-flow-line` when hoop one is collapsed and `.sf-flow-inset` when it
 * is not. One of them alone would have drawn the hairline in only one of the two
 * states — the class of bug that reads as "it flickers when I expand something".
 *
 * IT LANDS ON BOTH SURFACES, WHICH IS THE POINT OF W196. The rail draws hoops
 * with the same component and had the same gap; nothing about the rail groups
 * its hoops deliberately (its own grouping — the nudge, the offers — comes
 * AFTER every hoop, in `afterSystems`), so there is nothing here to scope away
 * from it. The 8px lead is `.sf-flow-line--service`'s own, so the two boundaries
 * on the panel are one distance rather than two.
 */
.sf-flow-line + .sf-flow-line[data-system],
.sf-flow-inset + .sf-flow-line[data-system] {
  border-top: var(--border-hairline);
  margin-top: 8px;
}

/*
 * THE PER-ORDER LINES — the install, and (after Ryan's 2026-08-26 add-on
 * ruling) a switched-on sub-option beside it. One treatment for both, because
 * they are one kind of thing: a line whose quantity is 1 whatever the system
 * above it says. The board drew this row for the install; the add-on wears the
 * board's own drawing rather than a second idea about it.
 *
 * THE HAIRLINE IS ON THE GROUP, not on every row: two per-order lines read as
 * one block under one rule rather than as two ruled bands. Done by SUPPRESSING
 * it on a service line that follows another one — NOT by `:first-of-type`,
 * which counts every `div` sibling in the rail (the system's own line, the
 * framed inset) and therefore matches none of these.
 */
.sf-flow-line--service {
  align-items: center;
  border-top: var(--border-hairline);
  font-size: var(--text-support);
  margin-top: 8px;
}

.sf-flow-line--service + .sf-flow-line--service {
  border-top: 0;
  margin-top: 0;
}

/* WHAT THE SHOPPER ANSWERED, under a per-order line's name — a personalised
 * accessory's lettering, today. The system line's own caption metrics, one
 * step quieter, and a block so it takes its own line inside the name run. */
.sf-flow-line__caption--service {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  margin-top: 2px;
}

.sf-flow-line__name--service {
  flex: 1;
}

/* ---- the framed inset: the derived children ---- */

/*
 * The board's inset, indented 26px so it clears the qty column and starts at
 * the THUMBNAIL's left edge, with the 2px left rule and the quiet ground the
 * pattern names (tokens §4).
 */
.sf-flow-inset {
  background: var(--brand-color-paper, #fbfbfa);
  border-left: var(--border-inset);
  border-radius: 0 var(--radius-inner) var(--radius-inner) 0;
  margin: 0 0 4px 26px;
  padding: 5px 10px;
}

.sf-flow-inset__row {
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-small);
  gap: 10px;
  justify-content: space-between;
  padding: 6px 0;
}

.sf-flow-inset__left {
  display: inline-flex;
  gap: 8px;
}

/* The sale row's chip: accent-soft ground, accent-strong ink (tokens §1). */
.sf-flow-badge {
  align-items: center;
  background: var(--brand-color-accent-soft, #fbefe5);
  border-radius: var(--radius-inner);
  color: var(--brand-color-accent-strong, #9c420a);
  display: inline-flex;
  font-size: var(--text-caption);
  font-weight: 600;
  gap: 5px;
  padding: 2px 8px;
}

/* Inside the inset the amounts INHERIT the row's 13.5 — the board sets a size
 * only on the parent line's figure, and the children's are one step down. */
.sf-flow-inset__row .sf-flow-line__amount {
  font-size: var(--text-small);
}

.sf-flow-badge__mark {
  align-items: center;
  display: inline-flex;
}

/* ---- the nudge (words-spec; no board draws it) ---- */

/*
 * THE NUDGE, built out of the rail's own vocabulary: the framed-inset shape the
 * derived children wear — quiet ground, 2px left rule, indented past the
 * thumbnail — with the rule in ACCENT instead of `line-mid`. That single
 * substitution is the whole visual difference, which is what makes it read as
 * addressed to you without shouting.
 *
 * NOT the accent-soft band: on this page accent-soft already means SALE (the
 * discount chip two rows below it), and a prompt wearing the sale's ground would
 * be read as a promotion. DERIVED, flagged, and screenshotted for Ryan.
 */
.sf-flow-nudge {
  background: var(--brand-color-paper, #fbfbfa);
  border-left: 2px solid var(--brand-color-accent, #c4530a);
  border-radius: 0 var(--radius-inner) var(--radius-inner) 0;
  margin: 0 0 8px 26px;
  padding: 8px 26px 8px 10px;
  position: relative;
}

.sf-flow-nudge__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-caption);
  line-height: 1.45;
}

/* The door, INSIDE the sentence — see the component. */
.sf-flow-nudge__action {
  background: none;
  border: 0;
  color: var(--brand-color-accent-strong, #9c420a);
  cursor: pointer;
  font-family: inherit;
  font-size: inherit;
  font-weight: 600;
  padding: 0;
  text-decoration: underline;
  white-space: nowrap;
}

/* The only thing in the corner. Absolute inside a `position: relative` band —
 * which the band declares for exactly this reason. */
.sf-flow-nudge__dismiss {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-support);
  line-height: 1;
  padding: 2px 4px;
  position: absolute;
  right: 4px;
  top: 5px;
}

.sf-flow-nudge__dismiss:hover {
  color: var(--brand-color-ink, #17181a);
}

/* ---- W152: the per-hoop offer, asked from the rail ---- */

/*
 * ═══ THE OFFER IS THE NUDGE, AND HERE IT LITERALLY IS ═══
 *
 * Ryan's W152: "Changing SYSTEM quantity did not re-offer the per-hoop match —
 * the offer must also surface at hoop-quantity change." The system quantity has
 * one control in this flow, the stepper in the rail's edit state, so the
 * question is now asked here as well as on the accessory card.
 *
 * NO NEW TREATMENT WAS INVENTED FOR IT, and there is a short history behind
 * that. The card's `.sf-flow-acc__offer` block (§W124, above) was transplanted
 * value-for-value FROM `.sf-flow-nudge` — the framed inset this rail draws when
 * the options have not been set — because the nudge is already the storefront's
 * way of saying "this is addressed to you, and you may ignore it". So bringing
 * the offer INTO the rail is bringing it home: it wears the nudge's own classes,
 * and this modifier is the only difference.
 *
 * WHAT THE MODIFIER DOES, AND WHY IT IS ALL IT DOES. The nudge reserves 26px of
 * right padding for an absolutely-positioned × in its corner. This band has no
 * corner control — both of its answers are WORDS, because accepting is a real
 * destination ("Set to 3") and declining is a real refusal ("No thanks"), and
 * neither is a glyph a reader has to interpret. So the padding comes back and
 * the row becomes a wrapping flex line, exactly as the card's does: the question
 * takes the measure and pushes its two answers to the far end, so a wrapped
 * offer reads sentence-then-answers rather than interleaved.
 */
.sf-flow-nudge--offer {
  /*
   * CENTRED, NOT BASELINE (GUI review F6's second half). Baseline is right for a
   * sentence beside two words and wrong the moment those words are 44px touch
   * targets — it would hang the boxes off the sentence's own baseline and put
   * their ink low in them. Declared HERE rather than in a second rule for the
   * same selector: two same-specificity rules in one sheet declaring one
   * property is a cascade race, which is the trap the modal shell's dual-class
   * note warns about.
   */
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  padding-right: 10px;
}

/* The question takes the row; the two answers sit together at its end. */
.sf-flow-nudge--offer .sf-flow-nudge__body {
  margin-right: auto;
}

/*
 * DECLINING IS QUIETER THAN ACCEPTING, and quieter than the × it replaces.
 *
 * It is the answer the shopper already gave by doing nothing, so it is drawn as
 * the smallest thing that is unmistakably a control — the card's
 * `.sf-flow-acc__offer-no`, value for value. Accepting reuses
 * `.sf-flow-nudge__action` unchanged (accent-strong, 600, underlined), which is
 * the same weight the card's accept carries and is DELIBERATELY NOT A BUTTON'S
 * BOX: a 40px accent button inside a suggestion band would out-shout the
 * Checkout below it, and `btngate`'s "QUIET LINKS" exclusion is written for
 * exactly this.
 */
.sf-flow-nudge__decline {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-caption);
  padding: 0;
  text-decoration: underline;
  white-space: nowrap;
}

.sf-flow-nudge__decline:hover {
  color: var(--brand-color-ink, #17181a);
}

/*
 * ═══ THE OFFER'S TWO ANSWERS ARE REAL TARGETS (GUI review F6) ═══
 *
 * "No thanks" measured FIFTEEN PIXELS tall — the caption's line box and nothing
 * else — which is unusable on a phone and actively hazardous where three bands
 * stack, one per short accessory: three 15px refusals a few pixels apart, each
 * belonging to a different product, with the accept link beside them.
 *
 * BOTH ANSWERS, NOT JUST THE ONE NAMED. The review called out the decline
 * because it is the smaller and the more dangerous to mis-hit, but the accept
 * has the same line box and sits in the same band — fixing one and leaving the
 * other would leave the pair mismatched and the band still hazardous. They are
 * two answers to one question and they get one treatment.
 *
 * 44px IS THE TOUCH FLOOR and it is the same number F9 puts under the steppers,
 * which is the point: one panel, one floor. The band grows by the difference and
 * that is the cost of the fix — a suggestion nobody can safely dismiss is worse
 * than a suggestion that takes another 29px.
 *
 * THE BAND'S OWN `align-items` CHANGED WITH IT — see its rule above, where the
 * change is declared once rather than in a second rule for the same selector.
 */
.sf-flow-nudge--offer .sf-flow-nudge__action,
.sf-flow-nudge--offer .sf-flow-nudge__decline {
  align-items: center;
  display: inline-flex;
  min-height: 44px;
}

/* ---- the money moment ---- */

/* 2px faint rule, 700 label, 24px mono figure — the pattern, unchanged
 * (tokens §4). */
.sf-flow-rail__total {
  align-items: center;
  border-top: var(--border-money);
  display: flex;
  justify-content: space-between;
  margin-top: 14px;
  padding-top: 14px;
}

.sf-flow-rail__total-label {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-emphasis);
  font-weight: 700;
}

.sf-flow-rail__total-money {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: 24px;
  font-weight: 700;
}

/*
 * W119 (Ryan, 2026-09-04): THE SECOND PLACE-ORDER BUTTON, below the money
 * moment.
 *
 * It is REVEALED rather than disabled (see `FlowRailPlaceOrder`), so this block
 * only ever exists on a ready form — which is also why it needs no disabled
 * treatment of its own. The button itself is `.sf-flow-btn--block`, the rail's
 * own primary, so there is one drawn button in this column and not two ideas of
 * one.
 *
 * 14px OF LEAD is the rail's own gap between the money moment and whatever
 * follows it — the same distance the Checkout link takes on steps 1 and 2, so
 * the reveal lands where a reader's eye already expects the action.
 */
.sf-flow-rail__place {
  margin-top: 14px;
}

/* The answer, where it was asked for. Empty until there is one, and reserving
 * no height while it is — the same shape `.sf-flow-ck__result` uses under the
 * main bar. */
.sf-flow-rail__result {
  margin: 0;
}

.sf-flow-rail__result:not(:empty) {
  margin-top: 10px;
}

.sf-flow-rail__note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  line-height: 1.55;
  margin-top: 12px;
}

.sf-flow-rail__note--financing {
  line-height: normal;
  margin-top: 10px;
}

/* ---- step 3: the phone rung (<=767.98) ---- */

/*
 * NO BOARD DRAWS THE CHECKOUT ON A PHONE, so this rung is DERIVED and flagged.
 * What it derives from is the shape every mobile checkout has converged on,
 * and it is one rule rather than a table of exceptions:
 *
 *   EVERY ROW IS TWO EQUAL COLUMNS, and the FIRST field of a three-field row
 *   spans both. That produces City over State/ZIP and Card number over
 *   Expires/CVC — the standard pairings — while First/Last, Email/Phone and
 *   Address/Apt stay side by side, which they comfortably do at 390.
 *
 * MEASURED, NOT ASSUMED. At 390 the card's inner measure is 304px (390 − the
 * ruled 15px gutters − the card's 2x28 padding), so two tracks are 145 each and
 * a three-track `2fr 1fr 1fr` would have been 130/65/65 — which is where the
 * board's own placeholders started clipping ("1234 5678 901", "MM /").
 */
@media (max-width: 767.98px) {
  .sf-flow-ck__row {
    grid-template-columns: 1fr 1fr;
  }

  /* `:has()` asks the ROW how many fields it holds rather than making the
   * component label it — the arrangement stays one fact, in the data. */
  .sf-flow-ck__row:has(> :nth-child(3)) > :first-child,
  .sf-flow-ck__row > :only-child {
    grid-column: span 2;
  }

  /*
   * THE ONE ROW SHAPE THE BLANKET RULE GETS WRONG (W76, 2026-09-03).
   *
   * A phone number beside its extension is not two peers, and two equal columns
   * treat it as though it were: at 390 the card's inner measure is 304px, so
   * each track is 145 and the number's control has 117px of text room —
   * "(281) 555-0143" at the platform's 16px measures right at that edge and
   * clips, while a four-digit extension sits in the same 145 half empty. The
   * row's DECLARED `2fr 1fr` gives 193/97, which fits both.
   *
   * IT IS A MODIFIER RATHER THAN ANOTHER `:has()` because the question is not
   * one the DOM can answer — "is the second field narrower than the first" is
   * about what the fields MEAN, and that lives in the copy. `CheckoutRow` takes
   * `keepTracksOnPhone` and the two phone rows pass it; every other row on the
   * page restacks exactly as it did.
   */
  .sf-flow-ck__row--keep {
    grid-template-columns: var(--sf-ck-cols, 1fr);
  }

  /*
   * AND THE INVERSE CASE (W97, Ryan 2026-09-03): "on mobile the address field
   * and apt/suite each get a full-width row; desktop keeps the 2fr/1fr pair."
   *
   * The rule above says "do not restack me"; this says "restack me FURTHER than
   * the blanket rule does" — two columns down to one. At 390 the card's inner
   * measure is 304px, so the blanket rule hands a street address 145 of it,
   * which is half a phone for the longest string this card holds, beside an
   * apartment box that genuinely needs almost none. One row each gives the
   * street the whole measure and costs one row of height.
   *
   * BOTH ADDRESS CARDS TAKE IT — the shipping one and the billing study — since
   * a billing street is the same length as a shipping street.
   */
  .sf-flow-ck__row--stack {
    grid-template-columns: 1fr;
  }

  /* The card's own padding comes in with the measure — the board's 28 either
   * side of a 304px card leaves too little for a two-up row. */
  .sf-flow-ck {
    padding: 22px 18px;
  }
}

/* ---- the checkout step's derived rows ---- */

/*
 * THE TAX ROW, THE RARE FREIGHT ROW AND THE GRAND TOTAL — drawn only on step 3,
 * because only step 3 hands the rail its `totals` (see `FlowRailTotals`). Every
 * rule here is scoped to the presence of `.sf-flow-rail__totals`, so the other
 * two steps' rails are byte-identical to what they were before this pass — the
 * bar every recorded gate on them is.
 *
 * THE MONEY MOMENT TRAVELS DOWN. On steps 1 and 2 the subtotal IS the money
 * moment: 2px `faint` rule, 700 label, 24px mono. On the checkout the board
 * demotes it to an ordinary hairline row (14.5 label, 15px mono) and puts the
 * 2px rule under the TAX, on the TOTAL. `:has()` is what says "this rail has a
 * total below" without the component having to pass a second flag for a fact
 * the first one already carries.
 */
.sf-flow-rail__body:has(.sf-flow-rail__totals) > .sf-flow-rail__total {
  border-top: var(--border-hairline);
  margin-top: 12px;
  padding-top: 12px;
}

.sf-flow-rail__body:has(.sf-flow-rail__totals) > .sf-flow-rail__total .sf-flow-rail__total-label {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  font-weight: 400;
}

.sf-flow-rail__body:has(.sf-flow-rail__totals) > .sf-flow-rail__total .sf-flow-rail__total-money {
  font-size: var(--text-body);
  font-weight: 400;
}

/* The board's own 10px between the subtotal and the tax row. */
.sf-flow-rail__row {
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-support);
  gap: 10px;
  justify-content: space-between;
  margin: 10px 0 0;
}

.sf-flow-rail__row-label {
  min-width: 0;
}

/*
 * `.sf-flow-rail__row-note` STOOD HERE AND IS GONE (W81, Ryan 2026-09-03). It
 * drew the muted run after the tax row's label — "· calculated for your
 * address" — and the ruling struck the run along with the sentence: the row's
 * VALUE says "Pending Calculation." now, in the place the reader is looking.
 * It was the only instance of the class.
 */

.sf-flow-rail__row-money {
  color: var(--brand-color-ink, #17181a);
  flex: none;
  font-family: var(--brand-font-mono, monospace);
}

/*
 * ═══ W94 (Ryan, 2026-09-03): A WAITING SLOT WEARS THE LABEL'S CLOTHES ═══
 *
 * W81 put words where a figure goes, and the words then wore the FIGURE's
 * treatment — mono, ink, and on the total 24px/700. Walking it on dark, that
 * made "Pending Calculation" the loudest thing in the rail, shouting about the
 * one number nobody has computed. Ryan ruled it DOWN to the Subtotal / Sales
 * Tax LABEL treatment while it is pending.
 *
 * WHICH IS EXACTLY WHAT THIS SETS, property for property, from the rules
 * fifty lines up: `ink-soft`, `--text-support`, weight 400 — and the body face
 * rather than the mono one, because mono is a NUMERAL treatment (tokens §4) and
 * there is no numeral here. When a real figure lands the modifier is not
 * rendered and every one of these reverts to the value treatment above, which
 * is the half of the ruling that has to keep working.
 */
.sf-flow-rail__row-money--pending,
.sf-flow-rail__total-money--pending {
  color: var(--brand-color-ink-soft, #44464a);
  font-family: inherit;
  font-size: var(--text-support);
  font-weight: 400;
}

/*
 * ═══ W120 (Ryan, 2026-09-04): THE TAX-EXEMPT RUNG ═══
 *
 * "'I'm sales tax exempt' button by the tax line after calc → upload flow."
 *
 * IT IS NO BOARD'S — the drawing predates the feature — so this rung is DERIVED
 * and flagged. What it derives from is the sheet's own quiet vocabulary rather
 * than a new one: `.sf-flow-nudge__action`'s shape (a bare button that reads as
 * a link inside a line of text) at the muted colour and the small-ui size the
 * tokens give every quiet link on this site.
 *
 * QUIET IS THE RULING, not a default. Ryan's word for it is "button" and the
 * treatment is deliberately not one: this control is for the small minority of
 * shoppers who are genuinely sales tax exempt, and an accent button beside the
 * tax row would read as something everybody is being offered.
 *
 * 4px UNDER THE ROW, which is inside the row's own 10px rhythm rather than
 * beside it — the rung belongs TO the tax row and a full 10 would make it a
 * fourth line in the column.
 *
 * NOTHING HERE RENDERS AT REST. The component draws this rung only once the tax
 * row holds a real figure, which no server render, no env-less render and no
 * recorded pixel gate ever reaches — so `a3-rail`'s crop is unchanged by it.
 */
/*
 * W145 (Ryan, 2026-09-04): THE ASK, ON THE LABEL'S OWN LINE.
 *
 * It sits inside `.sf-flow-rail__row-label` now, so the row's `space-between`
 * keeps the figure hard right and this rides after the words "Sales Tax". The
 * 8px is the gap between a label and an aside on it — closer than the row's own
 * 10px column gap, because this belongs TO the label rather than beside it.
 *
 * `white-space: nowrap` so the trigger never breaks mid-phrase into the money
 * column; the label itself is `min-width: 0` and wraps first, which is the
 * right order — the sentence gives before the control does.
 */
.sf-flow-rail__exempt-ask--inline {
  margin-left: 8px;
  white-space: nowrap;
}

/*
 * W159 (Ryan, 2026-09-04): THE APPLIED STATE IS ON THE LABEL'S LINE.
 *
 * "Once an exemption is applied, the rail renders it INLINE WITH THE SALES TAX
 * LABEL — 'Exemption applied' plus a Remove link, ONE LINE, not a separate
 * paragraph below."
 *
 * It was a `display: flex` block with `margin: 4px 0 0` — its own row under the
 * tax. `inline-flex` puts the pair INSIDE the label span beside the ask it
 * replaces, so the row's `space-between` keeps the figure hard right exactly as
 * it does for the ask (W145). The 8px lead is the ask's own
 * (`--exempt-ask--inline`), so the two states start in the same place and the
 * label does not shift when one becomes the other.
 */
.sf-flow-rail__exempt {
  align-items: baseline;
  display: inline-flex;
  gap: 0 8px;
  margin-left: 8px;
}

/*
 * THE TRIGGER AND THE UNDO, one treatment. Both are quiet links in a line of
 * text and there is no reason for them to differ — "Remove" is not a more
 * dangerous act than "I'm sales tax exempt" is a commitment; each simply
 * changes what the page is carrying, and the other one is always one press
 * away.
 *
 * THE PLATFORM RING IS ALREADY ON THEM. `.sf-flow-rail button:focus-visible`
 * fifty lines from the top of this sheet rings every control in the rail and
 * composes the white halo that separates it from the card's own hairline — so
 * there is no focus rule here, which is the sheet's ONE-BLOCK organisation
 * working rather than an omission.
 */
.sf-flow-rail__exempt-ask,
.sf-flow-rail__exempt-drop {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  flex: none;
  font-family: inherit;
  font-size: var(--text-small);
  padding: 0;
  text-decoration: underline;
}

.sf-flow-rail__exempt-ask:hover,
.sf-flow-rail__exempt-drop:hover {
  color: var(--brand-color-ink, #17181a);
}

/* What the page is carrying, once it is carrying one. Muted like the control
 * beside it, and `min-width: 0` so the line gives before the row does — the
 * label column wraps first and "Remove" is never pushed off the end of it. */
.sf-flow-rail__exempt-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  line-height: 1.45;
  min-width: 0;
}

/* The board's money moment, unchanged (tokens §4) — it has simply moved down
 * past the tax. `.sf-flow-rail__total` already carries all of it; the modifier
 * exists so the demotion rule above can exclude this one by SELECTOR rather
 * than by hoping it is never a direct child of the body. */
.sf-flow-rail__total--grand {
  margin-top: 14px;
  padding-top: 14px;
}

/* The sentence that says why the two figures above are a dash. It is not the
 * board's — the board has no state for "nobody asked" — and it is the one thing
 * on this rail that would be deleted rather than changed the day the seam
 * answers. */
.sf-flow-rail__note--pending {
  margin-top: 12px;
}

/* ---- the edit state ---- */

/*
 * ONE CLOSING RULE, NOT TWO (GUI review F7).
 *
 * This panel used to draw its own bottom hairline AND sit directly above the
 * subtotal's rule, so the edit state ended in two horizontal lines a few pixels
 * apart with nothing between them — read as a rendering fault rather than as
 * structure, and it competed with the between-blocks rules W171 had just made
 * meaningful. The money moment's rule closes the panel; this one is deleted.
 *
 * THE PADDING STAYS, because it was never the border's: it is the air between
 * the last block and the subtotal, and dropping it would pull them together.
 */
.sf-flow-edit {
  margin-top: 12px;
  padding-bottom: 14px;
}

.sf-flow-edit__row {
  display: flex;
  gap: 10px;
}

.sf-flow-edit__main {
  flex: 1;
}

/*
 * ONE NAME SIZE ACROSS BOTH MODES (GUI review F10).
 *
 * It was `--text-small` (13.5) while the VIEW state's `.sf-flow-line__name`
 * draws `--text-support` (14.5) — so pressing Edit shrank every product name on
 * the panel by a pixel. Nobody would name it as a bug and everybody would feel
 * the panel "resetting" on a toggle that is supposed to reveal controls, not
 * restyle content. A name is a name in either mode.
 *
 * IT IS THE VIEW STATE'S OWN TOKEN, not a 14.5 that happens to match: the two
 * rules now read from one place, so the next re-ruling of a line's name moves
 * both modes or neither. That is the difference between agreeing today and
 * being unable to disagree.
 */
.sf-flow-edit__name {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 600;
}

/* The edit state's figures are one step down from the view state's, as drawn. */
.sf-flow-edit .sf-flow-line__amount {
  font-size: var(--text-small);
}

/*
 * THE QUANTITY'S OWN LINE (Ryan, 2026-08-26), below the photo-and-title row
 * rather than tucked under the title inside it. It wears the OPTION ROW's
 * vocabulary — the label column at the same 76px, the same 8px gap, the same
 * row height — so the three changeable things in this state read as one list
 * with one left edge, which is what "mirror the option-row labelling" asks for.
 */
.sf-flow-edit__qty-row {
  align-items: center;
  display: flex;
  font-size: var(--text-small);
  gap: 8px;
  margin-top: 12px;
}

/* THE ONE QTY CONTROL (ruled): the parent's stepper, and nothing else on the
 * rail is editable. */
.sf-flow-qty {
  align-items: center;
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: inline-flex;
}

.sf-flow-qty__step {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-small);
  padding: 5px 11px;
}

.sf-flow-qty__step:disabled {
  cursor: default;
  opacity: 0.45;
}

/*
 * ═══ W171: THE SAME STEPPER, BUILT OUT OF LINKS ═══
 *
 * An accessory's quantity lives in the query string (the step-2 card edits the
 * same number), so the rail changes it by GOING somewhere rather than by
 * handling a press — see `flow-rail-edit-row.tsx` for the whole argument. What a
 * reader sees has to be one control either way, so these three declarations are
 * simply what a `<button>` already computes: `inline-block`, centred text, and
 * `line-height: normal`. They are scoped to `a` so the hoop's own button
 * stepper is untouched — a shared declaration would have been a change to a
 * control that two gates measure.
 */
a.sf-flow-qty__step {
  display: inline-block;
  line-height: normal;
  text-align: center;
  text-decoration: none;
}

/*
 * THE RANGE'S ENDS, where a link would have nowhere to go. It is the accessory
 * card's own `--off` precedent: the glyph stays so the control does not change
 * width, drawn exactly as `:disabled` draws the button stepper's ends, and it
 * is `aria-hidden` in the markup because it is not a control at all. The way
 * down from one is Remove, inside the same block.
 */
.sf-flow-qty__step--off {
  cursor: default;
  opacity: 0.45;
}

.sf-flow-qty__value {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-small);
  padding: 5px;
}

/*
 * `.sf-flow-edit__remove` STOOD HERE AND IS RETIRED BY NAME (W171).
 *
 * It dressed the SYSTEM line's Remove, which was an `aria-disabled` stub —
 * removing the only system from a flow that exists to buy it is a destination
 * question rather than a button. Ryan's pattern ruling "tolerates absent slots,
 * it doesn't fork", so the slot is empty now and the rule that drew a
 * permanently-dead control goes with it. The per-line Remove that really acts is
 * `.sf-flow-edit__line-remove` below, and it is a ghost, not a quiet link.
 */

/*
 * W152 x W171: the offer, raised inside its own accessory's block, takes the
 * same 12px rhythm every other thing in a block opens with — the quantity row
 * and the options block each carry it. Its `margin-bottom: 8px` and its 26px
 * indent come from `.sf-flow-nudge` and are left alone: the indent is what makes
 * it read as belonging to the line above rather than as a row of its own.
 *
 * A DESCENDANT SELECTOR, not a child one: W171 moved the band down a level into
 * `.sf-flow-edit__line`, and the `>` this used to carry would have quietly
 * stopped matching — the class of bug the sweep cannot see, because the rule
 * still has markup somewhere on the page.
 */
.sf-flow-edit .sf-flow-nudge--offer {
  margin-top: 12px;
  /*
   * FLUSH TO THE BLOCK'S SPINE (GUI review F11).
   *
   * `.sf-flow-nudge` indents 26px, and that indent was earned in the VIEW state:
   * there the band sits beside a thumbnail column and the inset it borrows its
   * shape from, so stepping in is how it says "this belongs to the line above".
   * Inside an edit BLOCK it buys nothing — belonging is already said by the rule
   * above and the rule below — and it costs the one thing W171 built: the single
   * left spine every label, thumbnail and now Remove line up on. So the indent
   * goes here and stays there.
   */
  margin-left: 0;
}

/*
 * ═══ W171 (Ryan, 2026-09-04): ONE BLOCK PER PRODUCT, RULED APART ═══
 *
 * "Kind of a mess … not consistent between the basketball hoop and anything
 *  else in the order. Consistency is king!"
 *
 * His first complaint was ownership: W167's Remove buttons floated in a list and
 * he could not tell which belonged to which product. The fix is not the button —
 * it is the BLOCK. Every line on this panel (the hoop included) is now one
 * `.sf-flow-edit__line` holding its own photograph, name, money, stepper, extras
 * and Remove, and the blocks are separated by a horizontal rule with generous
 * air on both sides. Nothing inside a block can be read as belonging to the one
 * above it.
 *
 * ─── THE RULE IS A SIBLING SELECTOR, WHICH IS THE WHOLE TRICK ───────────────
 *
 * `+` rather than a border on every block: the first block must not carry a rule
 * (the panel's top edge already separates it from the rail's head), and the last
 * must not carry one either (`.sf-flow-edit`'s own bottom hairline closes the
 * panel). A sibling selector states exactly "between two of these" and needs no
 * `:first-child` exception to walk back.
 *
 * ─── 22px, AND WHY IT IS NOT THE 12 EVERYTHING ELSE USES ────────────────────
 *
 * Inside a block the rhythm is 12px — the quantity row, the options block, the
 * lettering field and the Remove all open with it. "Generous air" has to be
 * legibly MORE than that or the rule reads as one more row rather than as a
 * boundary, so the between-blocks gap is 22 above and 22 below the hairline. The
 * result is a 44px band around a 1px line, which is the largest space anywhere
 * on this panel and therefore unambiguous about what it separates.
 *
 * ─── WHAT THIS REPLACED, BY NAME ────────────────────────────────────────────
 *
 * `.sf-flow-edit__lines` (a wrapper around everything that was not the hoop),
 * `.sf-flow-edit__line-head` and `.sf-flow-edit__line-name` (a second head row
 * and a second name treatment, parallel to the hoop's own). All three existed
 * because W167 drew accessories differently from the system; W171's ruling is
 * that there is no "differently", so the hoop's `.sf-flow-edit__row` and
 * `.sf-flow-edit__name` dress every line and those three rules are retired.
 */
.sf-flow-edit__line + .sf-flow-edit__line {
  border-top: var(--border-hairline);
  margin-top: 22px;
  padding-top: 22px;
}

/*
 * ═══ THE FACT ROW (GUI review F4) ═══
 *
 * The install visit's block was the only one with no labelled row — it has no
 * quantity of its own, because a visit is one per hoop and follows the system's
 * count — so it read as a name, a price and a lone control. And at three hoops
 * its amount is three times the figure the shopper was quoted, with nothing on
 * the panel saying why.
 *
 * ONE STATIC TWO-COLUMN ROW ANSWERS BOTH. It is the quantity row's geometry
 * exactly — the same `.sf-flow-option__label` in the left column, the same 8px
 * gap, the same 12px opening rhythm — so the spine runs unbroken down the panel
 * whether a row's right-hand side is a stepper or a sentence.
 */
.sf-flow-edit__fact-row {
  align-items: baseline;
  display: flex;
  font-size: var(--text-small);
  gap: 8px;
  margin-top: 12px;
  /*
   * THE SAME ROW HEIGHT AS EVERY OTHER CONTROL ROW (W179). The quantity rows
   * stand 28px because that is the stepper's box; this row has no stepper, so
   * left alone it collapsed to its 16px text line and the install's block sat a
   * rung shallower than the three above it. Measured 16 vs 28 at 1440. One
   * declaration makes every line's control row the same height whatever is in
   * it, which is the whole of "consistency is king" at this scale.
   */
  min-height: 28px;
}

/*
 * `.sf-flow-edit__fact` STOOD HERE AND IS RETIRED BY NAME (W179). It dressed
 * the fact row's right-hand VALUE column; that column is where the install's
 * Remove goes now, so the label and the value merged into one run and the
 * separate value element no longer exists.
 */

/*
 * THE BLOCK ITSELF IS A PLAIN FLOW BOX, deliberately — no flex, no gap.
 *
 * Its children already carry their own `margin-top: 12px` (the quantity row and
 * the options block have since Ryan's 2026-08-26 ruling), and a gap on top of
 * those margins would have doubled the hoop's internal rhythm — a re-layout of
 * the one arrangement W171 names as the REFERENCE. Keeping the box plain is what
 * makes "the hoop is unchanged" literally true.
 */
.sf-flow-edit__line {
  display: block;
}

/*
 * ═══ REMOVE MOVED INTO THE CONTROL ROW (W179) ═══
 *
 * `.sf-flow-edit__line-actions` STOOD HERE AND IS RETIRED BY NAME. The GUI
 * review's F1 took the Remove off the right edge, stripped its ghost box and
 * gave it a 40px row of its own on the block's left spine. Ryan walked that and
 * moved it one step further: into the row it acts on, between the "Quantity"
 * label and the stepper. A row whose only job was to hold one quiet link is a
 * row the panel does not need.
 *
 * EVERYTHING F1 WON IS KEPT, which is the test of the move rather than a hope:
 * the control is still quiet (`.sf-flow-quiet--button`), still starts on the
 * spine (its row's label is `width: 100%`, so below the collapsed rung the label
 * takes its own line and Remove leads the control line at the same left edge),
 * and is still a real target — the control ROW carries the 44px floor at that
 * rung now, which is the row the review's own F9 had already put a floor on.
 */
/*
 * ═══ W202 ITEM 6b (Ryan, 2026-09-09): REMOVE IS A REAL BUTTON ═══
 *
 *   "Have this be a button, giving it a proper hit space."
 *
 * IT WAS A BARE UNDERLINED RUN — `.sf-flow-quiet--button`, 13.5/400, no box —
 * and W200 item 8 put it 8px from the stepper's `+`. Measured at 390: the
 * control's own drawn height was 16px, beside a 44px tap target its edge nearly
 * touched. A destructive control that small, that close to a control people
 * press repeatedly, is a mis-tap the layout was inviting.
 *
 * ─── THE BOX IS THE HOUSE'S COMPACT RUNG ────────────────────────────────────
 *
 * 40px, the `--control-height-sm` every other button on this panel stands at
 * (Ryan's "a button is a button", 2026-09-01) — the same rung the band's View
 * and Checkout share. Nothing here invents a size.
 *
 * ─── AND THE TREATMENT IS QUIET, WHICH IS THE HALF THAT MATTERS ─────────────
 *
 * A HAIRLINE BORDER rather than the ghost's full ink outline, and MUTED type
 * rather than ink: the control has to read as pressable without becoming the
 * loudest thing in its line's block. That was the GUI review's F1 finding in
 * the first place — a bordered ink Remove made the destructive control the
 * loudest thing there — and it is why this is not simply `.sf-flow-ghost`.
 * Hover brings it up to full ink, so intent is confirmed at the moment a
 * pointer is actually on it.
 *
 * NEVER THE ACCENT PRIMARY. That is Checkout's, and an orange Remove beside a
 * stepper is precisely the reactionary tap this item exists to prevent.
 *
 * ─── THE TARGET IS 44, THE DRAWN BOX IS 40 ─────────────────────────────────
 *
 * W198 item 11's own pattern, and its arithmetic verbatim: an absolutely
 * positioned child is laid out against its containing block's PADDING box —
 * the 40px border box minus this control's 1px border on each side, so 38 —
 * and 38 + 3 + 3 = 44. `-2px` measured 42 and the driven hit test caught it at
 * the corners. The pseudo-element is transparent and deliberately NOT
 * `pointer-events: none`: taking the press is the whole job, and it is a child
 * of the control, so a press on it is a press on the control.
 *
 * `flex: none` SURVIVES from the retired rule and still earns it: the control
 * sits beside the stepper in a flex row and must not grow into it.
 */
.sf-flow-edit__line-remove {
  align-items: center;
  background: transparent;
  border: 1px solid var(--brand-color-line-mid, #d8d6d0);
  border-radius: var(--radius-control);
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  font-family: inherit;
  font-size: var(--text-small);
  font-weight: 600;
  justify-content: center;
  min-height: var(--control-height-sm, 40px);
  padding: 0 14px;
  position: relative;
  text-decoration: none;
}

.sf-flow-edit__line-remove::after {
  content: "";
  inset: -3px;
  position: absolute;
}

.sf-flow-edit__line-remove:hover {
  border-color: var(--brand-color-ink, #17181a);
  color: var(--brand-color-ink, #17181a);
}

/* ---- W167: the lettering field ---- */

/*
 * THE SMALLEST THING THAT CAN CHANGE THE TEXT — a label, a field, a counter.
 * The step-2 card's simulator (the pad photograph with the glyphs standing on
 * it) is a PICTURE OF THE PRODUCT and needs a card's width to read; this column
 * is 336px and the shopper here is fixing a typo, not deciding whether to buy
 * lettering. Same authored words, same normaliser, two renderings.
 */
.sf-flow-edit__text {
  display: flex;
  flex-direction: column;
  /*
   * THE ROW SETS THE REGISTER, AND THAT IS HOW F5 IS ACTUALLY SATISFIED.
   *
   * `.sf-flow-option__label` declares COLOUR and WIDTH and no font-size at all —
   * the option rows read 13.5 by INHERITING it from `.sf-flow-options`, and the
   * quantity row by inheriting it from `.sf-flow-edit__qty-row`. So putting the
   * shared class on this label bought the colour and the full-width break and
   * left the size inheriting from the rail body: 16px, measured live, which is
   * LARGER than the 12.5 the review complained about rather than equal to the
   * 13.5 it asked for.
   *
   * Declaring the size on the label would have "fixed" it and reintroduced
   * exactly the second declaration F5 exists to remove. Setting it on the ROW is
   * what the other two rows do, so the label still declares nothing and all
   * three labels get their size the same way.
   */
  font-size: var(--text-small);
  gap: 4px;
  /*
   * W171 POINT 3, SOLVED BY THE STRUCTURE RATHER THAN BY THIS RULE. Ryan: the
   * full-width field "looked odd against the name indented by its static
   * quantity prefix" — and his own diagnosis was the fix. The prefix is gone
   * (it became the stepper), so the name starts at the block's left edge, the
   * field starts at the same edge, and the two agree without either being
   * nudged. All this line does is give the field the block's own 12px rhythm.
   */
  margin-top: 12px;
}

/*
 * `.sf-flow-edit__text-label` DECLARES NOTHING NOW (GUI review F5).
 *
 * It was 12.5/600 on a panel whose every other label is 13.5/400 — measured on
 * both — so "Lettering" sat a size down and a weight up from "Quantity"
 * directly above it, which reads as a different KIND of label rather than the
 * same one. The element wears `.sf-flow-option__label` outright now, so there
 * is no second declaration left to drift and the day that label's type is
 * re-ruled this moves with it. The class stays in the markup as the hook the
 * review's own probe reads; it has no rule, which is the point.
 */

/* The card's own field, on the COMPACT rung rather than the full one — it
 * stands beside a 40px ghost in a 336px column, and BTN-UNIFY's W13 is that a
 * field is a button's height. */
.sf-flow-edit__text-input {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  font-family: inherit;
  font-size: var(--text-small);
  min-height: var(--control-height-sm, 40px);
  padding: 0 12px;
  width: 100%;
}

.sf-flow-edit__text-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  line-height: 1.4;
  margin: 0;
}

.sf-flow-options {
  display: flex;
  flex-direction: column;
  font-size: var(--text-small);
  gap: 10px;
  margin-top: 12px;
}

.sf-flow-options__eyebrow {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
}

/*
 * AN OPTION ROW IS NOW JUST ITS LABEL AND ITS CHIPS. The right-hand delta
 * column is deleted (Ryan, 2026-08-26 — the figures moved into the chips), and
 * with it the `justify-content: space-between` that only existed to push it
 * there. What that column cost, beside the duplication: it was `flex: none`
 * against a WRAPPING chip list, so a wide figure pushed the chips onto a second
 * line and left the figure floating at the row's mid-height — the defect flagged
 * on this page the day it shipped, fixed here by removal rather than by tuning.
 */
.sf-flow-option {
  align-items: center;
  display: flex;
  gap: 8px;
}

/* The chips wrap under their label, so the flex gap has to carry the row's
 * vertical rhythm too: 6px between the label and its chips, the board's 8
 * between the chips themselves. */
.sf-flow-option__choices {
  row-gap: 6px;
}

.sf-flow-option__choices {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/*
 * THE LABEL TAKES ITS OWN LINE, above its chips — `width: 100%` inside the
 * wrapping chip flex breaks after it. The board draws it as a 76px COLUMN
 * beside the chips, and that shape did not survive Ryan's 2026-08-26 ruling
 * moving the price into each chip: a priced `Adjustment` row is 323.6px wide
 * against the 314 a 360 rail left. Something had to give, and it is the least
 * costly thing on the row — the label is one word, it reads as a header over
 * the choices it names, and the chips get the whole measure and hold one line
 * at every width the site draws (down to 320, where they set 239.6 into 260).
 *
 * THE 380 RAIL (Ryan, 2026-08-27) GIVES THAT WIDTH BACK — AT ONE RUNG. Measured
 * on the widened grid: the rail body leaves 334px, and the priced `Adjustment`
 * row set side by side is 319.3 (71.7 label + 8 + 103.9 + 8 + 127.7), so the
 * board's original row fits at 1440 with 14.7 to spare. It does NOT fit at the
 * 1239.98 rung, where the rail is still 300 and leaves 254 — side by side would
 * need a ladder, and the label-above shape is the one that holds at every rung
 * with none. Kept on that measurement, and FLAGGED for Ryan rather than
 * reversed: one line either way, and it is a drawing decision, not a
 * consequence of the width.
 */
.sf-flow-option__label {
  color: var(--brand-color-ink-soft, #44464a);
  width: 100%;
}

/*
 * THE ROW'S HEAD: the label, and the door into the explanation (Ryan's W8,
 * 2026-09-01 — "PDP already explains each option; link the same way from the
 * editor").
 *
 * TRANSPLANTED FROM `.sf-pdp-mbuy__option-head`, which is the product page's
 * own arrangement for a narrow column: label at the left, quiet link opposite
 * it, chips on the line below. Same three declarations — `baseline` so the
 * caption-sized link sits on the label's line rather than 2px under it, and
 * `space-between` for the opposite edges. What is NOT copied is that head's
 * `font-size: var(--text-caption)`, which there applies to the label too: this
 * rail's label is recorded by `flowgate`'s `editoptions` crop at the block's
 * own `--text-small`, so only the LINK drops a rung.
 *
 * `width: 100%` IS THE ONE ADDITION, and it is what the PDP does not need. Over
 * there the head is a block child of a column; here it is a flex ITEM of the
 * wrapping chip row, and the full width is what breaks the line after it —
 * exactly the mechanism the label itself used before this element existed. The
 * label keeps its own `width: 100%` so it fills what the link leaves.
 */
.sf-flow-option__head {
  align-items: baseline;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  width: 100%;
}

/* A fixed box beside a run that gives — and one rung quieter than the row it
 * names, which is the rail's own register for a secondary affordance. (It used
 * to cite `.sf-flow-edit__remove` as the sibling example; W171 retired that
 * control, and the register it named is unchanged.) The colour, the underline
 * and the hover are `.sf-flow-quiet`'s; this adds only the size and the box. */
.sf-flow-option__learn {
  flex: none;
  font-size: var(--text-caption);
}

/* 1.5px accent border IS the selection language — never normalised to 1px
 * (tokens §1). */
.sf-flow-chip {
  background: none;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-small);
  padding: 5px 12px;
}

.sf-flow-chip--on {
  border: var(--border-selected);
  font-weight: 600;
}

/*
 * THE PRICE INSIDE THE CHIP (Ryan, 2026-08-26) — the PDP's own price language,
 * where the phone product board already writes "Rust Armor +$129" in the chip.
 * Mono, because money is a mono duty (tokens §2), and one step down so the
 * figure sits beside the label without competing with it. `white-space: nowrap`
 * is load-bearing: it is what stops a chip breaking between its name and its
 * price, which is the wrap this move was made to end.
 */
.sf-flow-chip__delta {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  font-weight: 400;
  white-space: nowrap;
}

/*
 * ─── THREE DRILL SHAPES HAVE BEEN RETIRED IN THIS RAIL, ALL BY NAME ───────
 *
 * `.sf-flow-option--sub` and `.sf-flow-option--sub .sf-flow-option__label`
 * dressed the drill as a derived child of the `adjustment` row — paper ground,
 * 2px inset rule, quieter label. Ryan, 2026-08-27: "add it as a separate line
 * item (not as part of a configuration)."
 *
 * `.sf-flow-items` and `.sf-flow-items__eyebrow` then gave it a block of its
 * own under the options, and `.sf-flow-item__main`, `__photo`, `__body` and
 * `__chips` drew MP1's photo-and-chips group inside it (after
 * `.sf-flow-item--on`, `__price` and `__toggle` had drawn the Add/Remove row
 * before them).
 *
 * ─── AND WHAT DRAWS NOW (Ryan's WP6, 2026-08-29) ──────────────────────────
 *
 * One checkbox — "Add drill — $149" — INSIDE `.sf-flow-options`, immediately
 * after the option row whose chip gates it. The 2026-08-27 ruling that gave it
 * a block of its own is superseded in as many words: "directly under the
 * Lightning chip row."
 *
 * NO INDENT HERE. This rail's option label is `width: 100%` — it takes its own
 * line above its chips, measured at 360px and kept at 380 — so the chips start
 * at the block's own left edge and so does this.
 *
 * ITS TWIN IS `.sf-pdp-item` IN pdp.css, and the two sheets are deliberately
 * not merged (share the engine, keep the chrome per-surface — the chips and
 * the stepper are the precedent). Both copy `.sf-flow-ck__check`'s checkbox,
 * which is this site's, four hundred lines above in this same file.
 */
.sf-flow-item {
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-small);
  gap: 2px;
}

/* The whole line is the tap target, at the platform's 44px floor. */
.sf-flow-item__control {
  align-items: center;
  cursor: pointer;
  display: flex;
  gap: 10px;
  min-height: 44px;
}

/* `.sf-flow-ck__box-mark` at the rail's scale: 18px, the same 1.5px accent
 * border and radius 4. */
.sf-flow-item__box-mark {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-selected);
  border-radius: var(--radius-inner);
  color: var(--brand-color-surface, #ffffff);
  display: inline-flex;
  flex: none;
  height: 18px;
  justify-content: center;
  position: relative;
  width: 18px;
}

.sf-flow-item__box {
  height: 100%;
  inset: 0;
  margin: 0;
  opacity: 0;
  position: absolute;
  width: 100%;
}

.sf-flow-item__box-mark > svg {
  visibility: hidden;
}

.sf-flow-item__box-mark:has(> .sf-flow-item__box:checked) {
  background: var(--brand-color-accent, #c4530a);
}

.sf-flow-item__box-mark:has(> .sf-flow-item__box:checked) > svg {
  visibility: visible;
}

.sf-flow-item__box-mark:has(> .sf-flow-item__box:focus-visible) {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

.sf-flow-item__box:focus-visible {
  outline: none;
}

.sf-flow-item__label {
  color: var(--brand-color-ink, #17181a);
  font-weight: 500;
}

/* The recovered sentence, at the rail's quietest rung. */
.sf-flow-item__note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  line-height: 1.5;
  margin: 0 0 2px;
}

/* ---- the collapsed bar (small screens only) ---- */

/*
 * THREE THINGS IN A ROW, and the row is read left to right (Ryan's ruling,
 * 2026-08-26): what the order IS, the button that opens it, and Checkout at the
 * far end. `flex-start` plus one auto margin, rather than `space-between`:
 * spread evenly, the expander would drift to the middle of the band, adjacent to
 * nothing, which is the one thing the ruling says it must not be.
 */
.sf-flow-rail__bar {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border-bottom: var(--border-hairline);
  display: none;
  gap: 12px;
  justify-content: flex-start;
  padding: 12px 16px;
}

/* Checkout takes the far end. */
.sf-flow-rail__bar .sf-flow-ghost {
  margin-left: auto;
}

/*
 * WHAT THE BAND SAYS — plain text, and drawn as plain text (Ryan's ruling,
 * 2026-08-26). No border, no ground, no pointer: a run that LOOKS like a control
 * and does nothing is worse than the caption the board drew. `baseline` rather
 * than `center` so the mono total sits on the label's line and not 1px off it.
 */
.sf-flow-rail__bar-text {
  /* CENTRED, not baseline-aligned, since the run's first item became a GLYPH
   * (Ryan's ruling, 2026-08-27). A drawn cart has no baseline to sit the mono
   * total on; centring is what puts the two on the same optical line. */
  align-items: center;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-support);
  gap: 8px;
  /* It may give before the two controls do — they are both fixed boxes and the
   * band has to hold one line at 320. */
  min-width: 0;
  /* The words "Your order" are still in here as `.sf-flow-sr`, and an
   * absolutely-positioned span whose ancestors are all static reaches the PAGE
   * for its containing block and extends the document's scrollable overflow
   * (playbook §5 — the same reason `.sf-flow-rail__disclose` is positioned). */
  position: relative;
}

/*
 * THE CART, WHICH IS WHAT THE BAND SAYS NOW (Ryan's ruling, 2026-08-27): "it's
 * kind of obvious that this is your cart", so the words go and the glyph
 * arrives — [cart] [total] … [arrow].
 *
 * THE SAME MARK, NOT A SECOND DRAWING: `CartMark` is the glyph the chrome's
 * header and the rail's own head already carry, at 18px here — one step under
 * the rail head's 22, which is the size the band's own 15/40 furniture sets.
 * `ink-soft` is inherited from the run, so the cart is as quiet as the label it
 * replaced and the mono total stays the one dark thing on the left.
 */
.sf-flow-rail__bar-cart {
  align-items: center;
  display: inline-flex;
  /* A fixed box beside a run that may give. */
  flex: none;
  justify-content: center;
}

.sf-flow-rail__bar-total {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-weight: 700;
  white-space: nowrap;
}

/*
 * W99 + W94 (Ryan, 2026-09-03): the band's slot while there is no total.
 *
 * The band now states the GRAND TOTAL on the checkout step rather than the
 * subtotal, and before tax and freight land there is no grand total — so it
 * says the rail's own "Pending Calculation", and says it at the quieter
 * register the rail uses for the same words. Bold mono figures are for
 * figures; this is the label treatment, exactly as `.sf-flow-rail__row-money
 * --pending` is.
 */
.sf-flow-rail__bar-total--pending {
  color: var(--brand-color-ink-soft, #44464a);
  font-family: inherit;
  font-size: var(--text-support);
  font-weight: 400;
}

/*
 * THE EXPANDER — its own near-square button, holding nothing but the arrow
 * (Ryan's ruling, 2026-08-26). It was the whole width of the band, wrapping the
 * label and the total, and that shape reads as a dropdown selecting between
 * orders. Now it is a control the size of a control.
 *
 * 40x40 EXACTLY: square, and on the 40px floor a tap target has to clear. It
 * takes the site's control box — 1px `line-mid`, radius 6, `paper` fill — which
 * is the same box the run used to wear, so nothing about the treatment changed,
 * only what is inside it.
 *
 * `flex: none` because it is a fixed box and the text beside it is the thing
 * allowed to give.
 *
 * `position: relative` so the sr-only verb inside cannot reach the page for its
 * containing block — see `.sf-flow-sr`.
 */
/*
 * IT IS THE SAME GHOST AS THE CHECKOUT BESIDE IT (Ryan's W1, 2026-09-01):
 * "both meant to be ghost buttons, one white one tinted".
 *
 * WHAT CHANGED IS THE TREATMENT, NOT THE BOX. It was `paper` fill (#fbfbfa) with
 * the light `line-mid` control border and `muted` ink — a filled, soft-edged
 * square — sitting 6px taller than a transparent, ink-outlined Checkout in the
 * same band. Two controls, one bar, two different ideas of what a button is.
 * Now both are `.sf-flow-ghost`'s ground and border, and the compact rung made
 * the Checkout 40, so the two agree on size without this rule moving at all:
 * the 40x40 square below is UNCHANGED and still the tap-target floor its own
 * ruling (2026-08-26) set.
 *
 * STILL A SQUARE, not `--button-pad-x-sm`: it holds a chevron, not a word, and
 * a 40x40 icon button is sized by its tap target. That is why it is exempt from
 * the rung's TYPE assertions in `tools/btngate.mjs` and not from its height one.
 */
/*
 * ═══ W202 ITEM 1 (Ryan, 2026-09-09): "VIEW", AND IT OPENS THE DRAWER ═══
 *
 * `.sf-flow-rail__disclose` and `.sf-flow-rail__bar-chevron` ARE RETIRED BY
 * NAME, and so is the [aria-expanded="true"] rule that turned the mark: the
 * inline dropdown they opened is gone, and a chevron pointing at a body this
 * rung no longer draws would be an arrow to nowhere.
 *
 * WHAT REPLACES IT IS THE SAME BOX WITH A WORD IN IT. Every property below is
 * the retired rule's, unchanged — the transparent ground, the ink border, the
 * control radius, the 40px height that is BOTH `.sf-flow-ghost`’s compact rung
 * and the tap-target floor (Ryan, 2026-08-26) — so the band’s two controls
 * still agree with each other exactly as W1 ruled they must ("both meant to be
 * ghost buttons, one white one tinted"). Only the fixed 40px WIDTH goes,
 * because a square was the right box for a chevron and the wrong one for a
 * word.
 *
 * IT IS NOT `.sf-flow-ghost` ITSELF, for the same reason it never was: that
 * class carries the ghost’s own type and padding scale, and this control is
 * sized by the band. Sharing the properties without sharing the class is what
 * has kept `btngate`’s height assertion true here while its TYPE assertions
 * stay scoped to the ghosts.
 *
 * `position: relative` IS KEPT and still earns its place — the composed name’s
 * runs are inside it, and an absolutely-positioned descendant whose ancestors
 * are not positioned escapes its scroller to the document (playbook §5).
 */
.sf-flow-rail__view {
  align-items: center;
  background: transparent;
  border: 1px solid var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: flex;
  flex: none;
  font-family: inherit;
  font-size: var(--text-small);
  font-weight: 600;
  height: var(--control-height-sm, 40px);
  justify-content: center;
  padding: 0 var(--button-pad-x-sm, 14px);
  position: relative;
}

/* ------------------------------------------------------------- responsive -- */

/*
 * THE BOARD'S OWN RESPONSIVE RULE, in its caption: "≥1200px the rail rides at
 * 360px; ~1024 it narrows to 300px; below ~900 it becomes this sticky collapsed
 * bar". Mapped onto the site's existing rungs rather than the board's round
 * numbers — 1239.98 and 919.98 are two of the six cuts chrome.css already
 * carries, and a page that changes shape 24px away from where the header does
 * is a page with two ladders. A named cut belongs to the WIDER side (playbook
 * §5).
 *
 * THE TOP OF THAT LADDER IS 380 NOW, not the caption's 360 (Ryan's ruling,
 * 2026-08-27 — see `.sf-flow__grid`). The two rungs BELOW it are unchanged: 300
 * here, and the full-bleed collapsed band at 919.98. The ruling widened the
 * rail; it did not re-draw the ladder under it.
 */
/*
 * THE DESKTOP RUNG — the one place the install pairs are flipped, so the phone
 * cannot inherit it (Ryan's ruling keeps the phone stacking install-first, and
 * the DOM is in that order). 768 is the site's own mobile cut and the width the
 * pairs already go single-column at, so nothing changes shape twice.
 *
 * THE RATIO. `1fr 1.5fr` puts the installation service at 60% of the pair's
 * measure — the ruling asked for "more than half, call it ~58-62%", and 60 is
 * also where the tier-1 board already was (its 1.35fr is 57.4%), so the two
 * pairs converge on one number instead of carrying two.
 */
@media (min-width: 768px) {
  .sf-flow-pair,
  .sf-flow-crew {
    grid-template-columns: 1fr 1.5fr;
  }

  /* Self-install LEFT, the service RIGHT. The DOM order is professional-first
   * for the phone, so the swap is here and only here. */
  .sf-flow-pair__card,
  .sf-flow-crew__service {
    order: 2;
  }

  .sf-flow-pair__card--self,
  .sf-flow-crew__self {
    order: 1;
  }

  /* The SERVICE card's call to action lands BOTTOM-RIGHT — the convention the
   * same ruling names, and the same right edge the step's own Continue keeps.
   * The self-install card beside it is excluded on purpose: its quiet link is
   * not a Continue, and a right-aligned link in the left column would read as
   * pointing at the card next to it. */
  .sf-flow-pair__card:not(.sf-flow-pair__card--self) .sf-flow-pair__action {
    text-align: right;
  }
}

@media (max-width: 1239.98px) {
  .sf-flow__grid {
    grid-template-columns: minmax(0, 1fr) 300px;
  }
}

@media (max-width: 919.98px) {
  /* One column, and the rail rides ABOVE the step content as the board's
   * caption has it ("sticky under the header … step content below").
   *
   * FLUSH AGAINST THE STEPPER (Ryan's ruling, 2026-08-27): the grid's 28px of
   * air was the only thing between the two bands, and a strip of page ground
   * between a `well` band and a white one reads as a seam rather than as a
   * stack. The three grounds now meet edge to edge — nav blue, paper title,
   * well stepper, white order band — which is the same "separated by a change
   * of ground, not by a rule" the stepper's own ruling is written on.
   *
   * THE GAP IS NAMED because the pinned rules below have to put exactly it back
   * when the band leaves flow. One number, one place. */
  .sf-flow__grid {
    --sf-flow-gap: 20px;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sf-flow-gap);
    margin-top: 0;
  }

  /*
   * THE COLLAPSED RAIL IS A FULL-BLEED BAND (Ryan's ruling, 2026-08-26).
   *
   * The board drew it as one rounded, shadowed box inside the page gutter, and
   * on a phone that reads as a CARD that happens to be stuck — a floating thing
   * the page scrolls behind. Edge to edge it reads as what it is: a band, the
   * continuation of the header, the way every other stuck strip on this site
   * behaves. So the box loses its radius and its side borders and takes the
   * page's gutter back out again.
   *
   * `--sf-gutter` is `.sf-container`'s own custom property (chrome.css) and the
   * rail is inside one, so this is a single declaration rather than a
   * breakpoint ladder repeating 15 / 32 / 40 — the same idiom `.sf-pdp-mstage`
   * uses, and the one chrome.css's gutter block asks every full-bleed element
   * to use. It is therefore correct at all three gutter rungs this cut spans,
   * and stays correct the next time the gutter is ruled.
   *
   * IT CANNOT CREATE HORIZONTAL OVERFLOW: a negative inline margin inside a
   * padded container gives back exactly what the padding took, so the band's
   * border box lands on the viewport's edges, never past them. `max-width` is
   * not in play below 920. The `overflow` gate asserts this at 18 widths.
   *
   * THE DROP SHADOW STAYS (ruled). It is now the only thing separating the
   * band from the page it floats over, which is the whole reason it was kept.
   */
  .sf-flow-rail {
    border: var(--border-hairline);
    /* Sides go; the two horizontal hairlines stay — they are the band's own
     * edges against the well stepper above and the page below. */
    border-inline: 0;
    border-radius: 0;
    box-shadow: 0 10px 26px rgb(23 24 26 / 12%);
    margin-inline: calc(var(--sf-gutter, 15px) * -1);
    order: -1;
    overflow: hidden;
    /* Flush under the PINNED TITLE (Ryan's ruling, 2026-08-27) — it was the
     * nav row's own offset until the title started pinning above it. The band
     * is still the header's continuation; the header simply has one more row in
     * it here. `--sf-band-stuck-h` is that row's measured height (see the title
     * block at the top of this sheet), so this offset moves with the band
     * rather than repeating its arithmetic. */
    top: calc(var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px));
    /* Over the step content it scrolls across, and over nothing else. */
    z-index: 2;
  }

  /*
   * AND ONCE IT REACHES THAT OFFSET IT LEAVES FLOW (Ryan's item 4, 2026-08-27:
   * "make it sticky in ALL states").
   *
   * `position: sticky` above is the resting state and the unscripted one; it
   * travels only inside `.sf-flow__grid`, which is exactly as tall as the STEP
   * CONTENT the state renders — 367px with no zip, 1044 at tier 1 — so the band
   * was released a third of the way down a short state's page and rode most of
   * a long one. That was the whole of the reported inconsistency (measured
   * 2026-08-27; `flow-pin.tsx` carries the numbers and why no containing block
   * on this page can fix it). Fixed to the viewport, every state is the same
   * state.
   *
   * THE PAGE DOES NOT MOVE THROUGH THE PIN. The band's row leaves the grid at
   * the instant the grid gives its height back as padding — the measured height
   * plus the row gap the vanished item was costing — so the content below is
   * where it was, the document is as tall as it was, and the reader sees the
   * switch only as the band no longer scrolling.
   */
  .sf-flow[data-pinned] .sf-flow-rail {
    left: 0;
    margin-inline: 0;
    position: fixed;
    right: 0;
  }

  .sf-flow[data-pinned] .sf-flow__grid {
    padding-top: calc(var(--sf-flow-rail-h, 0px) + var(--sf-flow-gap, 20px));
  }

  /* CONTENT KEEPS THE PAGE'S INSET (ruled): the band bleeds, what is written on
   * it lines up with everything else on the page. Same property, so the bar's
   * label starts on the same vertical as the step dots above it and the card
   * edges below it, at every gutter rung. */
  .sf-flow-rail__bar {
    display: flex;
    padding-inline: var(--sf-gutter, 15px);
  }

  /*
   * ═══ W202 ITEM 1: THE MOBILE ORDER IS THE DRAWER, AND ONLY THE DRAWER ═══
   *
   *   "The inline mobile dropdown expansion is retired."
   *
   * The body is not drawn at this rung and can no longer be opened — the
   * band’s control opens the cart drawer instead. What is left is the one
   * declaration that says so; the paper ground, the padding and the border
   * reset all dressed a panel that no longer appears.
   */
  .sf-flow-rail__body {
    display: none;
  }

  /*
   * ═══ W23’S FLOATING PANEL IS RETIRED WITH THE DROPDOWN (W202 item 1) ═══
   *
   * Ryan, 2026-09-02: "'My order' drop-down on mobile PUSHES content down; must
   * OVERLAY/float on top of the content." That ruling is not overturned — the
   * surface it governed is gone, and its successor is a <dialog> in the TOP
   * LAYER, which is the strongest form of "floats over the page and does not
   * push it" the platform has.
   *
   * THE RULES BELOW ARE DELETED rather than commented into place. What they
   * argued is kept here in prose, because the next person to hang something
   * off this band will need it:
   *
   * WHY IT PUSHED, and it pushed in both of the band's two states for two
   * different reasons — which is why one declaration fixes both:
   *
   *   AT REST the band is `position: sticky` and `order: -1` inside
   *     `.sf-flow__grid`. Sticky elements are IN FLOW, so an in-flow body simply
   *     made the grid's first row taller and everything under it moved down.
   *   ONCE PINNED the band is `position: fixed` and the grid gives its measured
   *     height back as padding — `flow-pin.tsx` watches the rail with a
   *     `ResizeObserver` precisely so "opening the order changes its height
   *     while it is pinned, and the grid's compensating padding has to follow it
   *     in the same frame". Correct, and it is exactly the push: the padding
   *     grew by the whole open order.
   *
   * TAKING THE BODY OUT OF FLOW ANSWERS BOTH. The rail's box is the BAR again,
   * at rest and pinned; `--sf-flow-rail-h` measures the bar and stops moving, so
   * the grid's padding stops moving with it; and the body hangs off the bar's
   * bottom edge and paints over whatever is under it.
   *
   * THE CONTAINING BLOCK IS THE RAIL, in both states and for free: `sticky` and
   * `fixed` are both positioned, so the rail is the containing block either way
   * and `top: 100%` is the bar's own bottom edge whichever one is in force.
   *
   * `position: relative` on the body is UNCHANGED IN PURPOSE — it hosts
   * absolutely-positioned descendants and it is an `overflow` container, and an
   * abs span whose ancestors are not positioned escapes to the document
   * (playbook §5). `absolute` establishes the same containing block, so the
   * property is satisfied by the new value rather than removed.
   *
   * IT KEEPS ITS OWN SCROLL AND ITS OWN CEILING: the open order can be taller
   * than a phone screen, and now that it no longer lengthens the page there is
   * nowhere else for it to go.
   */
  /*
   * ─── W7: THE QUANTITY ROLLS UNDER ITS LABEL, LIKE THE OPTION ROWS ─────────
   *
   * Ryan, 2026-09-01 (mobile walkthrough): "Options editor: Version/Adjustment
   * values wrap BELOW their labels but the Quantity stepper sits inline —
   * quantity rolls below its label like the others."
   *
   * THE TWO ROWS ALREADY SHARE THE CLASS THAT DOES IT. `.sf-flow-option__label`
   * is `width: 100%` and the quantity row's label IS one, so the only thing
   * separating the two treatments was that `.sf-flow-edit__qty-row` never
   * declared `flex-wrap` — a full-width item in a nowrap flex does not break the
   * line, it just becomes the greedy one and squeezes the controls beside it.
   * So this is ONE declaration rather than a second treatment.
   *
   * AND NO ROW-GAP, WHICH IS THE MEASURED ANSWER RATHER THAN THE OBVIOUS ONE.
   * `.sf-flow-option__choices` declares `row-gap: 6px` and then a second rule
   * for the same selector declares `gap: 8px`, which resets both axes — so the
   * option rows' real label-to-chips gap is 8, not the 6 their comment claims.
   * This row already carries `gap: 8px`, so it inherits the same 8 by having
   * nothing said about it. Verified: label→control tops are 24px apart on both
   * (16px label + 8px gap). Declaring 6 here made this row 22 and the two rows
   * disagree by 2px. (The stale `row-gap: 6px` above is inert and is FLAGGED
   * rather than deleted — it is not this pass's block.)
   *
   * SCOPED TO THE COLLAPSED-RAIL RUNG, and the playbook's §5 warning is why:
   * "a 'harmless' flex-wrap fix is NOT harmless on a row with zero slack."
   * MEASURED at the 300px rail (920–1239.98), this row HAD exactly zero — 109.8
   * label + 8 + 81.27 stepper + 8 + 46.94 Remove = 254.01 into 254 — so it was
   * the literal case that rule was written for, and it is left alone.
   *
   * W171 CHANGED THAT ARITHMETIC AND THE SCOPING STAYS ANYWAY. The Remove left
   * this row (the system's was a stub and is retired; a per-line Remove sits at
   * the bottom of its own block now), so the measured content is 109.8 + 8 +
   * 81.27 = 199.07 into 254 and the row has ~55px of slack it did not have. The
   * declaration is NOT lifted out of the media query on the strength of that:
   * what it would change above the cut is a wrap, and the wrap is a LAYOUT
   * question Ryan already has an open call on (the flag below). Slack is the
   * reason the fix was dangerous, not the reason it was scoped. Above the cut
   * the rail's edit state is byte-identical, which `flowgate`'s `editoptions`
   * region proves.
   *
   * FLAGGED, because the report's premise is not quite the layout's: the option
   * rows wrap at EVERY width, not only on the phone (measured — the label is
   * `width: 100%` unconditionally, and has been since the price moved into the
   * chips). So above 920 the quantity row still reads differently from the two
   * under it. Lifting this pair of declarations out of the media query is the
   * one-line change that closes that, and it costs no recorded pixel — the
   * `editoptions` crop is `.sf-flow-options`, which the quantity row is not
   * inside. Ryan's call, not this pass's.
   */
  .sf-flow-edit__qty-row {
    flex-wrap: wrap;
  }

  /*
   * ═══ THE TOUCH FLOOR, ON THE RUNG WHERE THERE IS A THUMB (GUI review F9) ═══
   *
   * The stepper's halves measured 30.58 x 26 and its frame 28px tall — which is
   * BELOW BOTH of BTN-UNIFY's rungs and well below any touch floor. That was
   * survivable while there was exactly one stepper on the panel, reachable by a
   * pointer on a desktop rail; W171 put one on every line with a quantity, and
   * on a phone that is three pairs of 26px targets stacked inside a scrolling
   * tray.
   *
   * 44px, AND ONLY HERE. The steppers are in `btngate`'s ruled STEPPER
   * exclusion ("the +/- pair is one drawn field") so they answer to no rung by
   * design, and their 28px frame is what the desktop edit state has always
   * drawn — including in `flowgate`'s recorded `editoptions` crop. Growing them
   * everywhere would move a locked number for a problem that only exists where
   * fingers do. Scoped to the collapsed rung, the recorded desktop crops are
   * untouched and the phone gets a real target.
   *
   * BOTH KINDS, ONE FLOOR: the hoop's `<button>` halves and an accessory's
   * `<a>` halves are one control to a reader (measured identical), so a floor
   * applied to one of them would break exactly the parity W171 established.
   * `inline-flex` centres the glyph in the taller box; without it the `+` sits
   * at the top of 44px of padding.
   */
  /*
   * BOTH SELECTORS, AND THE SECOND ONE IS NOT REDUNDANT — it is a specificity
   * bug caught on a phone screenshot. The base sheet declares
   * `a.sf-flow-qty__step { display: inline-block }` to make an anchor draw the
   * same box a button does; that selector is (0,1,1) and this rule is (0,1,0),
   * so a bare `.sf-flow-qty__step` here loses to it. The BUTTON halves became
   * 44px flex boxes with their glyph centred and the LINK halves stayed
   * inline-block — which put the accessory stepper's `+` at the TOP of a 44px
   * box while the hoop's sat centred, side by side on the same panel. Exactly
   * the parity W171 built and this rung would have quietly broken.
   */
  .sf-flow-qty__step,
  a.sf-flow-qty__step {
    align-items: center;
    display: inline-flex;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
  }

  /*
   * AND THE CONTROL ROWS CARRY THE FLOOR (W179). The Remove used to have a row
   * of its own with a 44px minimum; it lives in these rows now, so the minimum
   * comes with it. Both row kinds get it — the quantity row and the install's
   * coverage row — because both hold a Remove and one panel cannot have two
   * touch floors.
   */
  .sf-flow-edit__qty-row,
  .sf-flow-edit__fact-row {
    min-height: 44px;
  }

  /*
   * THE PIN'S READBACK CARRIER, and the one place the rung is stated to the
   * driver (`flow-pin.tsx`). `scroll-margin-top` is chosen for the reason
   * chrome.css's `.sf-title-band__sentinel` block gives: an unregistered custom
   * property computes to its token stream, so `getComputedStyle` hands back the
   * literal `calc(...)`, while the same expression on a REAL LENGTH comes back
   * resolved in pixels — which is the only way a script can learn what this
   * cascade added up to. It mirrors the band's own offset above rather than
   * declaring a second one.
   *
   * NOTHING EVER SCROLLS TO IT: zero-height, `aria-hidden`, no id. Outside the
   * cut it is not declared at all, and a zero reading is what tells the driver
   * this width has no collapsed band to pin.
   */
  .sf-flow__pin {
    scroll-margin-top: calc(var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px));
  }
}

@media (max-width: 767.98px) {
  /* Both two-up rows go single-column at the site's mobile cut: the board draws
   * them at 1440 only, and at 390 two 170px cards of five-line bullets is not a
   * drawing anyone made. */
  .sf-flow-pair,
  .sf-flow-crew {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
   * W4, THE HALF THAT IS ONLY TRUE ON A PHONE.
   *
   * THE FIGURE: 30 -> 21. The board drew 30 at 1440, inside a 540px card with
   * air on both sides of it, and boards win at the width they were drawn — so
   * the desktop keeps it. At 360 the same 30px sits beside a 14.5 label with
   * nothing between them and reads, in Ryan's word, humongous. 21 is the
   * platform's `--text-product`, it is still by some distance the loudest thing
   * in the card, and against the 14.5 label it is a 1.45:1 step instead of 2:1.
   *
   * THE BUTTON: W1's ruling — "the CHECKOUT button size is the keeper". This
   * note originally said the metrics were the Checkout button's, COPIED, because
   * the audit had not landed yet and the rule available was "do not invent a
   * fifth size". The audit has landed and the rule below reads its rung by name
   * instead, which is strictly better; the WEIGHT went to 600 with it, which
   * overrules the sentence that used to end this paragraph ("the 700 weight
   * stays"). See that rule's own note for why weight belongs to the rung and the
   * accent fill carries the emphasis on its own.
   */
  .sf-flow-crew__price-figure {
    font-size: var(--text-product);
  }

  /*
   * READS THE COMPACT RUNG rather than restating it (button-audit lane,
   * 2026-09-01 — this hunk's own note asked for exactly that and predates the
   * tokens existing). The three numbers it had were 40 / 14.5 / 700, measured
   * off the Checkout button; the first two ARE the rung and now say so.
   *
   * THE WEIGHT COMES DOWN TO 600 WITH THEM, which is the one place this lane
   * overrules the note above. Weight is pinned to the RUNG, not to the
   * treatment: 14.5/700 beside 14.5/600 at the same height is precisely the
   * disagreement Ryan counted when he said "five font sizes, font weights", and
   * the accessory row already had it (an accent "Add to order" at 700 against a
   * ghost "Full details" at 600). What still says "this is the primary" is the
   * ACCENT FILL, which is untouched — the fill is the treatment and it carries
   * the emphasis on its own. FLAGGED FOR RYAN as a judgment call.
   */
  .sf-flow-continue--incard .sf-flow-btn {
    font-size: var(--button-text-sm, 14.5px);
    font-weight: var(--button-weight-sm, 600);
    min-height: var(--control-height-sm, 40px);
    padding: 0 22px;
  }


  /*
   * THE PHONE STEPPER, ONE LINE (Ryan's ruling, 2026-08-26). The row used to
   * WRAP here — three labelled steps and two 56px connectors do not fit 390 —
   * and the ruling refused the obvious fix of shrinking the type. So the row
   * sheds what it can spare instead: only the CURRENT step keeps its label, the
   * others are their numbered dots, and the connectors come down to 12px.
   *
   * MEASURED at 390 (the narrowest phone the site draws for): three 28px dots
   * (84) + the current label at its full 14.5 (~120) + its 10px gap + two
   * connectors at 12 + 8 + 6 + the item's own 10 (72) = ~286 into the 362 the
   * mobile gutter leaves. At 360 it is 286 into 332. The font is the desktop
   * font, untouched, which was the point.
   *
   * NOT `display: none` on the labels that leave: they become `.sf-flow-sr`, so
   * the sequence still reads as three NAMED steps to a screen reader. Hiding
   * them outright would make the phone's indicator say less than the page does.
   */
  .sf-flow-steps__list {
    flex-wrap: nowrap;
    /* The list takes the row so the connectors can spend what the shed labels
     * gave back — ~50px at 390 — instead of leaving the three dots huddled at
     * the left edge. */
    flex: 1;
  }

  .sf-flow-steps__label:not(.sf-flow-steps__label--current) {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }

  /* The item is the positioned ancestor those hidden labels need, or they
   * extend the DOCUMENT's scrollable overflow (playbook §5). */
  .sf-flow-steps__item {
    position: relative;
  }

  /* The current label must not be the thing that gives: it is the only word on
   * the row, and truncating it would be the crushed-font failure by another
   * route. The dots and the rules are already fixed-width, so nothing else can. */
  .sf-flow-steps__label--current {
    white-space: nowrap;
  }

  /* ELASTIC, with the measured 12px as its floor: at 430 the row has slack and
   * the connectors take it; at 360 they hold at 12 and the row still fits. */
  .sf-flow-steps__rule {
    flex: 1 1 12px;
    margin: 0 6px 0 8px;
    min-width: 12px;
    width: auto;
  }

  .sf-flow-zip {
    padding: 28px 18px;
  }

  .sf-flow-zip__form {
    flex-wrap: wrap;
    justify-content: center;
  }

  /* The stock card is a column at every width now (the delivery strip's own
   * line), so only the service-centre card has to be turned here. */
  .sf-flow-center {
    align-items: flex-start;
    flex-direction: column;
    gap: 14px;
  }
}

/* ------------------------------------------ step 2: the phone rung (≤767.98) -- */

/*
 * NO BOARD DRAWS STEP 2 ON A PHONE, so this rung is DERIVED — and it is derived
 * by measurement rather than taste. At 390 the page's gutter leaves 360px of
 * content; the board's card is a 220px media column plus a 20px gap, which
 * leaves the description 120px. That is not a narrower card, it is a broken
 * one. So the card stacks — media over body, the media taking the full measure
 * — which is the same shed the product page's own two-column blocks make at
 * this cut, and 767.98 is the site's mobile cut so nothing changes shape twice.
 *
 * THE STAGE KEEPS ITS 168px. It is a fixed height on the board because the
 * placeholder has no aspect ratio to preserve; when the photography lands, this
 * is the one declaration that becomes an `aspect-ratio`.
 */
@media (max-width: 767.98px) {
  /* The head's Continue drops under the filters rather than squeezing beside
   * them: four 48px chips and a 48px primary do not share 360px. It keeps its
   * right alignment, which is the platform rule and reads correctly stacked.
   *
   * W88 CHANGED WHAT IS ABOVE IT, not this rule: the column used to hold a
   * heading and a sentence, and now it holds the chip strip. The chips wrap
   * among themselves inside their own row (`flex-wrap` on `__facets`), so the
   * stack is two items at every phone width. */
  .sf-flow-step2__head {
    align-items: stretch;
    flex-direction: column;
    gap: 14px;
  }

  /* AND THE CHIPS TAKE THE MEASURE at this rung. Stacked above a full-width
   * Continue, four intrinsic-width chips would leave a ragged right edge beside
   * a button that has one; `flex: 1` with a floor lets them share the row and
   * wrap in pairs rather than at whatever width their words happen to be. */
  .sf-flow-facet {
    flex: 1 1 auto;
    min-width: 44%;
  }

  .sf-flow-acc__card {
    flex-direction: column;
    gap: 14px;
  }

  .sf-flow-acc__media {
    width: 100%;
  }

  /*
   * W107 IS A DESKTOP RULING, and the phone keeps its row. Ryan's words are
   * "Desktop: wasted space under the left photo", and the space he is describing
   * cannot exist here: below this cut the card is a COLUMN, so the media is full
   * measure with the body under it and nothing is left over beside anything.
   * Two rows of half-measure tiles would be ~185px squares — a strip taller than
   * the stage above it, on the rung with the least room.
   *
   * SO: back to one row, at the board's own 40px. What the phone does inherit
   * from W107 is the slot count — four tiles rather than five, because
   * `ACCESSORY_THUMB_SLOTS` is one number for both rungs and the "+N" signpost
   * absorbs the difference.
   */
  .sf-flow-acc__thumbs {
    flex: none;
    grid-auto-rows: 40px;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    max-height: none;
    min-height: 0;
  }

  /* The simulator's two panels stack, and the pad photo takes the measure. */
  .sf-flow-acc__sim {
    grid-template-columns: minmax(0, 1fr);
  }

  /* W186 AT THE PHONE RUNG: the pad is the full measure here, so the bleed is
   * the other three edges — the right one included, and the bottom one NOT,
   * because the field now sits under the photograph rather than beside it. */
  .sf-flow-acc__sim-pad {
    margin: calc(-1 * var(--sf-flow-acc-sim-pad, 14px))
      calc(-1 * var(--sf-flow-acc-sim-pad, 14px)) 0;
  }

  /*
   * W185 AT THE PHONE RUNG: the film takes the whole measure and its control
   * goes away with the thing it controlled. A 40% float in a single-column card
   * would be a 130px video beside two words of wrapped text, and an Expand
   * button on a box that is already full width is a control that does nothing —
   * the same reasoning that leaves the Lightning Adjust players without one.
   */
  .sf-flow-acc__film-wrap,
  .sf-flow-acc__film-wrap:has(> .sf-xp[data-big="true"]) {
    float: none;
    margin: 0 0 12px;
    width: 100%;
  }

  .sf-flow-acc__film .sf-xp__controls {
    display: none;
  }

  .sf-flow-acc__sim-input {
    max-width: none;
  }

  /*
   * THE CONTROL ROW WRAPS, and the actions keep the right edge. Three 40px
   * controls plus their gaps measure past 360 with a wide primary in them, and
   * a row that overflows is the defect the overflow gate exists to catch —
   * `margin-left: auto` still pushes the actions right on the second line.
   */
  .sf-flow-acc__controls {
    flex-wrap: wrap;
  }

  /*
   * AND IT ALWAYS WRAPS, rather than wrapping when it happens not to fit. Left
   * to the flex rule alone, an ADDED card (whose action says "Remove") fits one
   * line at 390 and an unadded one (whose action says "Add to order") does not
   * — so a list of eight cards would draw two different control rows depending
   * on what the shopper had already done to each. A declared basis makes every
   * card the same shape at this rung; `margin-left: auto` still keeps the
   * actions on the right edge.
   */
  .sf-flow-acc__actions {
    flex-basis: 100%;
  }
}
