/*
 * Product-page stylesheet — the above-the-fold PDP.
 *
 * Served as a static asset and pulled in by `<PdpStyles>` 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/Main.dc.html (desktop, drawn at 1440) and
 * design/produnk/MobileA.dc.html (drawn at 390), mechanically tokenised:
 * colours and typefaces read `--brand-*`, sizes and radii read the platform
 * constants in globals.css. Board values ship verbatim — the odd numbers (46px
 * display, 82px option label, 5.5px paddings elsewhere on the site) are
 * measured, not sloppy (playbook §3).
 *
 * Class prefix `sf-pdp-`, which the partition in `tests/pdp-page.test.ts` sweeps
 * against this page's markup. NOTE FOR THE NEXT CATEGORY-TEST EDIT: chrome.css's
 * own sweep in `tests/category-page.test.ts` claims every `sf-` name except
 * `sf-cat-` and `sf-home-`, so `sf-pdp-` has to join that exclusion or the chrome
 * sweep starts claiming names it has no markup for. This pass deliberately does
 * not edit that file (another agent owns it) — the exclusion is FLAGGED, not
 * applied.
 *
 * NO SECTION HEAD HERE. The category and home sheets each carry a copy of the
 * platform's tinted-circle section header, and a fourth sheet was supposed to
 * trigger the move to a shared one. This board draws none: the PDP's cards use
 * an inline mark + bold title at 14.5px, which is the CARD header pattern, not
 * the section one. The shared-sheet move is still owed — it is just not owed by
 * this pass.
 *
 * 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-pdp` resets it once, the same way
 * `.sf-cat-hero` does.
 *
 * THE BOARDS ARE ALSO CONTENT-BOX and this site is border-box, so every place
 * the drawing carries an EXPLICIT width or height ALONGSIDE padding or a border
 * — both zip fields, the three fixed-width buttons, the quantity readout —
 * ships the border-box equivalent with its arithmetic written beside it, not
 * the board's authored number: THE BOARD'S NUMBER IS ITS CONTENT BOX, and the
 * padding and border it draws around that number are ours to add back in.
 * Copying the authored numbers instead read 0.781% (both zip rows) and 7.734%
 * (the spec card) against the board. Each site carries its own arithmetic;
 * `.sf-pdp-zip` is the worked example.
 *
 * The selected THUMBNAIL used to be on that list and no longer is — Ryan ruled
 * its board size difference an authoring artifact rather than a decision
 * (2026-08-26); see the note on `.sf-pdp-tile--selected`.
 */

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

/*
 * EVERY INTERACTIVE CONTROL IN THIS SHEET, in one block (Ryan's ruling,
 * 2026-08-26 — the platform pattern, its three grounds and the contrast
 * arithmetic are documented once, at the top of chrome.css, and in
 * docs/storefront-tokens-and-patterns.md §3).
 *
 * THE PRODUCT PAGE NEVER GOT THE RULING. It was built the same day the ring
 * was ruled and the ruling reached its four siblings and not this sheet: every
 * control below was on the UA's default `outline: auto 1px`, and the three
 * `:focus-visible` rules this sheet already carried were scattered beside their
 * components — the exact organisation the ruling's third rule forbids. They are
 * MOVED here rather than left in place, and two of them changed:
 *
 *   - `.sf-pdp-buybar__cta` drew `logo-orange-bright` #FF7529. That token's
 *     documented duty (tokens §1) is the logo dot and the active-nav underline
 *     ONLY, and one orange is the site's rule — the same swap chrome.css made
 *     for the header and footer when the ruling landed there.
 *   - the eyebrow links and the landing CTA drew their ring at `outline-offset:
 *     3px`, an authored number from before the token existed. They take
 *     `--sf-focus-ring-offset` like everything else, so the whole page's rings
 *     stand off their controls by the same amount.
 *
 * It is written here rather than beside each component on purpose: the ruling is
 * that the ring is a SITE-WIDE rule, and a rule scattered across sixteen
 * component blocks is sixteen places for the next control to be missed. New
 * control in this sheet: add its class to the group whose ground it lands on.
 *
 * The `var()` fallbacks repeat the platform values exactly once each, so this
 * sheet still draws a compliant ring if it is ever loaded without chrome.css —
 * the `--sf-pinned-offset` discipline this sheet already follows for the buy
 * bar's top rung.
 *
 * WHAT IS NOT HERE, and it is not an omission. The gallery's thumbnail tiles,
 * the phone stage's dots and its three view pills are drawn controls that are
 * not controls yet: they render as `<li>` and `<span>` (see `pdp-gallery.tsx`),
 * so they are not tab stops and have no ring to draw. The day the viewer lands
 * and they become buttons, they join a group below — the tiles the MEDIA one,
 * since they sit on photography.
 */

/*
 * --- on the light grounds: paper and surface ---
 *
 * Measured 2026-08-26 (chrome.css carries the table): accent #C4530A is 4.42:1
 * on paper #FBFBFA and 4.58:1 on surface #FFFFFF, both clear of the 3:1 bar.
 * Which of the two a given ring lands on is the control's own container — the
 * rail's eyebrow link and the landing CTA stand on the page's paper, everything
 * else stands inside a white card — and both are recorded, so no reading here
 * is new arithmetic.
 *
 * THE BUY BAR IS ON THIS LIST, not on a dark one. Its ground is `surface`
 * #FFFFFF at both edges it pins to (`.sf-pdp-buybar` background, phone bottom
 * and desktop top), so its accent-filled CTA takes the standard ring: at the
 * 2px offset the ring never touches the button's own accent fill — the gap it
 * stands off in is the bar's white — so the 4.58:1 reading is the one that
 * governs. The bar being a floating pinned surface changes where it is, not
 * what colour is under the ring.
 *
 * The two zip fields ARE the drawn field — `paper` fill, `line-mid` border,
 * their own width (see the border-box arithmetic on `.sf-pdp-zip`) — not a bare
 * input inside one, so their ring goes on the input itself and the
 * wrapper-`:has()` rule does not apply to them.
 */
.sf-pdp-landing-cta__button:focus-visible,
.sf-pdp-rail__eyebrow-link:focus-visible,
.sf-pdp-mrail__eyebrow-link:focus-visible,
.sf-pdp-quiet:focus-visible,
.sf-pdp-card__caption--specifying a:focus-visible,
/* W184: the specifying link is a `<button>` now — it opens the specs pop-up
 * rather than navigating — and rings exactly as the anchor it replaced did. */
.sf-pdp-card__caption--specifying button:focus-visible,
.sf-pdp-chip:focus-visible,
.sf-pdp-mchip:focus-visible,
/* `.sf-pdp-item__toggle` AND `.sf-pdp-mitem__toggle` LEFT THIS ROSTER (Ryan's
 * MP1, 2026-08-29) with the Add/Remove buttons they ringed, and the two chips
 * that replaced them never needed an entry — a chip's ring is declared with the
 * chips two entries above. THE CONTROL IS A CHECKBOX NOW (WP6, same day), and
 * its ring is not here either: a transparent input on a drawn box delegates to
 * the box, in the `:has()` group below. */
.sf-pdp-zip:focus-visible,
.sf-pdp-mzip:focus-visible,
.sf-pdp-ghost:focus-visible,
.sf-pdp-mghost:focus-visible,
.sf-pdp-buy__cta:focus-visible,
.sf-pdp-mbuy__cta:focus-visible,
.sf-pdp-sec__ghost:focus-visible,
.sf-pdp-nav__item:focus-visible,
.sf-pdp-rev__chip:focus-visible,
/* `.sf-pdp-rev__toggle-option` LEFT THIS ROSTER (Ryan's WP9, 2026-08-29) with
 * the chip pair it ringed: the Photos control is the shared segmented toggle
 * now, and `.sf-seg__option`'s ring is declared beside the pattern in
 * sections.css, exactly as the pager's and the configuration button's are. */
/* `.sf-pdp-rev__bar` LEFT THIS ROSTER (ruling 14, 2026-08-27) with the press it
 * used to take: the histogram rows are `<div>`s now, never focused, and a ring
 * declaration for an element that can never take focus is a rule nobody can
 * ever see fire. Two controls JOINED it in the same pass — a review's photo
 * thumbnails and the panel's "See all photos", both real openers. */
.sf-pdp-rev__item-photo:focus-visible,
/* The phone strip's tiles (ruling R4) — the same opener as `__item-photo`, at
 * the rung where the panel is not drawn. Same ring, same reason. */
.sf-pdp-rev__mphoto:focus-visible,
.sf-pdp-rev__photos-all:focus-visible,
.sf-pdp-rev__pin:focus-visible,
/* `.sf-pdp-rev__clear` LEFT THIS ROSTER (Ryan’s DD-C, 2026-08-28) with the
 * link it ringed: the affordance is the X inside the search field now, whose
 * ring is drawn INSIDE the field, in chrome.css beside the button. */
.sf-pdp-rev__vote:focus-visible,
/* `.sf-pdp-rev__page` and `.sf-pdp-ans__page` LEFT THIS ROSTER (ruling PP5,
 * 2026-08-28) with the pagers themselves: one pager means one ring, declared
 * beside the pattern in `sections.css`. */
/* `.sf-pdp-ans__pin`, `__clear` and `__reply-input` LEFT THIS ROSTER with the
 * controls themselves (rulings A2 and A3): the Featured pill and its Clear
 * button were replaced by the Filter-by dropdown, and the panel's reply field
 * went with the panel. `__sort` went to `__filter`'s dropdown, whose ring is
 * declared in chrome.css beside the control. */
/* `.sf-pdp-ans__config` LEFT THIS ROSTER (DD-A) with the button: it is
 * `.sf-sec-config__button` now, ringed in sections.css beside the pattern. */
/* `.sf-pdp-ans__continue` LEFT THIS ROSTER (the shared-embed-core extraction,
 * 2026-08-28) with the button: the answer box's hand-off is
 * `.sf-embed__continue` now, ringed in sections.css beside the configuration
 * button, together with the quiet link AI5 put on the same line. */
.sf-pdp-ans__vote:focus-visible,
.sf-pdp-ans__links a:focus-visible,
.sf-pdp-buybar__cta:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * --- drawn inside: the quantity stepper's two buttons ---
 *
 * `.sf-pdp-qty` is a drawn field — surface fill, `line-mid` border, control
 * radius — and its minus and plus are BARE buttons (no background, no border)
 * sitting flush against it on three sides each. An outward ring would be drawn
 * half underneath that border and half outside the field, so theirs is drawn
 * inside, the same call `.sf-quiet-select__item` makes in chrome.css.
 *
 * NOT the wrapper-`:has()` rule, deliberately. That rule is for a field holding
 * ONE bare input, where the field is the box the reader is looking at; this
 * field holds TWO controls, and a ring on the field could not say which of them
 * has focus. Each keeps its own. The ring's adjacent colour inside the field is
 * `surface` #FFFFFF — 4.58:1, the reading above.
 */
/*
 * --- the ring on the FIELD, not on the input inside it (below the fold) ---
 *
 * tokens §3's ONE blessed case for suppressing the UA outline: a bare input
 * inside a DRAWN field, where the box the reader is looking at is the field. The
 * two search fields and the "With photos" checkbox are all that shape — the
 * checkbox's real `<input>` is transparent and 16px square, sitting exactly on
 * the drawn box, so the ring has to go on the label that holds both.
 *
 * The reviews field also draws a 3px accent-soft HALO when it is filtered. That
 * is a `box-shadow` and this is an `outline`, so the two compose rather than
 * replace each other — a custom treatment may exist alongside the ring and never
 * instead of it (Ryan's ruling, 2026-08-26).
 */
/* `.sf-pdp-rev__check:has(…)` LEFT THIS RULE (Ryan's MP3, 2026-08-29) with the
 * checkbox it ringed. The Photos filter is two ordinary buttons now, so it
 * takes the ORDINARY outward ring beside the section's other chips — the
 * `:has()` indirection existed only because a checkbox's real input is
 * invisible and the drawn box is not a control. */
/* THE DRILL CHECKBOX'S DRAWN BOX (Ryan's WP6, 2026-08-29) — the same blessed
 * shape at the other end: a transparent `<input>` clipped exactly onto a drawn
 * 20/18px square, so the ring belongs to the square the reader is looking at.
 * It is the shape MP3's "With photos" checkbox vacated three months of rulings
 * ago in this same sheet, arriving back on a control that really is a
 * checkbox. Both compositions, one declaration. */
.sf-pdp-item__box-mark:has(> .sf-pdp-item__box:focus-visible),
.sf-pdp-mitem__box-mark:has(> .sf-pdp-mitem__box:focus-visible),
.sf-pdp-ans__ask:has(.sf-pdp-ans__field-input:focus-visible),
.sf-pdp-rev__field:has(.sf-pdp-rev__field-input:focus-visible) {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/* THE PHONE STEPPER'S BUTTONS TOOK THE SAME READING (2026-08-27): it is the
 * same drawn field holding the same two bare buttons, at the phone's touch
 * sizes. One selector list, one decision. */
.sf-pdp-qty__step:focus-visible,
.sf-pdp-mqty__step:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-inset, -2px);
}

/*
 * --- on the photograph: the stage's carousel arrows ---
 *
 * `.sf-pdp-stage__nav` is the pair of discs the stage fades in on hover, and
 * they are absolutely positioned ON `.sf-pdp-gallery__stage` — which is now a
 * real photograph rather than the sketched well it was when this block was
 * written for the spin pill it replaces. A ring at a 2px offset is therefore
 * drawn on the PHOTOGRAPH, which has no known adjacent colour, so it takes the
 * ink-ring-plus-white-halo pair (chrome.css, MEDIA) rather than the accent:
 * accent covers only grounds lighter than L=0.638 or darker than L=0.027, and a
 * product shot can be anywhere in between.
 *
 * THE FOCUS RING IS ALSO WHAT REVEALS THEM. They rest at `opacity: 0` and the
 * stage's `:focus-within` brings them up, so a keyboard reaching one both sees
 * it and gets the ring — a control that took focus while still transparent
 * would be a ring drawn around nothing.
 *
 * The disc draws no shadow at rest, so unlike the category hero's floaties the
 * halo composes with nothing and is declared alone.
 */
.sf-pdp-stage__nav:focus-visible {
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff));
}

/*
 * --- on the photograph: the gallery's OPENERS ---
 *
 * The desktop stage and every thumbnail now open the shared media viewer, and
 * each opener is a transparent button covering the frame it opens. Same ground,
 * same reasoning, therefore the same MEDIA pair as the spin pill above: these
 * sit ON the photograph, not beside it.
 *
 * THE RING IS DRAWN OUTSIDE both of them, at the platform's 2px offset. There is
 * room: the stage's ring lands on the page's paper, and a thumbnail's lands in
 * the 12px flex gap between tiles, so neither can reach a neighbour.
 */
.sf-pdp-gallery__open:focus-visible,
.sf-pdp-tile__open:focus-visible {
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff));
}

/*
 * --- on the photograph, with no room outside it: the PHONE stage's opener ---
 *
 * `.sf-pdp-mstage` is FULL-BLEED — it takes the page's gutter back out with a
 * negative margin — so its box IS the viewport's width and a ring at a positive
 * offset is drawn off the left and right edges of the screen. That is exactly
 * the case `--sf-focus-ring-inset` is declared for (chrome.css: "a control whose
 * ring would otherwise land off the viewport").
 *
 * THE HALO GOES INWARD WITH IT. The MEDIA pair only works as a pair — an ink
 * ring for light grounds, a white halo for dark ones, overlapping ranges — so a
 * ring moved inside and a halo left outside would be half a treatment. The
 * shadow is `inset` with the same 4px spread and the same surface white, and the
 * 2px outline is painted over its outer half, which is the same reading the
 * outward pair gives, mirrored.
 */
.sf-pdp-mstage__open:focus-visible {
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-inset, -2px);
  box-shadow: inset 0 0 0 4px var(--brand-color-surface, #ffffff);
}

/*
 * --- on the photograph, WITH room outside it: the phone stage's caption control
 *
 * The circle that reveals the frame's caption (Ryan's G2 v2, 2026-08-27) sits
 * ON the photograph like the stage's own arrows do, so it takes the same MEDIA
 * pair — an ink ring for light grounds, a white halo for dark ones, and neither
 * assumption made about a product shot.
 *
 * OUTWARD, unlike the full-bleed opener above it. The opener's ring is inset
 * because the opener IS the viewport's width and a positive offset is drawn off
 * the screen; this control is a 36px disc inset 12px from the stage's left
 * edge, so a 2px ring at a 2px offset lands 8px inside the viewport with room
 * to spare. It draws no shadow at rest, so the halo composes with nothing.
 */
.sf-pdp-mstage__cap:focus-visible {
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff));
}

.sf-pdp {
  line-height: normal;

  /*
   * THE STAGE'S GROUND — one recipe, both compositions (Ryan, batch #4 G1,
   * 2026-08-27: "give the stage backdrop character — a slight gradient …
   * subtle, not a vignette cliché").
   *
   * IT IS BOUNDED BY THE PAGE, and that bound is the whole design. The column
   * behind the stage is `paper` #FBFBFA; a sweep that reaches `surface`
   * #FFFFFF at its top edge would be LIGHTER than the page there, and the
   * stage's own box — which has no border and only a 4px radius — would
   * dissolve into the column at exactly the edge a reader uses to read it as a
   * box. So every stop stays at or below the flat `well` this replaced:
   *
   *   0%    `well` lifted 45% toward `surface` ≈ #F7F6F4 — six levels under
   *         paper, so the top edge still reads.
   *   56%   `well` #F1F0EC itself, the tone the stage has always been. More
   *         than half the box is unchanged, which is what makes this a wash
   *         rather than a restyling.
   *   100%  `line` #E8E7E3 — the foot, nine levels under `well`, so the
   *         product stands on something instead of floating in a flat field.
   *
   * FIFTEEN LEVELS END TO END. That is deliberately below the pixel gates'
   * acceptance threshold of 24 and deliberately above nothing at all: playbook
   * §4's subtle-tint lesson says a wash this quiet is INVISIBLE to threshold 24
   * and must be gated by computed style instead, which `tools/pdpstagegate.mjs`
   * now does by name.
   *
   * 170deg RATHER THAN 180. A dead-vertical wash on a 632x640 box reads as a
   * seam; ten degrees of tilt puts the light corner top-left and the shade
   * bottom-right, which is where a studio sweep puts them. It is NOT a radial
   * darkening of all four edges — the vignette cliché Ryan named — because a
   * vignette frames a photograph, and this stage's problem was already too much
   * frame.
   */
  --sf-pdp-stage-ground: linear-gradient(
    170deg,
    color-mix(in srgb, var(--brand-color-surface, #ffffff) 45%, var(--brand-color-well, #f1f0ec)) 0%,
    var(--brand-color-well, #f1f0ec) 56%,
    var(--brand-color-line, #e8e7e3) 100%
  );

  /*
   * THE CAPTION BAND'S HEIGHT, and the reason it is a token rather than a
   * number written four times (Ryan's ruling G2 v2, 2026-08-27: the caption
   * moves INSIDE the photo section's drawn box).
   *
   * FOUR RULES READ IT and every one of them would be wrong on its own if the
   * band's floor were re-ruled: the stage's own height grows by it, the track
   * that carries the photographs is clipped short by it, the counter is pushed
   * up off the box's foot by it, and the arrows re-centre on the photo area
   * inside it. That is the whole point of the token — the band is a band, and
   * everything else on the stage is measured from the PHOTO AREA, which is the
   * box less this.
   *
   * ─── THE NUMBER, DERIVED FROM THE TYPE IN THE BOX ───────────────────────
   *
   *   36px  TWO 18px LINE BOXES, the floor the outside bar reserved and the
   *         reason it existed: 53 of the gallery's 93 frames are untitled and
   *         the longest sales point wraps to two lines at the narrow end of the
   *         desktop composition, so a band sized to its CONTENT would move the
   *         four rows of thumbnails under it on every swipe. 18 is an INTEGER
   *         line box on purpose — 12.5px of mono at a 1.5 ratio is 18.75, and a
   *         fractional band puts the strip below it on a half-pixel, where
   *         every glyph in four rows of tiles rasterises against a different
   *         subpixel phase than the board's.
   *   +14   the lead-in between the photograph and the words, which is the
   *         outside bar's own measured gap, kept.
   *   +14   AND THE SAME AGAIN UNDER THEM, which is what is new. The old bar
   *         ended at the column's flow and had nothing below it to clear; this
   *         one ends at a drawn rounded edge, and text butted against that edge
   *         reads as an overflow rather than a caption.
   *
   * 64px. The column is therefore 14px taller than it was — the whole of the
   * new bottom pad, nothing else — and `tools/pdpgate.mjs` carries that as
   * `CAPTION_BAR_DELTA`, re-derived from 50 with this ruling's attribution.
   */
  --sf-pdp-stage-band: 64px;
}

/*
 * THE BOARD'S TWO-COLUMN GRID.
 *
 * The board draws `repeat(2, minmax(0, 1fr))` across the full 1440 and then puts
 * the page's 64px gutter INSIDE the outer padding of each column (left column
 * `48px 24px 48px 64px`, right column `72px 64px 48px 40px`). Here `.sf-container`
 * owns that 64 — it is the same measure the header and footer wrapped around this
 * page use, and duplicating it in the columns would double it — so each column
 * keeps only its INNER padding. At 1440 the two are the same page to the pixel:
 * 1440 − 128 = 1312 of measure, two 656 tracks, a 632px gallery column and a
 * 616px rail, exactly as the board resolves.
 */
.sf-pdp__grid {
  display: grid;
  /* `minmax(0, …)`, not a bare `fr`: an `fr` track's automatic minimum is its
   * MIN-CONTENT, and both columns hold nowrap furniture (the 250px spec buttons,
   * the option chip rows). A bare `1fr` is floored by them and the grid stops
   * being an even split well before the mobile cut. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* The phone composition, hidden until the mobile cut. */
.sf-pdp__mobile {
  display: none;
}

/* Quiet link: muted and underlined, the same size as the text around it, never
 * coloured (tokens §1). It is the only link treatment on this page except the
 * spec-market card's one accent link, which the board draws in the default
 * anchor colour. */
.sf-pdp-quiet {
  color: var(--brand-color-muted, #6c6a62);
  text-decoration: underline;
}

.sf-pdp-quiet:hover {
  color: var(--brand-color-accent-strong, #9c420a);
}

/*
 * THE SAME QUIET TREATMENT ON A BUTTON (W180/W181, Ryan 2026-09-04).
 *
 * The buy card's five `Details` / learn-about controls opened `"#"` and now
 * open pop-ups, and a control that does not navigate is not an anchor (the
 * storefront's standing rule) — so they are `<button>`s, and a button arrives
 * with a UA background, border, padding, font and alignment that all have to be
 * undone before `.sf-pdp-quiet` can be seen at all.
 *
 * MEASURED, NOT ASSUMED: shipping the buttons WITHOUT this reset grew the phone
 * buy card 20px (`pdpgate mbuy` read a 581px crop against a declared 560) — the
 * UA padding on five controls, exactly the failure this class exists to stop.
 *
 * IT IS THE GENERAL FORM of a reset this sheet already carries twice —
 * `.sf-pdp-install-video` and `.sf-pdp-rev__photos-all` are the same four
 * declarations under two feature names. Those are the install and review lanes'
 * to fold in when they are next in this file; naming it after the treatment
 * rather than the feature is what lets a fourth caller stop repeating it.
 * `flow.css`'s `.sf-flow-quiet--button` is the identical call on that surface.
 */
.sf-pdp-quiet--button {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  padding: 0;
  text-align: inherit;
}

/*
 * W183(b): THE INSTALL CARD'S PIER AND ASSEMBLY LINKS ARE BUTTONS NOW — they
 * open the shared lightbox rather than navigate, so `<a>` is wrong for them
 * (playbook's own rule: a control that does not navigate is not an anchor).
 * The same four-declaration reset `.sf-pdp-rev__photos-all` already carries
 * for its own button-that-reads-as-a-link, shared here rather than repeated a
 * third time; `.sf-pdp-quiet` — the class beside this one on the element —
 * still supplies the colour, size and underline, unchanged and shared by
 * both rungs.
 */
.sf-pdp-install-video {
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  padding: 0;
}

/* ----------------------------------------------------------- gallery ----- */

/*
 * THE STICKY GALLERY (Ryan's words-spec, 2026-08-26): "on desktop the
 * photo/gallery column is `position: sticky` while the reader scrolls through
 * the above-the-fold rail content, then RELEASES and scrolls away when the
 * above-the-fold section ends, making way for full-width content below."
 *
 * IT IS ORDINARY `position: sticky`, and the release is free. A sticky element
 * is confined to its CONTAINING BLOCK — here `.sf-pdp__grid` — so it un-pins
 * of its own accord the moment the grid's bottom edge reaches it. There is no
 * script, no sentinel, no scroll listener, and nothing for
 * `prefers-reduced-motion` to turn off: the same call the category page's photo
 * panel and its AI stack both made.
 *
 * `align-self: start` IS THE HALF THAT MAKES IT WORK, and it is not cosmetic. A
 * grid item stretches to its row by default, so this column was 1665.98px tall
 * — exactly the rail's height — and a sticky box the full height of its
 * containing block has ZERO travel and never pins. Sized to its own content it
 * is 1156px against the rail's 1666, which is 510px of pinned scroll.
 *
 * THE OFFSET reads the site's cascade, the same two rungs every other sticky on
 * this site reads (chrome.css publishes both):
 *
 *   --sf-pinned-offset   the pinned nav row     54px  (50 at the 920 cut)
 *   --sf-band-stuck-h    the stuck title band    0px  ON THIS PAGE
 *
 * AND THE BAND TERM IS ZERO HERE FOR FREE, which is the 2026-08-26 ruling
 * arriving in this sum with nothing written to make it happen: chrome.css
 * declares `--sf-band-stuck-h` off `:root:has(.sf-title-band)`, a product page
 * renders no title band at all, so the selector does not match and the `:root`
 * default of `0px` stands. MEASURED before and after: the column pinned at
 * 141px with 87 of them clearance for furniture that is not on the screen; it
 * now pins at 54, and `html`'s `scroll-padding-top` came down with it.
 *
 * WITHOUT the `32px` the convention adds on top of them, and that is deliberate
 * rather than an omission: the 32 is air for a panel that draws none of its own
 * (`.sf-cat-photos__panel`), and this column already draws 48px of it in the
 * padding below. Pinned, the stage's top edge therefore lands 48px under the
 * chrome — the board's own number — where `32px +` would have put it at 80.
 * Both fallbacks are `0px`, so a brand that never loads this chrome pins at the
 * viewport top and a page with no sticky band pins under the bar alone.
 *
 * DESKTOP ONLY, BY CONSTRUCTION: below the 767.98 cut `.sf-pdp__grid` is
 * `display: none` and the phone composition is a different DOM entirely, so
 * this rule cannot reach a phone. The mobile board stacks and nothing sticks.
 *
 * AND THE TRADE-OFF THIS FLAGGED IS NOW RULED. Pass 2 measured the column at
 * 1156px against 846px of viewport under the pinned chrome and shipped the
 * consequence: while pinned, the last 310px of it — most of the thumbnail grid
 * — was below the fold and could not be scrolled to. Ryan's ruling is the
 * fit-rule below: a subject that does not fit does not pin. With today's
 * content the column does not fit at any ordinary desktop viewport, so sticky
 * is off in practice — which Ryan knows, and is what he is weighing the
 * stage-only pivot against. That is what a
 * top-anchored sticky column taller than the viewport does; it is a
 * consequence of the spec, not a defect in it, and the two one-line answers
 * (anchor to `bottom` instead, so the column scrolls fully into view and THEN
 * pins; or cap it with `max-height` and let it scroll inside) are a ruling, not
 * a judgement call for this pass.
 *
 * Nothing between here and the scrolling element may be `overflow: hidden`, or
 * sticky silently degrades to static — the standing gotcha. Verified: neither
 * `.sf-pdp`, `.sf-container` nor any ancestor up to the document sets one.
 */
.sf-pdp-gallery {
  align-self: start;
  padding: 48px 24px 48px 0;
}

/*
 * THE STICKY DECLARATION, AND IT NAMES NO COLUMN (Ryan's rule v3, 2026-08-26).
 *
 * WHICHEVER COLUMN IS SHORTER PINS. Both are candidates and both wear
 * `.sf-pdp-sticky`; `pdp-sticky-column.tsx` measures them and marks the shorter
 * one, because the shorter column is the one with travel to spare inside the
 * grid and the taller one is what the reader is actually scrolling through.
 * Today that is the gallery (1156px against the rail's 1666), but it is a
 * MEASUREMENT and not a fact about galleries: a product with a short rail and a
 * long thumbnail grid flips it, and so does a narrow desktop width.
 *
 * `align-self: start` IS ON THE CANDIDATE, not on the winner, and that is what
 * makes the measurement honest: a grid item stretches to its row by default, so
 * a stretched column reports the ROW's height — both columns would measure
 * equal and "which is shorter" would have no answer. Sized to their own content
 * they can be compared, and the loser is unaffected either way (nothing paints
 * on a column's box).
 *
 * THE ANCHOR IS `min()` OF TWO RUNGS, which is the whole of rule v3 in one
 * declaration:
 *
 *   TOP-PINNED, at the chrome's own offset, whenever the column FITS in the
 *   space under it. This is the ordinary case and the one the boards draw.
 *
 *   BOTTOM-ANCHORED otherwise. `100vh - height` is negative-or-smaller than the
 *   offset exactly when the column is taller than the viewport, and a sticky
 *   box with that `top` does not come to rest until its own BOTTOM reaches the
 *   viewport's bottom — so the reader scrolls through the whole column once,
 *   every thumbnail included, and only then does it hold. That replaces the
 *   flagged pass-2 defect (a top-pinned column taller than the screen keeps its
 *   own foot below the fold forever, because scrolling is what holds it there)
 *   without giving up the pin.
 *
 * `min()` picks between them with no branch and no script: the driver publishes
 * the measured height and CSS does the arithmetic, so a resize re-answers it in
 * the same frame as the layout it belongs to. `--sf-pdp-sticky-h` falling back
 * to `0px` makes the second rung `100vh`, which is never the smaller of the
 * two — an unmeasured column top-pins, which is the pre-v3 behaviour.
 */
.sf-pdp-sticky {
  align-self: start;
}

.sf-pdp-sticky[data-sticky-pin="on"] {
  position: sticky;
  top: min(
    calc(var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px)),
    calc(100vh - var(--sf-pdp-sticky-h, 0px))
  );
}

/*
 * THE STAGE IS A CAROUSEL NOW (2026-08-27), and this rule is where that shows.
 *
 * It kept its box exactly — the 640px well, the radius, the ground — because
 * `tools/pdpgate.mjs` crops its `stage` region at this selector and the column
 * around it must not move. What changed is INSIDE: the flex column that centred
 * a sketched hoop and a mono caption is gone, and in its place is an absolutely
 * positioned track carrying three photographs.
 *
 * `overflow: hidden` is what makes the two neighbours invisible until a swipe
 * pulls them in. `touch-action: pan-y` is what makes the swipe possible at all:
 * it hands the horizontal axis to this component and leaves the vertical one to
 * the browser, so the page still scrolls under a finger that starts on the
 * photograph. Without it the browser claims both axes and the pointer stream
 * simply stops mid-gesture.
 *
 * `user-select: none` stops a drag across the stage selecting the caption, and
 * the grab cursors say the box is draggable before the reader tries it.
 *
 * THE GROUND IS A SWEEP NOW (Ryan, batch #4 G1, 2026-08-27) — see
 * `--sf-pdp-stage-ground` below for the recipe and why it is bounded where it
 * is.
 *
 * THE BOX GREW DOWNWARD ON 2026-08-27 (Ryan's G2 v2), and only downward. It was
 * 632 x 640 at 1440; it is 632 x 704 now, and the 64 is the CAPTION BAND that
 * used to sit outside it as a bar in the column. The photo area is still 632 x
 * 640 to the pixel — the track is clipped above the band, the counter is
 * measured from the photo area's corner and the arrows re-centre on it — so
 * nothing about the photograph moved; the frame around it got a foot. The
 * height is written as an addition rather than as 704 so the two terms stay
 * legible: the ruled well, plus the band.
 */
.sf-pdp-gallery__stage {
  background-color: var(--brand-color-well, #f1f0ec);
  background-image: var(--sf-pdp-stage-ground);
  border-radius: var(--radius-inner);
  cursor: grab;
  height: calc(640px + var(--sf-pdp-stage-band));
  overflow: hidden;
  /* The track, the opener and the furniture are positioned against this box. */
  position: relative;
  touch-action: pan-y;
  user-select: none;
}

.sf-pdp-gallery__stage[data-dragging="yes"],
.sf-pdp-mstage[data-dragging="yes"] {
  cursor: grabbing;
}

/*
 * THE OPENERS — one transparent control per surface, and the reason nothing
 * moved.
 *
 * The stage, the phone stage and every thumbnail are pressable. Each opener is
 * laid OVER its frame rather than being the frame's own box turned into a
 * `<button>`: the wells, the accent ring on the selected tile, the caption, the
 * counter and the flex-line arithmetic three paragraphs of this sheet are spent
 * on all keep the boxes they had, so `tools/pdpgate.mjs` can hold the regions at
 * rest.
 *
 * `inset: 0` needs a positioned ancestor and all three frames already are — the
 * stages for their track and their furniture, the tile for its own opener. Each
 * of those `position: relative` declarations carries a comment saying it is
 * load-bearing (the sr-span overflow shape, playbook §5); these openers are a
 * second reason.
 *
 * WITHOUT `inset: 0` THE BUTTON SHRINKS TO ITS ACCESSIBLE NAME — measured
 * 2026-08-27, when this rule was briefly lost in the carousel rewrite: a 16x6
 * box in the stage's top-left corner, which is the `.sf-pdp-sr` span's clip box
 * and the only visible symptom. The stage still "worked", for a reader who
 * pressed those six pixels.
 *
 * `border-radius: inherit` so the focus ring follows the frame's own corner
 * rather than boxing a rounded well in a square.
 */
.sf-pdp-gallery__open,
.sf-pdp-mstage__open,
.sf-pdp-tile__open {
  /* A button inherits none of the page's type or colour and draws a UA border,
   * a padding and a ground of its own; each reset below is one of those. */
  appearance: none;
  background: none;
  border: 0;
  border-radius: inherit;
  color: inherit;
  cursor: pointer;
  font: inherit;
  inset: 0;
  padding: 0;
  position: absolute;
}

/* ----------------------------------------------- the carousel, both boards -- */

/*
 * ONE MECHANISM, TWO BOARDS. `.sf-pdp-stage` is the desktop stage's carousel and
 * `.sf-pdp-mstage` is the phone's; the parts below are declared in pairs because
 * the two compositions are two DOMs (`pdp-rail-mobile.tsx` says why) and one
 * shared component draws both. Where a value differs it is in the phone block
 * further down this sheet, not here.
 *
 * THE TRACK IS THE ONLY THING THAT MOVES, and it moves by `transform` alone —
 * composited, so a swipe never touches layout or paint. The three slides are
 * parked a full stage-width apart with transforms of their own, which is why the
 * track's own offset is a single translation rather than an index times a width.
 */
.sf-pdp-stage__track,
.sf-pdp-mstage__track {
  inset: 0;
  position: absolute;
}

/*
 * AND ON THE DESKTOP BOARD THE TRACK STOPS ABOVE THE CAPTION BAND (Ryan's G2
 * v2, 2026-08-27: "the photo area ends above the band — NO photo ever behind
 * the text").
 *
 * THIS IS THE WHOLE MECHANISM of the in-box caption, and it is one line. The
 * band is not drawn OVER the photograph and the photograph is not faded under
 * the band: the box that carries the three slides simply ends where the band
 * begins, so there is nothing there to show through. The stage's
 * `overflow: hidden` does the rest — a slide is `inset: 0` of THIS box, so all
 * three are 64px shorter and the neighbours still park a full stage-width away.
 *
 * The phone board keeps `inset: 0`: its caption is a control-revealed overlay,
 * not a band, and its stage has no room under the photograph to give away.
 */
.sf-pdp-stage__track {
  bottom: var(--sf-pdp-stage-band);
}

/*
 * THE INSET CAME DOWN 28 -> 12 (Ryan, batch #4 G1, 2026-08-27: "photos
 * currently sit small in the box leaving an obvious frame").
 *
 * 28px was transplanted from the board, where this box held a SKETCH of a hoop
 * on a grey ground and the inset was that sketch's mount. It holds a photograph
 * now, and 28px of well on all four sides of a photograph is a matte frame —
 * the exact reading Ryan objected to. 12px keeps a hairline of ground on the
 * filled axis so the photograph reads as SITTING IN the box rather than being
 * the box, which is what the sweep behind it is for. Zero was computed and
 * rejected on one frame: the widest in the mirror (900x506) resolves to exactly
 * 632 across at this box, so it would meet both edges and that stage would have
 * no side ground at all.
 *
 * THE PHONE STAGE KEEPS ITS ZERO — it is full-bleed and its box is now the
 * dominant frame's own aspect, so an inset there would undo the fit outright.
 * See `.sf-pdp-mstage__slide`.
 */
.sf-pdp-stage__slide,
.sf-pdp-mstage__slide {
  align-items: center;
  display: flex;
  inset: 0;
  justify-content: center;
  padding: 12px;
  position: absolute;
}

.sf-pdp-stage__slide[data-slot="prev"],
.sf-pdp-mstage__slide[data-slot="prev"] {
  transform: translateX(-100%);
}

.sf-pdp-stage__slide[data-slot="next"],
.sf-pdp-mstage__slide[data-slot="next"] {
  transform: translateX(100%);
}

/*
 * THE PHOTOGRAPH FILLS ITS CONSTRAINED AXIS, AND IT NEVER CROPS (Ryan's
 * ruling, batch #4 G1, 2026-08-27).
 *
 * ─── WHAT WAS WRONG, AND IT WAS ONLY WRONG FOR SOME FRAMES ─────────────────
 *
 * This rule used to read `width: auto; height: auto` under a pair of
 * max-lengths. That fits a photograph BIGGER than the well down into it, which
 * is 89 of this gallery's 93 frames and looks correct — and it leaves a
 * photograph SMALLER than the well at its own intrinsic size, marooned in the
 * middle of a 632x640 box. The four legacy dimension diagrams are 280x280 on
 * the legacy site and 280x280 in our mirror (their raws were measured: there is
 * no bigger source to regenerate from), so those four drew a postage stamp
 * inside a frame of well. `auto` cannot tell the two cases apart, because it is
 * a MAXIMUM and never a target.
 *
 * `width`/`height: 100%` with `object-fit: contain` states the target instead:
 * fill the box, keep the aspect, letterbox whatever is left. One axis of every
 * frame now touches the slide's edge — which is what "fit each photo up to the
 * container" asked for — and the axis that does not is the residual the sweep
 * behind it is designed to carry.
 *
 * IT STILL NEVER CROPS. `cover` was not on the table: these are product
 * photographs and a cropped one cuts a pole or a board edge off, which is a
 * factual claim about the hoop rather than a composition choice. The strip's
 * thumbnails crop and say why (`.sf-pdp-tile__img`); the stage is where the
 * whole product is owed.
 *
 * THE INTRINSIC ATTRIBUTES STILL DO THEIR JOB. Every frame declares its size in
 * the markup, and with both lengths now at 100% those attributes are no longer
 * a size at all — they are the aspect ratio the box is reserved from before the
 * file decodes, which is all this sheet ever wanted them for.
 *
 * FLAGGED, MEASURED: the four 280x280 diagrams are the only frames this
 * UPSCALES, and they land at 2.17x at 1440. Ryan chose filling over framing
 * with that number in hand; if a quality review reverses it, the honest fix is
 * a bigger source, not a smaller box.
 */
.sf-pdp-stage__img,
.sf-pdp-mstage__img {
  display: block;
  height: 100%;
  object-fit: contain;
  width: 100%;
}

/*
 * ─── THE CAPTION BAND, INSIDE THE BOX (Ryan's ruling G2 v2, 2026-08-27) ─────
 *
 * TWO NAMES ARE RETIRED HERE, and they are two different elements a day apart.
 *
 *   `.sf-pdp-stage__caption` (retired 2026-08-27, batch #4 G2) was a line
 *   absolutely positioned on the stage's foot at `bottom: 24px; left: 24px`,
 *   sharing that foot with the counter pill and wearing a
 *   `max-width: calc(100% - 180px)` whose only job was to not collide with it.
 *
 *   `.sf-pdp-gallery__caption` (retired 2026-08-27, G2 v2) was what replaced
 *   it: a reserved BAR in the column between the stage and the thumbnail strip.
 *   It fixed the collision and the jumping strip, and Ryan rejected it on the
 *   reading — a bar outside the stage's drawn box, aligned to the section
 *   rather than to the photograph, does not belong to the photograph.
 *
 * `.sf-pdp-stage__band` is the answer to both. It is INSIDE the stage's box, on
 * the same ground, under the same rounded corners, and the photo area ends
 * above it — so it is a foot of the frame rather than a caption laid over a
 * picture or a bar floating under one.
 *
 * WHAT IT KEEPS FROM THE BAR: the reserved floor. `min-height` is two 18px line
 * boxes and the strip's top never moves, which is the invariant
 * `tools/pdpstagegate.mjs` drives across a short caption, the longest one, and
 * an untitled frame. It draws EMPTY for the untitled fifty-three rather than
 * falling back to "Product photo" — a caption that says nothing under a picture
 * that says it is furniture, and the reserved height is what makes an empty
 * band cost nothing.
 *
 * THE TEXT IS INSET 24px, which is the counter pill's own inset from the other
 * corner. The two are the only things drawn on this box's chrome and they now
 * share one margin, which is the alignment the outside bar could not have (it
 * was in the column, the counter was on the stage).
 *
 * CONTRAST, MEASURED, because the ground changed underneath the words: the
 * band sits in the bottom eighth of the sweep, whose darkest stop is `line`
 * #E8E7E3, and `muted` #6C6A62 on that is 4.95:1 — above the 4.5 floor for
 * text this size. On the page's `paper` where the bar used to sit it was
 * 5.30:1, so the move costs a third of a point and stays compliant. The band
 * cannot get darker than `line`: the sweep is bounded there by G1's own rule.
 */
.sf-pdp-stage__band {
  bottom: 0;
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  left: 0;
  letter-spacing: 0.14em;
  /* 18px LINE BOXES, INTEGER — see `--sf-pdp-stage-band` for the arithmetic
   * this is half of, and for why a fractional band is a pixel-gate regression
   * in four rows of thumbnails. */
  line-height: 18px;
  margin: 0;
  min-height: var(--sf-pdp-stage-band);
  padding: 14px 24px;
  position: absolute;
  right: 0;
  text-transform: uppercase;
}

/*
 * THE COUNTER SITS WHERE THE SPIN PILL SAT, and that is a trade this pass made
 * deliberately.
 *
 * The board drew a "Drag to spin · 360°" pill on the stage's foot, and it was an
 * `aria-disabled` stub — a real control that could not act yet and said so,
 * waiting on `PhotoViewers.dc.html`'s 360° pass. A stage that DRAGS to change
 * photographs cannot carry a pill saying that dragging spins the product: the
 * two gestures are the same gesture, and the pill would be documenting a
 * behaviour the stage no longer has. So the pill's box, treatment and position
 * are kept and its words are replaced with the one fact the carousel owes the
 * reader — where they are in the strip. FLAGGED: the 360° control comes back
 * with the 360° viewer, and it needs a press rather than a drag when it does.
 *
 * ─── AND IT DID NOT MOVE WHEN THE BOX GREW (Ryan's G2 v2, 2026-08-27) ───────
 *
 * "The counter stays where it is." The box under it is 64px taller than it was,
 * so `bottom: 24px` — which used to mean 24 from the photograph's foot — would
 * now mean 24 from the BAND's foot and would drop the pill onto the caption.
 * The 24 is therefore measured from the PHOTO AREA's corner, which is the
 * box less the band, and the pill lands on exactly the pixels it landed on
 * before. The corner ruling is a fact about the photograph, not about the box.
 */
.sf-pdp-stage__counter {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-pill);
  bottom: calc(var(--sf-pdp-stage-band) + 24px);
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-support);
  padding: 7px 16px;
  pointer-events: none;
  position: absolute;
  right: 24px;
}

/*
 * THE ARROWS ARE THE POINTER'S PATH THROUGH THE STRIP, and the keyboard's.
 *
 * QUIET UNTIL WANTED: they fade in on hover and on focus, because a photograph
 * with two permanent chrome discs over it is a slideshow widget rather than a
 * product shot, and the board never drew them. They are never `display: none` on
 * a pointer device — a hidden control is unfocusable, and these are the only way
 * a keyboard has of moving the stage.
 *
 * ON A TOUCH DEVICE THEY ARE REMOVED, not faded: there is no hover to reveal
 * them with, the swipe is the affordance, and the phone board draws dots
 * instead. `(hover: none)` is the query for "this pointer cannot hover", which
 * is the actual condition — a narrow window on a laptop still gets arrows.
 */
.sf-pdp-stage__nav {
  align-items: center;
  appearance: none;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: 50%;
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: flex;
  height: 40px;
  justify-content: center;
  opacity: 0;
  padding: 0;
  position: absolute;
  /* THE MIDDLE OF THE PHOTOGRAPH, NOT OF THE BOX (G2 v2, 2026-08-27). The box
   * gained a caption band at its foot; a bare `50%` would centre these discs on
   * the box and hang them 32px low over the picture they act on. Half of the
   * box less the band is the photo area's own middle, which is the same y they
   * have always been drawn at. */
  top: calc((100% - var(--sf-pdp-stage-band)) / 2);
  transform: translateY(-50%);
  transition: opacity 160ms ease;
  width: 40px;
}

.sf-pdp-gallery__stage:hover .sf-pdp-stage__nav,
.sf-pdp-gallery__stage:focus-within .sf-pdp-stage__nav {
  opacity: 1;
}

/*
 * W72 (Ryan, 2026-09-03) — SPENT AT THE ENDS. The stage inherited the viewer's
 * no-wrap, so at the first and last frame one of these discs has nowhere to go.
 * Dimmed rather than hidden: a control that vanishes at the boundary moves the
 * reader's landmark, and these two already fade in and out on hover — a third
 * state that removed one would read as a glitch. Same treatment, same reasoning
 * and the same numbers as the viewer's own disabled-arrow rule — see the
 * matching block in lightbox.css. (Named in prose rather than written out: the
 * prefix sweeps read this sheet for foreign class names and cannot tell a
 * comment from a selector.)
 *
 * THE DIM IS SCOPED TO THE SHOWN STATE, and that is not tidiness — a bare
 * `:disabled { opacity: 0.35 }` is a class-plus-pseudo-class and outranks the
 * `opacity: 0` on the element rule, so a spent arrow painted itself at 35% on a
 * stage nobody was pointing at. Caught by `pdpgate`, which read the resting
 * gallery a hundredth of a percent over its ceiling — small enough to wave
 * through and exactly the kind of thing a recorded ceiling exists to refuse.
 */
.sf-pdp-stage__nav:disabled {
  cursor: default;
}

.sf-pdp-gallery__stage:hover .sf-pdp-stage__nav:disabled,
.sf-pdp-gallery__stage:focus-within .sf-pdp-stage__nav:disabled {
  opacity: 0.35;
}

.sf-pdp-stage__nav--prev {
  left: 16px;
}

.sf-pdp-stage__nav--next {
  right: 16px;
}

@media (hover: none) {
  .sf-pdp-stage__nav {
    display: none;
  }
}

/*
 * REDUCED MOTION IS BELT AND BRACES HERE. `pdp-gallery.tsx` already skips the
 * settle and puts the reader on the next frame immediately when the preference
 * is set; this stops a transition the component did not start (a stylesheet
 * change, a later pass) from animating anyway.
 */
@media (prefers-reduced-motion: reduce) {
  .sf-pdp-stage__track,
  .sf-pdp-mstage__track {
    transition: none !important;
  }
}

/*
 * `.sf-pdp-gallery__caption` IS RETIRED (Ryan's ruling G2 v2, 2026-08-27), and
 * so is the `--sf-pdp-caption-line` custom property it declared.
 *
 * It was the reserved 50px bar between the stage and the strip — two 18px line
 * boxes and a 14px lead-in — and it did the job it was built for: the thumbnail
 * rows stopped jogging as the caption changed length. Ryan rejected it on a
 * different reading. Outside the stage's drawn box and aligned to the section's
 * left edge, it read as a line of section copy that happened to be near a
 * photograph rather than as the photograph's own caption.
 *
 * ITS FLOOR AND ITS WORDS MOVED INSIDE THE BOX, and everything the bar's block
 * argued is argued at `.sf-pdp-stage__band` instead: the measured two-line
 * reservation, the empty band for the fifty-three untitled frames, and the
 * counter question (which is settled the same way — the counter did not move,
 * because it is measured from the photo area rather than from the box).
 *
 * WHAT CHANGED IN THE COLUMN: it has two children again rather than three, and
 * it is 14px taller than the bar left it — the band's new bottom pad, which the
 * bar did not need because it ended at the column's flow rather than at a drawn
 * edge. `tools/pdpgate.mjs` carries that as `CAPTION_BAR_DELTA`, re-derived.
 */

/*
 * FLEX-WRAP, NOT GRID, and the difference is measurable.
 *
 * The board draws four flex rows of `flex: 1`, and a flex line redistributes its
 * free space around the ONE tile whose 2px border sits outside its box (see
 * `--selected` below): row 1 comes out 120 + 116 x 4, rows 2-4 come out 116.8 x
 * 5. A grid cannot do that — `1fr` tracks are sized before the items are laid
 * into them, so the wide tile overflows its track and its neighbours stay put,
 * which read as 0.550% against the board. Wrapped flex lines each distribute
 * their own free space, which is the board's behaviour exactly.
 *
 * `calc(20% - 12px)` is a fifth of the row less its share of the gaps: five per
 * line fit, six never do, at any container width.
 */
.sf-pdp-gallery__tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  /* 16 above the first row, 12 between rows — the board's own numbers, and the
   * reason the row gap is not simply the top margin. */
  margin-top: 16px;
}

/*
 * EVERY TILE WEARS A BORDER NOW (Ryan, batch #4 G3a, 2026-08-27:
 * "white-background photos vanish on the white page").
 *
 * It is literally true and it is a property of the photography rather than of
 * the layout: the legacy THOR frames are shot on white, the tile crops them
 * edge to edge (`.sf-pdp-tile__img` below says why), and a white rectangle on a
 * #FBFBFA page has no edge at all — the strip read as a row of floating hoops
 * with no tiles under them. `--border-control` is the site's own 1px
 * #D8D6D0 control edge, the same line the counter pill and the zip field wear,
 * which is 35 levels off the page ground and cannot be missed.
 *
 * NO GEOMETRY MOVED. `box-sizing: border-box` is global (globals.css `*`), so
 * the border is drawn INSIDE the 116.8 x 92 box every tile already had — the
 * same arithmetic that let the selected ring in without moving the strip.
 *
 * The transition serves the hover below; it names its two properties rather
 * than `all`, so a class change that swaps the ring cannot animate anything
 * this rule did not choose.
 */
.sf-pdp-tile {
  background: var(--brand-color-well, #f1f0ec);
  border: var(--border-control);
  border-radius: var(--radius-inner);
  flex: 1 1 calc(20% - 12px);
  height: 92px;
  /* `overflow: hidden` is what rounds the PHOTOGRAPH's corners: the thumbnail
   * fills the tile edge to edge, and a square image inside a rounded well is the
   * one place this sheet's radius would visibly stop being applied. */
  overflow: hidden;
  /* The opener is absolutely positioned against its own tile; an unpositioned
   * tile would hand it the page instead — the sr-span overflow shape (playbook
   * §5), here with a visible box rather than a hidden one. */
  position: relative;
  transition:
    border-color 140ms ease,
    box-shadow 140ms ease;
}

/*
 * `cover`, NOT `contain`, and it is the one place on this page that crops.
 *
 * The strip is a MAP of the stage — twenty 116x92 tiles whose job is to say
 * which photograph is where. Fitted, each portrait frame would draw a 74px
 * sliver between two bands of empty well and the strip would read as a row of
 * blank tiles with something small in the middle of each; filled, every tile is
 * a picture. The stage never crops, which is where the whole photograph is owed.
 */
.sf-pdp-tile__img {
  display: block;
  height: 100%;
  object-fit: cover;
  width: 100%;
}

/*
 * SELECTION IS THE RING, AND ONLY THE RING (Ryan, 2026-08-26).
 *
 * The board drew this tile 4px bigger than its neighbours — 120 x 96 against
 * 116 x 92 — and pass 1 transplanted that as drawn, converting the board's
 * content-box arithmetic into the border-box equivalent and flagging the size
 * difference for a ruling. The ruling came back: the extra 4px was an artifact
 * of how the board was authored (its default content-box hung the frame's
 * border outside the 92px well), not a decision, and a selected photo that is
 * physically LARGER than the ones beside it is not a selection language this
 * site has anywhere else.
 *
 * So the tile keeps the same 92 box as every other tile — no `flex-basis`
 * override, no `height` override, both inherited from `.sf-pdp-tile` — and
 * selection is said by the design system's own 1.5px accent ring (tokens §1,
 * the chip/filter selection language, "never normalise it to 1px"). Border-box
 * draws the ring INSIDE the well, so the box does not move.
 *
 * CONSEQUENCE, MEASURED AND INTENDED: the first thumbnail row loses the 4px it
 * only had for holding this tile, so the three rows under it — and the foot of
 * the gallery column — sit 4px higher. That is the whole of the shift; the
 * pdpgate `tiles` and `gallery` regions are re-recorded for it.
 */
/*
 * AND SELECTION IS STATE NOW, not content (2026-08-27). The ring used to be
 * authored on one tile of a static grid; it follows the carousel, so which tile
 * wears this class changes as the reader swipes — and pressing a tile is what
 * moves it, which is the reversal `pdp-gallery.tsx`'s header flags. Border-box
 * still draws the ring INSIDE the well, so the box does not move and the
 * photograph under it is clipped by the tile's own radius rather than covering
 * the ring.
 */
/*
 * AND IT IS STRONGER THAN THE RING WAS (Ryan, batch #4 G3b, 2026-08-27:
 * "selected = thicker highlight border + drop shadow").
 *
 * 1.5px WAS NOT SURVIVING THE TRIP TO THE SCREEN. Measured on this build at
 * DPR 1, `getComputedStyle` returned a USED border-width of 1px on the selected
 * tile — the browser rounds a fractional border to a device pixel, so the
 * design system's 1.5px accent ring and the 1px control edge every tile now
 * wears rasterised to the SAME weight, and selection was being said in colour
 * alone at 116px wide. That is the defect Ryan is describing, and it is why
 * this is a deviation from the tokens' "1.5px accent ring, never normalise"
 * rule rather than a violation of it: the ring has to outweigh a border that
 * did not exist when that rule was written. 2.5px declared is a USED 2px at
 * DPR 1 (measured on this build, the same readback that caught the 1.5) and
 * 2.5px at DPR 2 — twice the neighbours' weight on either screen, which is the
 * property that matters rather than the declared number.
 *
 * THE SHADOW IS THE SECOND CHANNEL, and it is scaled to a 92px tile: the page's
 * card shadows are `0 10px 26px` over boxes ten times this size, and that blur
 * on a thumbnail is a smudge. `0 3px 10px` at 20% ink lifts the selected tile
 * off the strip without pretending it moved.
 *
 * STILL NO GEOMETRY. Border-box keeps the heavier ring inside the same 92 box,
 * and a `box-shadow` is painted outside the border box without participating in
 * layout — the strip's four flex lines are the same four flex lines.
 */
.sf-pdp-tile--selected {
  border: 2.5px solid var(--brand-color-accent, #c4530a);
  box-shadow: 0 3px 10px color-mix(in srgb, var(--brand-color-ink, #17181a) 20%, transparent);
}

/*
 * HOVER IS A BORDER AND A BREATH OF SHADOW — no lift (Ryan, batch #4 G3c,
 * 2026-08-27, delegating the judgment: "consistent with the site's restrained
 * motion language").
 *
 * THE SITE HAS NO LIFT ANYWHERE. Every hover on this storefront is one of three
 * things: a border-colour change (`.sf-nav-menu__card`), a ground wash
 * (`.sf-ph__sort-item`, `.sf-cat-embed__option`) or a colour going accent
 * (`.sf-home-band__card`). Not one of them translates a box. A thumbnail that
 * rose 2px under the cursor would be the only moving hover on the site, and it
 * would move it inside a strip whose whole job is to be a stable map — so the
 * effect is the border darkening to `faint` and the same shadow the selected
 * tile wears, at half its weight. Nothing moves, which is also why there is no
 * reduced-motion carve-out for the effect itself; the 140ms fade that carries
 * it is still cut below, because a preference for less motion is a preference
 * for state changes that are simply THERE.
 *
 * `@media (hover: hover)` because a touch device resolves `:hover` on tap and
 * leaves it stuck on the last tile pressed — which on this strip is the tile
 * the reader just selected, wearing a hover treatment nobody triggered.
 *
 * NOT ON THE SELECTED TILE. Its ring already outranks both, and letting hover
 * repaint it would make the map's one distinguished tile flicker as the pointer
 * crosses it.
 */
@media (hover: hover) {
  .sf-pdp-tile:not(.sf-pdp-tile--selected):hover {
    border-color: var(--brand-color-faint, #9a988f);
    box-shadow: 0 2px 8px color-mix(in srgb, var(--brand-color-ink, #17181a) 10%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sf-pdp-tile {
    transition: none;
  }
}

/*
 * THE THREE PLACEHOLDER TREATMENTS ARE GONE (2026-08-27). `--labeled` centred a
 * mono word in an empty well, `__play` drew an outlined play mark in it, and
 * `__badge` put an accent disc on the film's tile. All three were ways of
 * drawing a photograph that had not been taken; every tile now holds one, and
 * the words those labels carried are the stage's caption and the viewer's title.
 * `__label` survives because the `+N` signpost still needs it.
 */
.sf-pdp-tile__label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
}

.sf-pdp-tile--more {
  align-items: center;
  background: var(--brand-color-media-well-a, #23262e);
  display: flex;
  justify-content: center;
}

.sf-pdp-tile--more .sf-pdp-tile__label {
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-body);
  font-weight: 700;
}

/* ------------------------------------------- the gallery viewer's panel -- */

/*
 * WHAT THE SHARED VIEWER DRAWS UNDER ITS CAPTION for one of these frames — the
 * `panel` slot the viewer's core hands to whoever opened it.
 *
 * THIS IS NOT A FORK OF THE VIEWER. `MediaLightbox` owns the eyebrow, the
 * title, the caption, the control band, the strip, the gestures and the phone
 * tray, and none of them is touched here; the panel is the one documented seam
 * for a caller's own context, and every caller styles what it puts there under
 * its OWN prefix — `.sf-ph__lb-*` for the customer-photo grid in photos.css,
 * the harness's own block in lightbox.css, this for the product gallery. (The
 * viewer's prefix is never written in this sheet: `tests/media-lightbox.test.ts`
 * asserts the partition over the raw file, comments included.)
 *
 * IT IS THE FOUR FEATURED SPECS the rail already draws, which is per-PRODUCT
 * data rather than per-frame — the frames are the same system photographed
 * differently, so a per-frame record would be inventing differences the
 * photography does not have. On a phone this block is also what gives the
 * metadata tray something to open TO: a tray whose title line is the last thing
 * in it has no travel, and the gesture then reads as broken.
 */
.sf-pdp-lb {
  margin-top: 14px;
}

.sf-pdp-lb-eyebrow {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin: 0 0 8px;
}

.sf-pdp-lb-specs {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
}

/*
 * TERM AND VALUE ON ONE LINE, the term muted and the value in ink — the same
 * two-column reading the rail's own spec rows give, at the panel's smaller
 * size. `<dl>`/`<dt>`/`<dd>` because that is what a list of named values is;
 * the `<div>` per row is the grouping the spec allows and is what lets the pair
 * be a flex row.
 */
.sf-pdp-lb-row {
  display: flex;
  gap: 12px;
  justify-content: space-between;
}

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

.sf-pdp-lb-value {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-small);
  font-weight: 600;
  margin: 0;
  text-align: right;
}

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

.sf-pdp-rail {
  display: flex;
  flex-direction: column;
  gap: 34px;
  padding: 72px 0 48px 40px;
}

.sf-pdp-rail__head {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

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

/*
 * 46px is OFF THE PLATFORM SCALE — §2 runs 40 (category title) then 44 (display
 * small). The canvas's own type annotation names it: "display 46". It is the
 * board's measured number and it ships as drawn.
 */
.sf-pdp-rail__name {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-heading, system-ui, sans-serif);
  font-size: 46px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.05;
  margin: 0;
  /* THE HEADING RESET, UNDONE. This line became the page's `<h1>` when the
   * title band dropped its title (Ryan, 2026-08-26), and globals.css gives
   * every `h1` `text-wrap: balance` — which re-breaks a wrapped product name
   * to even out its lines and moved this region 14% off the board. The element
   * changed; the drawing did not. `wrap` is the correct spelling of "normal"
   * for this property (chrome.css:2000 already knew; `text-wrap: normal` is
   * invalid and silently dropped). */
  text-wrap: wrap;
}

.sf-pdp-rail__rating {
  align-items: center;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-support);
  gap: 10px;
  margin: 0;
}

.sf-pdp-rail__stars {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
  gap: 2px;
}

.sf-pdp-rail__score {
  font-weight: 500;
}

.sf-pdp-rail__count,
.sf-pdp-rail__provenance {
  color: var(--brand-color-muted, #6c6a62);
}

.sf-pdp-rail__pipe {
  color: var(--brand-color-line-mid, #d8d6d0);
}

.sf-pdp-rail__lede {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  line-height: 1.55;
  margin: 0;
}

/* --------------------------------------------------- featured specs ------ */

.sf-pdp-specs {
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 4px 0 2px;
}

.sf-pdp-specs__row {
  display: flex;
  font-size: var(--text-body);
  gap: 14px;
}

.sf-pdp-specs__label {
  color: var(--brand-color-ink, #17181a);
  flex: none;
  font-weight: 700;
  width: 118px;
}

.sf-pdp-specs__value {
  flex: 1;
  margin: 0;
}

.sf-pdp-specs__note {
  color: var(--brand-color-muted, #6c6a62);
}

/* ---------------------------------------------------------- buy card ----- */

.sf-pdp-buy {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 24px 28px;
}

.sf-pdp-buy__lines {
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-support);
  gap: 12px;
}

.sf-pdp-buy__line {
  align-items: baseline;
  display: flex;
  justify-content: space-between;
}

/* The product's own line is the only one set in ink — it is the thing being
 * bought; everything under it is an adjustment to it. */
.sf-pdp-buy__line:first-child .sf-pdp-buy__line-label {
  color: var(--brand-color-ink, #17181a);
  font-weight: 600;
}

.sf-pdp-buy__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-weight: 600;
  gap: 6px;
  padding: 4px 10px;
}

.sf-pdp-buy__badge-mark {
  display: inline-flex;
}

.sf-pdp-buy__amount {
  font-family: var(--brand-font-mono, monospace);
}

.sf-pdp-buy__amount--ink {
  color: var(--brand-color-ink, #17181a);
}

.sf-pdp-buy__amount--sale {
  color: var(--brand-color-accent, #c4530a);
  font-weight: 500;
}

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

/* ----------------------------------------------------------- options ----- */

/* THE OPTION ROW'S GEOMETRY, NAMED — because a second element now has to line
 * up with the chips (Ryan's WP6 line item), and a hand-copied 90px would be a
 * number free to disagree with the row that produced it. */
.sf-pdp-buy__options {
  --sf-pdp-option-label-w: 82px;
  --sf-pdp-option-gap: 8px;

  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  display: flex;
  flex-direction: column;
  font-size: var(--text-small);
  gap: 10px;
  padding-top: 12px;
}

.sf-pdp-buy__options-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: 5px;
}

/*
 * AN OPTION ROW IS NOW JUST ITS LABEL, ITS CHIPS AND ITS EDUCATION LINK.
 *
 * The right-hand delta column is deleted (2026-08-27), and with it the
 * `justify-content: space-between` that only existed to push it there. Ryan
 * ruled the figures into the chips on the ORDER RAIL on 2026-08-26 — citing
 * this page's own price language as the precedent, since MobileA.dc.html
 * already draws "Rust Armor +$129" inside a chip — and asked on 2026-08-27 for
 * the PDP's options to be patterned "VERY close (if not exactly)" on that rail.
 * The column said what only the SELECTED choice cost ("+$0", twice) on the page
 * whose job is to answer "how much more is Rust Armor?".
 *
 * A DEVIATION FROM Main.dc.html, recorded: the desktop board draws the column.
 * `tools/pdpgate.mjs` re-baselines `buy` for it, with this attribution.
 */
.sf-pdp-buy__option {
  align-items: center;
  display: flex;
  gap: var(--sf-pdp-option-gap);
}

.sf-pdp-buy__option-choices {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.sf-pdp-buy__option-label {
  color: var(--brand-color-ink-soft, #44464a);
  width: var(--sf-pdp-option-label-w);
}

/*
 * THE CHIP. A `<button>` where the board draws a `<span>`, because a choice is a
 * control — so it needs the font reset a button does not inherit. The selected
 * state is the platform's 1.5px accent border (tokens §1: never normalised to
 * 1px), which makes the chip a pixel wider than an unselected one. That is the
 * board's own geometry, not drift.
 */
.sf-pdp-chip {
  background: none;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: inherit;
  cursor: pointer;
  font: inherit;
  padding: 5px 12px;
}

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

/*
 * THE PRICE INSIDE THE CHIP — the ORDER RAIL's treatment, to the declaration
 * (`.sf-flow-chip__delta` in flow.css). Mono, because money is a mono duty
 * (tokens §2), one step down so the figure sits beside the label without
 * competing with it, and `white-space: nowrap` so a chip can never break
 * between its name and its price — which is the wrap the delta column's removal
 * was made to end.
 *
 * NO BOARD DRAWS THIS ONE. Main.dc.html puts the desktop figure in a column
 * instead, so there is nothing to transplant and the rail's own drawing is the
 * precedent — which is what Ryan asked for. The PHONE chip is the opposite case
 * and is treated as such: see `.sf-pdp-mchip__delta`.
 *
 * PARITY NOTE: flow.css owns the twin. The two sheets are deliberately NOT
 * merged (share the engine, keep the chrome per-surface); consistency is the
 * tokens they both read, and this comment is where a reader is told there is a
 * twin to keep in step.
 */
.sf-pdp-chip__delta {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  font-weight: 400;
  white-space: nowrap;
}

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

.sf-pdp-buy__total {
  align-items: center;
  border-top: var(--border-money);
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  padding-top: 12px;
}

.sf-pdp-buy__total-label {
  font-size: var(--text-body);
  font-weight: 700;
}

.sf-pdp-buy__total-money {
  align-items: center;
  display: flex;
  gap: 10px;
}

.sf-pdp-buy__strike {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-body);
  text-decoration: line-through;
}

.sf-pdp-buy__price {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-page-title);
  font-weight: 700;
}

/* ------------------------------------------------------- line items ----- */

/*
 * THE LINE-ITEM CHECKBOX — things ON the order that are not the system.
 *
 * ─── WHERE IT IS, AND WHAT LEFT (Ryan's WP6, 2026-08-29) ───────────────────
 *
 * "Directly under the Lightning chip row — not down by the price." So the
 * block that stood under the money moment is RETIRED BY NAME, all of it:
 * `.sf-pdp-buy__items` and `.sf-pdp-buy__items-eyebrow` (its dashed rule and
 * its "ADD TO YOUR ORDER" mono micro header), and MP1's group inside it —
 * `.sf-pdp-item__main`, `__photo`, `__body`, `__chips` — with the photograph
 * and the two chips they drew. MP1 had already retired `.sf-pdp-item--on`,
 * `__price` and `__toggle` with the Add/Remove row before them. Three shapes,
 * all named, none to be rebuilt.
 *
 * WHAT DRAWS NOW is one line inside `CHOOSE YOUR OPTIONS`, immediately after
 * the option row whose chip gates it:
 *
 *   Adjustment   (Hand Crank) (Lightning +$249)   See it in action
 *                [x] Add drill - $149
 *                Any name brand 18 to 24 volt battery powered drill can be...
 *
 * ─── THE INDENT IS DERIVED, NOT TYPED ──────────────────────────────────────
 *
 * It has to start where the CHIPS above it start, or the line reads as a new
 * block rather than as a consequence of the row. That column is the option
 * label's width plus the row's gap, and both are now variables the option row
 * itself consumes (`--sf-pdp-option-label-w`, `--sf-pdp-option-gap`), so the
 * checkbox cannot drift from the chips the day either value moves. MEASURED:
 * 82 + 8 = 90px, which is the chips' own left edge.
 *
 * ─── THE CONTROL IS THE SITE'S CHECKBOX, to the declaration ────────────────
 *
 * `flow-checkout-payment.tsx` + `flow.css`'s `.sf-flow-ck__check` family is
 * this site's checkbox and it was already built correct: the real input
 * CLIPPED inside the drawn box (not `display: none` — a hidden control is not
 * a control), the box `position: relative` so the absolute input cannot take
 * the page as its containing block (the playbook's sr-span rule), the ring on
 * the DRAWN box via `:has(:focus-visible)` (tokens §3's one sanctioned
 * indirection), and the tick present at both states so ticking reflows
 * nothing. Copied here rather than shared, which is this codebase's ruled
 * architecture for the order engine: share the control, keep the chrome
 * per-surface.
 *
 * THE 44px TOUCH FLOOR IS ON THE LABEL, which is what makes it honest: the
 * whole line is the tap target (Ryan's ruling), so the target is the width of
 * the words rather than a 20px square a thumb has to find.
 */
.sf-pdp-item {
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-left: calc(var(--sf-pdp-option-label-w) + var(--sf-pdp-option-gap));
}

/* THE WHOLE LINE IS THE LABEL — 44px minimum, declared here rather than on the
 * box, so the floor holds however short the words get. */
.sf-pdp-item__control {
  align-items: center;
  cursor: pointer;
  display: flex;
  gap: 10px;
  min-height: 44px;
}

/* The drawn box: 20px, 1.5px accent border, radius 4 — flow.css's own
 * `.sf-flow-ck__box-mark`, to the value, because there is one checkbox on this
 * site and this is what it looks like. */
.sf-pdp-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: 20px;
  justify-content: center;
  position: relative;
  width: 20px;
}

/* THE REAL CONTROL, CLIPPED INSIDE THE DRAWN BOX — not `display: none`, which
 * would take it out of the tab order, and `position: absolute` inside a
 * `position: relative` box so it cannot take the PAGE as its containing block
 * and extend the document's overflow (the playbook's sr-span rule).
 *
 * `outline: none` IS UNCONDITIONAL, not on `:focus-visible`, which is this
 * sheet's own convention for the shape (`.sf-pdp-rev__field-input` and
 * `.sf-pdp-ans__field-input` both do it): the ring is moved to the drawn box in
 * the focus block at the top of this sheet, and a `:focus-visible` suppression
 * would read as a rule fighting the ring rather than delegating it. */
.sf-pdp-item__box {
  height: 100%;
  inset: 0;
  margin: 0;
  opacity: 0;
  outline: none;
  position: absolute;
  width: 100%;
}

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

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

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

/* The words carry the price, in one text node (`lineItemOfferLabel`). Body ink
 * at the options block's own size — it is an offer to read, not a caption. */
.sf-pdp-item__label {
  font-weight: 500;
}

/*
 * THE RECOVERED SENTENCE (Ryan's MP1, kept by WP6: "the your-own-drill
 * sentence stays"). Muted caption ink — the card's quietest rung, which is
 * right for a fact that removes an obligation rather than adding one — and it
 * wraps at the measure the checkbox's indent leaves it.
 *
 * NOT INDENTED PAST THE BOX. It belongs to the offer, not to the control, and
 * a second indent under a 90px one would set two sentences four words to the
 * line.
 */
.sf-pdp-item__note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  line-height: 1.5;
  margin: 0 0 2px;
}

/* ----------------------------------------------------------- actions ----- */

.sf-pdp-buy__actions {
  display: flex;
  gap: 12px;
  margin-top: 20px;
}

/*
 * A FIELD BESIDE A BUTTON MATCHES ITS HEIGHT (Ryan's W13, 2026-09-01).
 *
 * THIS WAS WHY ADD TO CART MEASURED 52. The drawn quantity field took its height
 * from its two steppers' `15px` of padding on a 16px line box — 52 — and the CTA
 * beside it is a `flex: 1` sibling in a stretch row, so the FIELD was setting the
 * BUTTON's height and the button's own declared 48 never got to apply. Declaring
 * the field at the control height fixes the pair in one place, which is the
 * point of `--control-height` being shared by fields and buttons rather than
 * each having a private number.
 *
 * The steppers keep their side padding and give up their vertical padding: the
 * box is the field's now, and they fill it.
 */
.sf-pdp-qty {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: inline-flex;
  min-height: var(--control-height, 48px);
}

.sf-pdp-qty__step {
  align-items: center;
  align-self: stretch;
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-emphasis);
  justify-content: center;
  padding: 0 16px;
}

/* THE ENDS OF THE RANGE SAY SO — the order rail's own treatment
 * (`.sf-flow-qty__step:disabled`), arriving with the shared stepper. A minus
 * sign that cannot go below one is a control that should not look pressable. */
.sf-pdp-qty__step:disabled {
  cursor: default;
  opacity: 0.45;
}

.sf-pdp-qty__value {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-body);
  /* Board 20, plus its 8 of padding. */
  min-width: 28px;
  /* Vertical padding retired with the field's declared height above — it was
   * the other half of what drew a 52px box. */
  padding: 0 4px;
  text-align: center;
}

/*
 * ADD TO CART IS A LINK NOW (2026-08-27) — the order-flow seam entered service,
 * and the flow's own doctrine is that a control which changes the order GOES
 * somewhere. So the rule has to dress an `<a>` as well as the `<button>` that
 * stands in when the seam declines: `display: block` (a no-op for the flex item
 * it already is, load-bearing if this box is ever laid out otherwise) and
 * `text-decoration: none`, which a button never needed. Everything else is
 * unchanged, and the box is the board's to the pixel.
 */
.sf-pdp-buy__cta {
  background: var(--brand-color-accent, #c4530a);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  /* THE PRIMARY RUNG, DECLARED (Ryan's button audit, 2026-09-01). `padding:
   * 16px 0` on the rail's inherited line box measured 52 — the tallest Add to
   * Cart on the site, and four px off the buy bar's own 48 for the SAME ACTION
   * on the SAME PAGE. The height is stated and the label centred, which is what
   * `display: flex` is for here; `display: block` could not centre it. */
  align-items: center;
  display: flex;
  flex: 1;
  font-family: inherit;
  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;
  text-decoration: none;
}

.sf-pdp-buy__assurance {
  align-items: flex-end;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  flex-direction: column;
  font-size: var(--text-small);
  gap: 7px;
  margin-top: 14px;
}

.sf-pdp-buy__warranty {
  align-items: center;
  display: inline-flex;
  gap: 6px;
}

.sf-pdp-buy__warranty-mark {
  color: var(--brand-color-faint, #9a988f);
  display: inline-flex;
}

/* ------------------------------------------- stock / install / specs ----- */

.sf-pdp-card {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 16px 22px;
}

/* The stock card is drawn a pixel tighter than the two under it. */
.sf-pdp-card--stock {
  gap: 10px;
}

/*
 * ─── A DELIBERATE TWO-LINE CARD (Ryan's ruling PA1, 2026-08-28) ─────────
 *
 * "The In Stock / Installation / Specs headers are too small — size them up, and
 * the icon scales with them. The zip forms may wrap to a second line on desktop:
 * handle that wrap CLEANLY, don't shrink the headers back."
 *
 * HE WAS RIGHT ABOUT THE WRAP, AND IT WAS ALREADY AT THE EDGE. Measured at 1440
 * BEFORE the size-up: the card's content box is 572px and the stock row needed
 * 570.0 (title 292.4 + gap 16 + controls 261.6); the install row needed 570.0
 * exactly as well. Two pixels of slack — so ANY increase in the header wraps
 * this row, at every width, and there is no "fits sometimes" state worth
 * designing for.
 *
 * SO IT IS TWO LINES ON PURPOSE, not two lines when it must be: the title takes
 * a whole line (`flex: 1 1 100%`) and the controls take the next one, left-
 * aligned on the card's own text edge, which is where the foot rows under them
 * already sit. A `flex-wrap` alone would have given the same picture by accident
 * and a different one the day a shorter product name arrived.
 *
 * 12px BETWEEN THE TWO LINES: the card's own flex gap is 11 and 10, so a row gap
 * inside one of its rows sits one step above the gaps around it — the two lines
 * read as one card rather than as two.
 */
.sf-pdp-card__row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  row-gap: 12px;
}

.sf-pdp-card__title {
  align-items: center;
  display: flex;
  /* THE WHOLE LINE (ruling PA1) — see `.sf-pdp-card__row` for the measurement
   * that made the second line deliberate rather than incidental. */
  flex: 1 1 100%;
  gap: 9px;
}

/* The specs card's title is not in a row with controls — it is the card's own
 * lead line, and `--lead` already declares `flex: none` below. */

.sf-pdp-card__title--lead {
  flex: none;
  margin-bottom: 7px;
}

.sf-pdp-card__mark {
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  flex: none;
}

/*
 * ─── THE HEADER SIZES UP (Ryan's ruling PA1, 2026-08-28) ──────────────
 *
 * 14.5 → 18px, which is `--text-card-header` — the platform's own step for
 * exactly this thing, and two rungs up the scale rather than one because the
 * complaint was that these read as captions rather than as the three headings
 * they are.
 *
 * THE NOTE DOES NOT GO WITH IT. "In stock" and "· estimated delivery 1–2 weeks"
 * are a heading and its supporting clause, and sizing both would have grown the
 * row by more than twice what the heading needed while making the clause
 * compete with the heading it supports. So the note holds at `--text-support`
 * (its own rule below), and the ruling's "size UP" lands on the words it names.
 *
 * THE MARKS SCALE WITH IT, in `pdp-rail.tsx`: 17×14 → 21×17 (truck), 15×15 →
 * 19×19 (wrench), 17×11 → 21×14 (ruler) — 18/14.5 on each drawn dimension,
 * rounded to whole pixels, so each glyph keeps its own aspect.
 */
.sf-pdp-card__heading {
  font-size: var(--text-card-header);
  font-weight: 700;
  margin: 0;
}

.sf-pdp-card__good {
  color: var(--brand-color-good, #1f7a4d);
}

/*
 * ─── THE OTHER STATUS (stock-truth seam, 2026-08-31) ──────────────────
 *
 * The card drew ONE state because the line was an authored constant. It
 * draws the product team's own `stocking` word now, and on the day this
 * landed that word was "Backordered" on two of the four systems and
 * "Unavailable" on two other products in the same file. The legacy site
 * gives everything that is not in-stock ONE warning class — it does not
 * have a third treatment and neither does this.
 *
 * `--brand-color-accent` RATHER THAN A NEW TOKEN. The brand declares no
 * warning ink, and inventing one is a palette decision that belongs to a
 * board, not to a seam. The accent is already in this exact card — it is
 * what the urgency band's bolt is drawn in, eleven pixels below — and it
 * reads as attention rather than as error, which is what a September ship
 * date is. If Ryan wants a distinct warning ink, it is this one rule.
 *
 * NO WEIGHT AND NO SIZE HERE, deliberately: `.sf-pdp-card__heading` above
 * owns both, exactly as it does for the green state, so the two statuses
 * are the same heading in two inks and cannot drift in anything else.
 */
.sf-pdp-card__warn {
  color: var(--brand-color-accent, #c4530a);
}

/* THE SUPPORTING CLAUSE HOLDS ITS SIZE (ruling PA1) — see the heading above.
 * Declared rather than inherited now that the two differ. */
.sf-pdp-card__note {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  font-weight: 400;
}

/* The spec-market card sets its audience clause one step down the scale. */
.sf-pdp-card--specs .sf-pdp-card__note {
  font-size: var(--text-small);
}

.sf-pdp-card__controls {
  align-items: center;
  display: flex;
  gap: 8px;
}

/*
 * The zip field. A real `<input>` where the board draws a styled div, so it
 * carries the control reset an input does not inherit — and `readonly`, because
 * nothing on this page can answer a zip yet (see the STUB comments in
 * `pdp-rail.tsx`).
 */
.sf-pdp-zip {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-small);
  padding: 10px 12px;
  /* THE WORKED EXAMPLE for the sheet header's content-box rule: the board's
   * authored 96 is its CONTENT box, so the border-box width is 96 + 24 of
   * padding + 2 of border. */
  width: 122px;
}

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

/* The ink-outlined secondary button: wayfinding, never commerce (tokens §3).
 *
 * THE COMPACT RUNG, DECLARED (Ryan's button audit, 2026-09-01). It was
 * `padding: 10px 18px` at 13.5/600 and measured 38 in one place and 40 in
 * another — the same class, two boxes, because the height was the padding plus
 * whatever line box it inherited. TRANSPARENT rather than `surface`: the ghost
 * is defined by showing its ground through (this is the trap `.sf-flow-ghost`
 * recorded — white-on-paper is a delta of 4 and reads as a filled button that
 * happens to match), and every ground this button lands on here is white
 * anyway, so nothing visibly changed except that it can no longer be wrong.
 *
 * ─── W200/7: AND IT DOES NOT UNDERLINE (Ryan, 2026-09-06) ───────────────────
 *
 * "'submittal package' renders UNDERLINED inside the button." It did, at both
 * rungs, and the reason is one the whole family shares: SOME OF THESE BUTTONS
 * ARE ANCHORS. "Download submittal package" navigates — to the printable spec
 * page, in a new tab — so under this storefront's standing rule (a control that
 * navigates is an `<a>`; one that does not is a `<button>`) it is correctly an
 * anchor, and an anchor carries the user agent's underline unless something
 * takes it off. Every OTHER member of this family happens to be a `<button>`,
 * which is why nothing noticed for as long as it did.
 *
 * IT IS NOT A ONE-CALLER FIX, deliberately. `.sf-pdp-quiet` next door IS an
 * underlined link and is meant to be; the difference is not the element, it is
 * whether the thing is drawn as a BOX. So the declaration goes on the box's own
 * class, where it holds for every future anchor-shaped ghost as well as for
 * this one — which is what `.sf-pdp-buy__cta`, `.sf-pdp-mbuy__cta` and
 * `.sf-pdp-buybar__cta` have all done since they were built, for the same
 * reason. The ghost family is simply the one that had never had an anchor in it
 * until W184 put one there.
 */
.sf-pdp-ghost {
  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-decoration: none;
}

/* NO VERTICAL PADDING ANY MORE: the base declares the height, and 11px of
 * padding on top of a declared 40 would draw a 62px button. The side padding
 * goes because this modifier states its own width. */
.sf-pdp-ghost--wide {
  flex: none;
  padding: 0;
  text-align: center;
  /* The board's authored 250 in ITS content-box: 250 + 2 of border. Same
   * conversion as `.sf-pdp-zip`. */
  width: 252px;
}

.sf-pdp-urgency {
  align-items: center;
  background: var(--brand-color-accent-soft, #fbefe5);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  display: flex;
  font-size: var(--text-caption);
  gap: 7px;
  justify-content: center;
  margin: 0;
  padding: 7px 10px;
}

.sf-pdp-urgency__mark {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
}

.sf-pdp-urgency__clock {
  font-family: var(--brand-font-mono, monospace);
  font-weight: 500;
}

.sf-pdp-card__foot {
  align-items: baseline;
  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  display: flex;
  font-size: var(--text-caption);
  gap: 14px;
  margin: 0;
  padding-top: 10px;
}

.sf-pdp-card__foot-label {
  color: var(--brand-color-ink-soft, #44464a);
}

.sf-pdp-card__foot-links {
  color: var(--brand-color-muted, #6c6a62);
}

.sf-pdp-card__action {
  align-items: center;
  display: flex;
  gap: 14px;
}

.sf-pdp-card__caption {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
}

/* The board draws this second caption a touch lower than the button's centre. */
.sf-pdp-card__caption--specifying {
  margin-top: 6px;
}

/* The one accent link on the page — the board leaves it in the default anchor
 * colour while every other link here is the quiet muted treatment. globals.css
 * sets `a { color: inherit }`, so it has to be said out loud.
 *
 * W184: AND THE ONE BUTTON DRAWN AS IF IT WERE. "CAD, CSI bid spec &
 * submittal package" used to be a `ChromeLink` to `"#"`; its own tail text
 * always said "— opens the spec modal", and now one exists to open
 * (`pdp-specs-modal.tsx`) — a control that does not navigate is not an anchor
 * (the storefront's standing rule), so it is a `<button>` now, sharing this
 * selector rather than a class of its own: the two are one visual treatment,
 * not two that happen to agree. */
.sf-pdp-card__caption--specifying a,
.sf-pdp-card__caption--specifying button {
  color: var(--brand-color-accent, #c4530a);
  /* The boards set `a { text-decoration: none }` globally and then underline
   * every quiet link BY HAND — so this, the one link they leave in the default
   * anchor treatment, is the one link on the page that is NOT underlined at
   * rest. The browser default underlines it, so it has to be said. It carries
   * its underline on hover and on focus instead, which is where the affordance
   * is actually needed once colour has already marked it. */
  text-decoration: none;
}

/* THE BUTTON'S OWN RESET — an `<a>` never needed one, so this is scoped to
 * `button` alone rather than folded into the shared rule above. The same four
 * declarations `.sf-pdp-rev__photos-all`/`.sf-pdp-install-video` carry for
 * their own button-that-reads-as-a-link, plus `font-size: inherit` because
 * this button sits inside running caption text rather than beside it. */
.sf-pdp-card__caption--specifying button {
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  font-size: inherit;
  padding: 0;
}

/* The hover treatment, extended to focus. It ADDS to the platform ring — which
 * this link takes at the top of the sheet — it does not replace it: a colour
 * shift and an underline are not a perimeter indicator (tokens §3). */
.sf-pdp-card__caption--specifying a:focus-visible,
.sf-pdp-card__caption--specifying a:hover,
.sf-pdp-card__caption--specifying button:focus-visible,
.sf-pdp-card__caption--specifying button:hover {
  color: var(--brand-color-accent-strong, #9c420a);
  text-decoration: underline;
}

/*
 * THE 768-1439 BAND HAS NO BOARD, and it is where this page's rows run out of
 * measure. Both boards are drawn at one width each (1440 and 390); between the
 * mobile cut and 1440 the desktop composition is simply narrower, and its rail
 * is HALF the page — 268px of card content at 768, against furniture the board
 * sized for 570.
 *
 * `tools/overflow.mjs` found it and named the offenders: at 768 the spec card's
 * captions pushed the DOCUMENT 75px sideways and the two zip rows another 44.
 *
 * EVERY RULE HERE IS BEHIND THE RUNG, and that is the lesson this block cost.
 * The first version of the fix was unconditional, on the reasoning that a flex
 * line which already fits is unaffected by being allowed to wrap or to shrink.
 * That reasoning is wrong for a row with NO SLACK: the spec card's action rows
 * come to exactly the board's 570 at 1440 (252 + 14 + 54.75 + 14 + 235.25), so
 * `flex-wrap` handed them to sub-pixel rounding (+53px of rail) and
 * `min-width: 0` let items that had been clamped at min-content shrink instead,
 * rewrapping their text (-11px of spec card). Both were caught by the pixel
 * gate, and both are the same shape: a rule that is a no-op only where there is
 * room to spare.
 *
 * 1023.98 is the site's EXISTING rung (the ladder documented in chrome.css),
 * chosen over a number of this page's own so the compositions keep changing
 * together. Everything above it is the board's drawing, untouched.
 *
 * It is a MEASURED answer to an unspecced band, not a designed one — the band
 * still wants a words-spec, the same gap the category page's hero and table
 * bands have.
 */
@media (max-width: 1023.98px) {
  .sf-pdp-card__row {
    flex-wrap: wrap;
  }

  .sf-pdp-card__controls {
    flex-wrap: wrap;
  }

  .sf-pdp-card__action {
    flex-wrap: wrap;
  }

  /* `min-width: 0` is the other half of wrapping: a flex item's automatic
   * minimum is its MIN-CONTENT, so a row of unshrinkable items overflows the
   * page before it ever considers giving way. */
  .sf-pdp-card__title,
  .sf-pdp-card__caption,
  .sf-pdp-buy__option-choices {
    min-width: 0;
  }
}

/* ================================================== phone composition ==== */

@media (max-width: 767.98px) {
  .sf-pdp__grid {
    display: none;
  }

  .sf-pdp__mobile {
    display: flex;
    flex-direction: column;
  }
}

/*
 * THE PHONE STAGE IS FULL-BLEED. The board draws it edge to edge with no radius,
 * so it takes the page's gutter back out again. `--sf-gutter` is `.sf-container`'s
 * own custom property (chrome.css), which is why this is one declaration rather
 * than a breakpoint ladder repeating the gutter's numbers.
 */
/*
 * THE PHONE STAGE IS SIZED TO THE PHOTOGRAPHS, NOT TO A NUMBER (Ryan, batch #4
 * G4, 2026-08-27: "the photos almost scale correctly … just make the section a
 * little taller and we'll get a perfect fit, right?").
 *
 * ─── WHAT HE WAS LOOKING AT ─────────────────────────────────────────────────
 *
 * Two pillars of ground down the left and right of every feature photograph.
 * The board's 430px height makes this box 390/430 = 0.9070 wide-over-tall at
 * the reference width, and the frames in it are 764x900 = 0.8489 — so a portrait
 * frame fills the height, comes up 19px short on each side, and does it on
 * EVERY frame because no frame in the mirror is 0.9070. Measured across all 93
 * rendered frames: ZERO fit this box exactly, and 50 of them show side pillars
 * averaging 19.3px a side.
 *
 * ─── THE RATIO IS MEASURED, NOT NUDGED ──────────────────────────────────────
 *
 * The aspect distribution over the 93 frames the four galleries actually draw
 * (not the 64 unique files — nine of the THOR feature photographs are shared by
 * all four systems, so a per-file count under-weights exactly the frames a
 * reader swipes through first):
 *
 *     0.849  764x900   36 frames   38.7%   <- dominant, by nearly 2x
 *     1.333  4:3       18 frames   19.4%
 *     0.750  3:4        7 frames    7.5%
 *     1.339  900x672    6 frames    6.5%
 *     1.000  square     5 frames    5.4%
 *     …eight more, one to three frames each
 *
 * `aspect-ratio: 764 / 900` states that dominant ratio as the box, which is why
 * it is a RATIO rather than the 459px it resolves to at 390: full-bleed means
 * the width is the viewport's, so a fixed height would be a perfect fit at one
 * phone size and the old defect at every other. Declared this way the fit holds
 * at 360, at 390, at 430 and on a folded screen.
 *
 * WHAT IT BUYS, COUNTED: 36 frames go to flush on BOTH axes (they were 0), and
 * the gallery's opening frame — the profile shot, 742x900 — lands within 5.7px
 * a side of flush, which is the "almost" in Ryan's sentence closed. Fourteen
 * frames are still pillared, all of them the 3:4-and-taller portraits, and they
 * are pillared by less than they were.
 *
 * WHAT IT COSTS, ALSO COUNTED: the 43 landscape frames trade side pillars for
 * deeper letterbox — a 4:3 frame drew 68.8px bands top and bottom at 430 and
 * draws 83.4px at 459.4. That is the trade the ruling makes, and the sweep
 * behind them (`--sf-pdp-stage-ground`) is what it is for. The stage grows
 * 29.4px at 390, which is 3.5% of an iPhone 14's viewport.
 */
.sf-pdp-mstage {
  /* THE SAME SWEEP AS THE DESKTOP STAGE, from the same declaration — one
   * mechanism, two boards, and a ground that differed between them would be a
   * drift nobody chose (see `--sf-pdp-stage-ground` for the recipe). */
  aspect-ratio: 764 / 900;
  background-color: var(--brand-color-well, #f1f0ec);
  background-image: var(--sf-pdp-stage-ground);
  margin-inline: calc(var(--sf-gutter, 15px) * -1);
  /* Same clip, same axis hand-off as the desktop stage — see its rule for why
   * `touch-action: pan-y` is what makes the swipe possible at all. */
  overflow: hidden;
  /* THE CAPTION CONTROL'S TWO NUMBERS, named once (Ryan's FX5, 2026-08-28).
   * The button is drawn from them, and the band RESERVES its column and its
   * height from the same two — see `.sf-pdp-mstage__band`. A reservation
   * written as its own literal is a reservation that stops matching the thing
   * it reserves for the first time the control is resized. */
  --sf-cap-inset: 12px;
  --sf-cap-size: 36px;
  /* The track, the opener, the caption control and its band, and the shared
   * position bar are all positioned against this box. */
  position: relative;
  touch-action: pan-y;
  user-select: none;
}

/*
 * ─── W200/4: THE PHONE STAGE'S GROUND IS WHITE (Ryan, 2026-09-06) ───────────
 *
 * "Product gallery photo background WHITE on mobile — keep the gray gradient on
 * desktop; it doesn't work on desktop to change it."
 *
 * WHY THE SAME SWEEP READS DIFFERENTLY AT THE TWO RUNGS, which is the part
 * worth carrying forward rather than the colour. `--sf-pdp-stage-ground` was
 * ruled for a box that is a FRAMED WELL INSIDE A COLUMN: 632x704, sitting on
 * `paper`, with a 4px radius and a 12px inset of ground on the filled axis, and
 * the whole point of the wash (see the token's own essay) is to keep that box
 * legible as a box against the page behind it. The phone stage has none of
 * that. It is FULL-BLEED — it takes the gutter back out with a negative margin
 * — so it has no edge to hold, no radius, and no page beside it; and its slide
 * carries `padding: 0`, so the dominant 764x900 frame fills it to the pixel on
 * both axes and the ground is not visible on that frame AT ALL. What is left is
 * the fourteen portraits and the forty-three landscapes, where the sweep draws
 * as a tinted band top-and-bottom of a photograph shot on white — a grey stripe
 * across the top of the page with no box around it to explain what it is.
 *
 * SCOPED TO THE COMPOSITION'S OWN CUT, not to "mobile". 767.98 is the width at
 * which `.sf-pdp__grid` goes away and `.sf-pdp__mobile` appears (the phone
 * composition block below), so this query and the one that decides which stage
 * exists are the SAME query — there is no band where a reader could get the
 * white ground under the desktop composition or the sweep under the phone one.
 * The rule is written here, immediately after the base declaration it
 * overrides, because a media query adds no specificity: source order is the
 * whole of why this wins, and putting it three hundred lines away would make
 * that invisible.
 *
 * `background-image: none` AS WELL AS THE COLOUR. Clearing only the colour
 * would leave the gradient painting over it, and clearing only the image would
 * leave `well` underneath any transparency. Both, or neither.
 */
@media (max-width: 767.98px) {
  .sf-pdp-mstage {
    background-color: var(--brand-color-surface, #ffffff);
    background-image: none;
  }
}

/*
 * NO PADDING ON THE PHONE'S SLIDE. The desktop stage insets its photograph by
 * 12px because it is a framed well inside a column; the phone stage is
 * FULL-BLEED — it takes the page's gutter back out with a negative margin — so
 * the photograph runs edge to edge the way the board draws it.
 *
 * AND SINCE G4 THAT ZERO IS LOAD-BEARING, not just faithful: the box is now the
 * dominant frame's own aspect, so a 764x900 photograph fits it to the pixel on
 * both axes — but only while nothing insets it. One px of padding here and the
 * "perfect fit" Ryan asked for is a 1px band of ground on all four sides.
 */
.sf-pdp-mstage__slide {
  padding: 0;
}

/*
 * ─── THE PHONE SACRIFICED ITS PERMANENT CAPTION (Ryan's G2 v2, 2026-08-27) ──
 *
 * `.sf-pdp-mstage__caption` IS RETIRED BY NAME. It was a centred mono line
 * absolutely positioned 30px up the stage's foot, drawn on every captioned
 * frame, ON the photograph — which is the one thing the desktop half of this
 * same ruling exists to stop ("the photo NEVER extends behind the text"). The
 * desktop answer was to grow the box downward into a band; the phone stage is
 * full-bleed with a buy rail immediately under it and has no room to grow, so
 * Ryan ruled the trade instead: no caption at rest, and a control that reveals
 * one.
 *
 * ─── AND THE REVEAL IS A BOTTOM SHEET NOW (Ryan's G2 v3, 2026-08-28) ───────
 *
 * THE THIN BAND ABOVE THE BUTTON IS RETIRED BY NAME. `.sf-pdp-mstage__band`
 * survives as an element; its whole GEOMETRY does not. What it was: a strip
 * absolutely positioned `bottom: 56px` (the control's 12px inset, its 36px
 * circle, and 8px of air), `left: 0; right: 0`, mounted into the DOM on the
 * press and unmounted on the close, growing UPWARD as a long caption wrapped.
 * Those five declarations — `bottom`, `left`, `right`, `position` and
 * `pointer-events` — are gone from it, and the strip they drew is gone with
 * them.
 *
 * WHAT RYAN RULED INSTEAD, in his words: the band "conceptually lives just
 * below the photo's bottom edge with its text ALREADY SET"; the press slides
 * "the whole band UP into position while FADING IN — text present throughout,
 * never appearing after the motion"; the circle "RIDES UP on the band's top
 * edge"; the rise is "only what the caption needs — a one-line caption rises
 * less than a two-line one"; and the position bar "stays ON TOP — the band
 * passes up BEHIND it".
 *
 * ─── HOW THE RISE MEASURES ITSELF ──────────────────────────────────────────
 *
 * "Only what the caption needs" is the whole mechanism, and it is why there is
 * a `__sheet` wrapper at all rather than one more absolutely positioned strip.
 * The sheet is anchored `bottom: 0` — the photo's own bottom edge — and its
 * height is its ONE in-flow child, the band. So:
 *
 *   AT REST   `translateY(100%)`, and 100% of a transform is 100% of the
 *             element's OWN height: the sheet drops by exactly the band's
 *             height, whatever that height is, and lands wholly below the
 *             photo's bottom edge where the stage's `overflow: hidden` clips
 *             it. One declaration serves a one-line caption and a two-line one
 *             with no measurement in JavaScript and no second number in CSS.
 *   OPEN      `translateY(0)`. The band covers the bottom of the photograph,
 *             and every pixel it rose was a pixel of caption.
 *
 * MEASURED, at 390 and at 320 (the two widths the flagship's captions differ
 * at): 14 + 16 + 26 = 56px for a one-line caption, 14 + 32 + 26 = 72px for a
 * two-line one — INTEGER LINE BOXES, which is the caption-v2 precedent (the
 * desktop band's 64 = 14 + 2x18 + 14, sized the same way and for the same
 * reason: a fractional band puts everything measured from it on half-pixels).
 * The gallery's longest caption is 38 characters and takes one line at 390 and
 * two at 320.
 *
 * ─── THE BUTTON IS IN THE BAND, AND IT IS THE BAND THAT MOVES (Ryan's FX5,
 *     2026-08-28) ──────────────────────────────────────────────────────────
 *
 * G2 v3 had the button RIDE the sheet: `__cap` was the sheet's own child at
 * `bottom: 100%` plus 12px of margin, so the sheet's single transform carried
 * it and it settled 12px above the band's top edge. Ryan looked at that on the
 * page and ruled the stronger version: "the button goes INTO the band —
 * bottom-left — with the caption text pushed over so it never runs under it."
 * That retires the ride, `bottom: 100%` and `margin-bottom: 12px` by name.
 *
 * AND IT COULD NOT BE DONE BY RE-ANCHORING THE SAME CHILD, which is the whole
 * reason this section is rewritten rather than amended. The sheet's elegance is
 * that its travel is 100% of its OWN height — a number only it knows — so a
 * child anchored inside the band at, say, `bottom: 26px` resolves at rest to
 * `26 - H`: below the photograph's foot, inside the stage's clip, gone.
 * MEASURED at 390, where H is 62: the control would have rested 36px under the
 * picture, and it is the only way to open the band it was hiding in. No
 * compensating transform can reach it either, because a child cannot name a
 * percentage of its parent's height.
 *
 * SO THE MOVEMENT WAS TAKEN OFF THE BUTTON ENTIRELY. It is the STAGE's child
 * again, at the same 12px inset it has been drawn at since it existed, and it
 * does not move in either state. The band rises AROUND it:
 *
 *   at rest  the sheet is clipped away below the foot; the button is on the
 *            photograph at 12px, exactly as before — the resting stage is
 *            byte-identical, which is what `tools/pdpgate.mjs` already records.
 *   open     the band's box runs from the foot to at least 62px up (12 + 36 +
 *            14: the button's inset, its diameter and the band's own top air),
 *            so the control that opened it is inside it, bottom-left, with the
 *            words indented past its column.
 *
 * ONE MOVING PART EITHER WAY. What was "one transform carries two elements" is
 * now "one transform carries the one element that moves", and the reservation
 * that makes the button belong to the band is arithmetic in the band's own box
 * rather than a second animation to keep in step.
 *
 * ─── THE THREE STILL DO NOT INTERLEAVE, AND THE BAR IS ON TOP ──────────────
 *
 * The arithmetic that killed the band-at-the-button's-height (the position
 * bar's phone ink — accent on line-mid, chosen for a photograph shot on white —
 * reads 2.59:1 against an 86%-ink band, below the AA floor for a graphic
 * object) is UNCHANGED by this ruling, and this is how it is honoured now that
 * the band's travel does cross the bar's height:
 *
 *   THE BAR PAINTS ON TOP. Ryan: "the band passes up BEHIND it." Both live on
 *   the content rung of the one z-index ladder (chrome.css: content 0-3), and
 *   they are ORDERED LOCALLY there — sheet 1, bar 2 — rather than by DOM
 *   accident, because the sheet's `transform` makes it a stacking context and
 *   paint order should be readable in the rules rather than derived from the
 *   markup.
 *
 *   AND THE TEXT CLEARS THE BAR BY PADDING, NOT BY LUCK. The bar's strip is
 *   14px to 18px up from the stage's foot; the band's 26px bottom padding puts
 *   the LAST line box's floor 26px up, 8px clear of the strip's top. No caption
 *   line can sit in it at any length, because the band grows by whole line
 *   boxes off its TOP edge — every extra line moves away from the bar.
 *
 * WHAT THE FOUR RULES BELOW DRAW, at the stage's foot:
 *
 *   `__progress`  the shared position bar, centred, 14px up, on top. It
 *                 replaced the dot rail (see below) and it is
 *                 `media-progress.tsx`'s element — this rule places it, re-inks
 *                 it and rungs it, and declares nothing about its geometry.
 *   `__sheet`     the reveal itself: the band's box, and the transform that is
 *                 the whole motion.
 *   `__cap`       a 36px circle at the bottom-LEFT, drawn only for a frame that
 *                 has a caption, positioned against the STAGE and still in both
 *                 states. Bottom-left is Ryan's placement and it is clear of the
 *                 counter's corner by construction — the counter is bottom-RIGHT
 *                 on the desktop stage and this composition has never drawn one
 *                 at all.
 *   `__band`      the very dark tinted band the control reveals, in flow inside
 *                 the sheet, which is what gives the sheet its height.
 */

/*
 * THE BAND IS INK AT 86%, AND THE NUMBER IS THE CONTRAST (Ryan: "a very dark
 * tinted band … with white text"; the on-media ink/halo pair is the discipline
 * it is derived from).
 *
 * A tint over a photograph has to be legible over the WORST photograph, and the
 * worst case here is the one this gallery is full of: a hoop shot on pure
 * white. `ink` #17181A at 86% over #FFFFFF composites to ≈#333436, and surface
 * white on that is 11.2:1 — which is why 86 rather than the 55–65 a "scrim"
 * usually means. Over a dark shot it only gets better, and it is never fully
 * opaque, so the photograph is still visible through its own caption.
 *
 * IT IS NOT A LIVE REGION AND IT IS NOT `aria-hidden` — see `pdp-gallery.tsx`.
 * `pointer-events: none` so a tap on the band still reaches the stage's opener
 * under it: the band is a label, and the box it lies on opens the viewer.
 */
/*
 * THE SHEET — the box the ruling's motion happens to, and nothing else. It
 * draws no ink of its own: it is the band's anchor and the transform's owner.
 * Since FX5 it is not the button's containing block; the button is the stage's.
 *
 * `pointer-events: none` because the band is a LABEL and the box under it opens
 * the viewer — a tap that lands on the caption must reach the stage's opener,
 * exactly as it did when the band carried this declaration itself. The button
 * is no longer inside it, so it no longer has to take its presses back.
 */
.sf-pdp-mstage__sheet {
  bottom: 0;
  left: 0;
  pointer-events: none;
  position: absolute;
  right: 0;
  /* SEE THE ESSAY ABOVE: 100% of the sheet's own height is the band's height,
   * so the rise measures itself and a one-line caption rises less than a
   * two-line one without a number being written anywhere. */
  transform: translateY(100%);
  /* The popup rung is for popups; this is page content lying on a photograph.
   * Content 0-3 (chrome.css's ladder), and the bar above it takes 2. */
  z-index: 1;
}

.sf-pdp-mstage__sheet[data-open="yes"] {
  transform: translateY(0);
}

/*
 * THE MOTION, AND IT IS THE VIEWER'S OWN. 220ms on
 * `cubic-bezier(0.22, 0.61, 0.36, 1)` is the declaration the media viewer's
 * phone info TRAY already draws (lightbox.css, the panel rule — named in prose
 * rather than by class because that sheet's prefix is asserted to appear in no
 * other sheet at all). Same gesture, same kind of surface, one component away:
 * transplanted rather than invented, so the site has ONE bottom-sheet motion.
 *
 * REDUCED MOTION IS THE ABSENCE OF THIS RULE, not an override of it (the same
 * shape the tray uses): under `reduce` the transform still lands, it simply
 * lands in one paint — "instant appear/disappear both ways, no slide", which is
 * the ruling. It also means the auto-dismiss on a frame change needs NO branch
 * in `pdp-gallery.tsx`: closing is the same state flip either way, and whether
 * it animates is this media query's business.
 *
 * THE BAND FADES ON THE SAME CLOCK, and the shared duration is the point: the
 * ruling says the text is "present throughout, never appearing after the
 * motion". One duration and one curve make the band and its words ONE object
 * arriving — and, on the way out, stop the band from sliding away empty, which
 * is what a shorter fade would draw.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-pdp-mstage__sheet {
    transition: transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  .sf-pdp-mstage__band {
    transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
}

.sf-pdp-mstage__band {
  background: color-mix(in srgb, var(--brand-color-ink, #17181a) 86%, transparent);
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  line-height: 16px;
  margin: 0;
  /*
   * ─── IT RESERVES THE BUTTON'S ROOM (Ryan's FX5, 2026-08-28) ──────────────
   *
   * "The button sits WITHIN the caption band, bottom-left, and the caption text
   * is indented to reserve the button's column — text never runs under it."
   * Both halves are the SAME two numbers the control is drawn from, named once
   * at `.sf-pdp-mstage` and consumed here, so the reservation cannot drift from
   * the thing it is reserving for.
   *
   * THE COLUMN — `inset + size + inset`: the button's own left inset, its
   * diameter, and the same inset again as the air between it and the first
   * glyph. 12 + 36 + 12 = 60px of left padding, and the text starts after it.
   * A text that merely avoided the button by luck would run under it the first
   * time a caption got long enough to wrap; this one cannot, at any length.
   *
   * THE FLOOR — `inset + size + 14`: the band has to be tall enough to CONTAIN
   * the control that opens it, which is 12px up from the stage's foot and 36px
   * tall, plus this band's own 14px of top air over it. 62px. It is a
   * `min-height` rather than a height because the caption still sets the box
   * whenever it needs more: MEASURED at 390 and 320, a one-line caption's own
   * box is 56 and a two-line one's is 72, so the floor binds the first and the
   * second binds itself.
   *
   * WHAT THAT COSTS, recorded rather than glossed: the G2 v3 ruling's "a
   * one-line caption rises less than a two-line one" now holds from two lines
   * up rather than from one — at one line the rise is the button's floor, 62,
   * instead of 56. The rise is still MEASURED from the box rather than
   * declared, which is the mechanism that ruling was actually about.
   *
   * 14 ABOVE, 26 BELOW, and only the second looks arbitrary: the bar's strip is
   * 14-18px up from the stage's foot and the band's floor IS the stage's foot,
   * so 26 puts the last line box 8px clear of the strip's top. The text clears
   * the bar by padding, which is Ryan's "not luck". At the 62px floor the last
   * line box sits higher still (MEASURED: 32px up), so the clearance is a floor
   * and not a coincidence.
   */
  min-height: calc(var(--sf-cap-inset) + var(--sf-cap-size) + 14px);
  opacity: 0;
  padding: 14px 16px 26px calc(var(--sf-cap-inset) * 2 + var(--sf-cap-size));
  text-transform: uppercase;
}

.sf-pdp-mstage__sheet[data-open="yes"] .sf-pdp-mstage__band {
  opacity: 1;
}

/*
 * THE CONTROL. A 36px circle rather than the platform's 44 floor: it sits ON a
 * photograph and a 44px disc at this corner is a chrome element the size of the
 * counter pill — the same call the viewer's own 40px nav discs make. Its TOUCH
 * TARGET is not its box, because nothing else is drawn within 12px of it in any
 * direction.
 *
 * WHITE ON INK AT 70%, which is the band's own recipe one step lighter: the
 * button is drawn over the raw photograph at rest, where it needs the same
 * unknown-ground treatment the band gets, and it must still read as a control
 * rather than as a hole in the picture.
 */
.sf-pdp-mstage__cap {
  align-items: center;
  appearance: none;
  background: color-mix(in srgb, var(--brand-color-ink, #17181a) 70%, transparent);
  border: 0;
  border-radius: 50%;
  /* ─── `bottom: 100%` AND `margin-bottom: 12px` ARE RETIRED BY NAME (Ryan's
   * FX5, 2026-08-28) ────────────────────────────────────────────────────────
   *
   * They were G2 v3's ride: the button was the sheet's child, anchored to the
   * band's top edge, so one transform carried both. Ryan's FX5 puts the button
   * INSIDE the band instead, and that is not an anchor this element can take —
   * a child sitting at `bottom: 26px` inside a sheet that rests at
   * `translateY(100%)` lands BELOW the photograph's own foot, clipped away,
   * with no way to open the band it is in (MEASURED at 390: the band's box is
   * 62px, so the control would have rested 36px under the picture). The
   * compensation would have to name the sheet's height, and the sheet's height
   * is the one number nothing but the sheet knows.
   *
   * SO IT IS THE STAGE'S CHILD AGAIN, at the 12px inset it has had since the
   * control was drawn, and it does not move in either state — the BAND rises
   * around it. The rest geometry is therefore byte-identical to what
   * `tools/pdpgate.mjs` already records, which is the property that let this
   * change be made without a board re-record.
   *
   * ABOVE THE BAND IT SITS IN. Content rung 3 on chrome.css's one ladder
   * (0-3), over the sheet's 1 and the position bar's 2: the band must not paint
   * over its own control, and where the button and the bar's strip do not
   * overlap at all the order between them is stated rather than left to two
   * elements' DOM positions. */
  bottom: var(--sf-cap-inset);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: flex;
  height: var(--sf-cap-size);
  justify-content: center;
  left: var(--sf-cap-inset);
  padding: 0;
  position: absolute;
  width: var(--sf-cap-size);
  z-index: 3;
}

/*
 * ─── THE DOT RAIL IS RETIRED (Ryan's ruling G5 v2, 2026-08-27) ──────────────
 *
 * `.sf-pdp-mstage__dots`, `.sf-pdp-mstage__dot`, `.sf-pdp-mstage__dot--on` and
 * `.sf-pdp-mstage__dot--edge` are gone by name. Seven dots windowed over
 * twenty-four frames slide only far enough to keep the lit one inside the
 * window, so swipes eight to twenty drew an IDENTICAL rail — Ryan read the dead
 * zone off the live page and ruled it bad feedback at this count.
 *
 * What sits here now is the site's ONE position bar, shared with the media
 * viewer and drawn by `media-progress.tsx`. This rule owns two things about it
 * and nothing else:
 *
 *   WHERE IT SITS — centred, 14px up, exactly where the rail's own baseline was
 *   so the stage's foot is unchanged.
 *
 *   WHAT COLOUR IT IS. The shared chrome's ruled ink is white-on-white-at-34%,
 *   which is right on the viewer's near-black scrim and INVISIBLE here: this
 *   bar lies on a product photograph shot on white. The two colours are read by
 *   the shared rules as inheritable custom properties for exactly this case, so
 *   the stage re-inks them with the pair its own dots used — `line-mid` track,
 *   `accent` fill. Measured: accent #C4530A on white is 4.59:1, well past the 3
 *   a graphic object owes, and the track reads against both the photograph and
 *   the fill. SAME COMPONENT, SAME GEOMETRY, SAME TREATMENT, TWO INKS — flagged
 *   as the one place "indistinguishable everywhere" bends, because the two
 *   grounds are two grounds.
 */
.sf-pdp-mstage {
  --sf-mprog-track: var(--brand-color-line-mid, #d8d6d0);
  --sf-mprog-ink: var(--brand-color-accent, #c4530a);
}

.sf-pdp-mstage__progress {
  bottom: 14px;
  left: 50%;
  position: absolute;
  transform: translateX(-50%);
  /* ON TOP OF THE SHEET (Ryan's G2 v3): "the progress bar stays ON TOP — the
   * band passes up BEHIND it." Both are content-rung numbers on chrome.css's
   * one ladder (0-3), ordered locally against each other rather than left to
   * DOM order, because the sheet's transform makes it a stacking context and
   * paint order should be readable in the rules rather than derived from the
   * markup. */
  z-index: 2;
}

.sf-pdp-mrail {
  display: flex;
  flex-direction: column;
  gap: 18px;
  /*
   * The board writes `26px 18px 0` — no bottom padding, because the sticky buy
   * bar it draws below sits on the section's own edge. That bar is NOT built
   * this pass (rule zero: it is a motion, and motion gets specced in words
   * first), so without a floor the last card would sit flush against the
   * footer. 48px is the DESKTOP column's own bottom padding, borrowed so the
   * two compositions end the same distance from the chrome. It comes back out
   * the day the bar lands.
   */
  padding: 26px 0 48px;
}

.sf-pdp-mrail__head {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

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

.sf-pdp-mrail__name {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-heading, system-ui, sans-serif);
  font-size: var(--text-section);
  font-weight: 700;
  letter-spacing: normal;
  line-height: 1.1;
  margin: 0;
  /* THE HEADING RESET, UNDONE. This line became the page's `<h1>` when the
   * title band dropped its title (Ryan, 2026-08-26), and globals.css gives
   * every `h1` `text-wrap: balance` — which re-breaks a wrapped product name
   * to even out its lines and moved this region 14% off the board. The element
   * changed; the drawing did not. `wrap` is the correct spelling of "normal"
   * for this property (chrome.css:2000 already knew; `text-wrap: normal` is
   * invalid and silently dropped). */
  text-wrap: wrap;
}

.sf-pdp-mrail__rating {
  align-items: center;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-small);
  gap: 8px;
  margin: 0;
}

.sf-pdp-mrail__star {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
}

.sf-pdp-mrail__score {
  font-weight: 500;
}

.sf-pdp-mrail__pipe {
  color: var(--brand-color-line-mid, #d8d6d0);
}

.sf-pdp-mrail__provenance {
  color: var(--brand-color-muted, #6c6a62);
}

.sf-pdp-mrail__lede {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  line-height: 1.55;
  margin: 0;
}

.sf-pdp-mspecs {
  display: flex;
  flex-direction: column;
  font-size: var(--text-caption);
  gap: 9px;
}

.sf-pdp-mspecs__row {
  display: flex;
  gap: 10px;
}

.sf-pdp-mspecs__label {
  color: var(--brand-color-ink, #17181a);
  flex: none;
  font-weight: 700;
  width: 84px;
}

.sf-pdp-mspecs__value {
  flex: 1;
  margin: 0;
}

.sf-pdp-mspecs__note {
  color: var(--brand-color-muted, #6c6a62);
}

/* ---------------------------------------------------- phone buy card ----- */

.sf-pdp-mbuy {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 16px;
}

.sf-pdp-mbuy__lines {
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-caption);
  gap: 10px;
}

.sf-pdp-mbuy__line {
  display: flex;
  justify-content: space-between;
}

.sf-pdp-mbuy__line:first-child .sf-pdp-mbuy__line-label {
  color: var(--brand-color-ink, #17181a);
  font-weight: 600;
}

.sf-pdp-mbuy__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-weight: 600;
  gap: 5px;
  padding: 3px 8px;
}

.sf-pdp-mbuy__badge-mark {
  display: inline-flex;
}

.sf-pdp-mbuy__amount {
  font-family: var(--brand-font-mono, monospace);
}

.sf-pdp-mbuy__amount--ink {
  color: var(--brand-color-ink, #17181a);
}

/* The phone board draws the sale figure at the regular weight; the desktop one
 * sets it at 500. Both ship as drawn. */
.sf-pdp-mbuy__amount--sale {
  color: var(--brand-color-accent, #c4530a);
}

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

.sf-pdp-mbuy__options {
  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 9px;
}

.sf-pdp-mbuy__options-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: 4px;
}

.sf-pdp-mbuy__option {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sf-pdp-mbuy__option-head {
  align-items: baseline;
  display: flex;
  font-size: var(--text-caption);
  justify-content: space-between;
}

.sf-pdp-mbuy__option-label {
  color: var(--brand-color-ink-soft, #44464a);
}

.sf-pdp-mbuy__option-choices {
  display: flex;
  font-size: var(--text-caption);
  gap: 6px;
}

.sf-pdp-mchip {
  background: none;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: inherit;
  cursor: pointer;
  flex: 1;
  font: inherit;
  padding: 8px 0;
  text-align: center;
}

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

/*
 * THE PHONE CHIP'S PRICE IS NOT DRESSED, AND THAT IS THE BOARD SPEAKING.
 * MobileA.dc.html writes the whole chip as one span — "Rust Armor +$129", body
 * face, chip size, no mono and no step down — so the figure inherits everything
 * and this rule declares only what composing it out of DATA rather than out of
 * one authored string could otherwise cost: a line break between the name and
 * the price. The rendered row is the board's, glyph for glyph.
 *
 * The desktop twin above IS dressed, because no board draws a desktop chip
 * price at all — see its note. Boards win where a board exists; the order
 * rail's drawing wins where none does.
 */
.sf-pdp-mchip__delta {
  white-space: nowrap;
}

.sf-pdp-mbuy__total {
  align-items: center;
  border-top: var(--border-money);
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  padding-top: 9px;
}

.sf-pdp-mbuy__total-label {
  font-size: var(--text-small);
  font-weight: 700;
}

.sf-pdp-mbuy__total-money {
  align-items: center;
  display: flex;
  gap: 8px;
}

.sf-pdp-mbuy__strike {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  text-decoration: line-through;
}

/*
 * 24px is THE MONO MONEY MOMENT — tokens §2 lists it as a deliberate exemption
 * from the type scale, which is exactly what the phone board draws here.
 */
.sf-pdp-mbuy__price {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: 24px;
  font-weight: 700;
}

/*
 * THE PHONE CARD'S LINE-ITEM CHECKBOX — the desktop rule at this card's scale.
 *
 * SAME RULING, NEW PLACEMENT (Ryan's WP6, 2026-08-29): the block that stood
 * under the money moment is RETIRED BY NAME — `.sf-pdp-mbuy__items`,
 * `.sf-pdp-mbuy__items-eyebrow`, and MP1's group inside it
 * (`.sf-pdp-mitem__main`, `__photo`, `__body`, `__chips`), after MP1 had
 * already retired `.sf-pdp-mitem__price`, `__toggle` and `--on`. The checkbox
 * draws inside `CHOOSE YOUR OPTIONS`, under the chip row that gates it.
 *
 * NO INDENT ON THIS COMPOSITION, and that is the phone option row speaking:
 * `.sf-pdp-mbuy__option` is a COLUMN — its label sits on a head line above
 * full-width chips — so there is no label column for the checkbox to clear.
 * Aligning it to the card's own left edge is aligning it to the chips.
 *
 * THE 44px TOUCH FLOOR IS ON THE LABEL, as on the desktop card. It did not
 * come from the retired chips: the phone chips are 33px (found by
 * `tools/pdporder.mjs` in the MP1 lane and still pending Ryan), so this
 * control's floor is declared on the thing a thumb actually hits.
 *
 * A RECORDED DEVIATION FROM MobileA.dc.html, which draws no drill anywhere.
 */
.sf-pdp-mitem {
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-caption);
  gap: 2px;
}

.sf-pdp-mitem__control {
  align-items: center;
  cursor: pointer;
  display: flex;
  gap: 10px;
  min-height: 44px;
}

/* The drawn box, at the phone card's own scale: 18px against the desktop 20,
 * the way every control on this composition steps down. */
.sf-pdp-mitem__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;
}

/* THE REAL CONTROL, CLIPPED INSIDE THE DRAWN BOX — not `display: none`, which
 * would take it out of the tab order, and `position: absolute` inside a
 * `position: relative` box so it cannot take the PAGE as its containing block
 * and extend the document's overflow (the playbook's sr-span rule).
 *
 * `outline: none` IS UNCONDITIONAL, not on `:focus-visible`, which is this
 * sheet's own convention for the shape (`.sf-pdp-rev__field-input` and
 * `.sf-pdp-ans__field-input` both do it): the ring is moved to the drawn box in
 * the focus block at the top of this sheet, and a `:focus-visible` suppression
 * would read as a rule fighting the ring rather than delegating it. */
.sf-pdp-mitem__box {
  height: 100%;
  inset: 0;
  margin: 0;
  opacity: 0;
  outline: none;
  position: absolute;
  width: 100%;
}

.sf-pdp-mitem__box-mark > svg {
  visibility: hidden;
}

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

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

.sf-pdp-mitem__label {
  font-weight: 500;
}

/* The recovered sentence, at this card's quietest rung — the block already
 * sets caption, so this only takes the ink and the measure. */
.sf-pdp-mitem__note {
  color: var(--brand-color-muted, #6c6a62);
  line-height: 1.5;
  margin: 0 0 2px;
}

/*
 * THE PHONE QUANTITY LINE — Ryan's ruling, 2026-08-27: "keep it in parity with
 * desktop." MobileA.dc.html draws no stepper, so this is a RECORDED DEVIATION
 * and `mbuy` re-records with it.
 *
 * ITS OWN LINE rather than the CTA's row, because the board draws that CTA
 * full-bleed and a stepper beside it would narrow the one element the board
 * commits to the full measure. The line wears the phone option row's head
 * vocabulary — label at the left, control at the right, baseline aligned.
 */
/*
 * ─── `.sf-pdp-mbuy__qty-row` AND `__qty-label` ARE RETIRED BY NAME (Ryan's
 *     FX7, 2026-08-28) ──────────────────────────────────────────────────────
 *
 * They were the stepper's own labelled line above the CTA — a `space-between`
 * flex row with the word at the left and the control at the right, 10px above
 * the button. Ryan: "the mobile quantity stepper goes INLINE with the
 * Add-to-Cart button, like desktop." The line is gone and the word with it; the
 * control's name is its `aria-label` now, which is what the desktop row it is
 * copying has always used.
 *
 * WHAT REPLACES THEM is the desktop row's three declarations at this
 * composition's sizing — see `.sf-pdp-mbuy__actions` below, beside the CTA it
 * shares a line with.
 */

/*
 * THE ACTIONS ROW — `.sf-pdp-buy__actions` (line 1640) in the phone card's box.
 * Same three declarations, same order: the stepper takes its own width and the
 * CTA takes the rest.
 *
 * THE 12px MARGIN IS THE CTA'S OWN, MOVED UP HERE. The button carried
 * `margin-top: 12px` when it was the block itself; a margin on a flex child is
 * height that child does not get under any alignment, and here it would also be
 * measured from the wrong box. The distance is unchanged.
 */
.sf-pdp-mbuy__actions {
  display: flex;
  gap: 12px;
  margin-top: 12px;
}

/* The desktop field's own declaration — surface fill, control border, control
 * radius — at the phone's touch sizing. */
/* The field matches the button beside it (Ryan's W13) — the phone twin of the
 * `.sf-pdp-qty` note. Its 44px steppers already cleared the tap floor; what it
 * lacked was the CONTROL height, so the row could settle anywhere between 44 and
 * the CTA's 48 depending on which sibling won the stretch. */
.sf-pdp-mqty {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: inline-flex;
  min-height: var(--control-height, 48px);
}

/*
 * 44px SQUARE, WHICH IS THE POINT OF THE RULING. The desktop step is
 * `padding: 15px 16px` around a 16px glyph — comfortably past 44 with a mouse
 * and irrelevant to a thumb. Declaring the floor rather than deriving it from
 * padding is what makes it hold when the type scale moves.
 */
.sf-pdp-mqty__step {
  align-items: center;
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-emphasis);
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0;
}

/* The ends of the range say so — the desktop stepper's own 0.45. */
.sf-pdp-mqty__step:disabled {
  cursor: default;
  opacity: 0.45;
}

.sf-pdp-mqty__value {
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-body);
  min-width: 28px;
  text-align: center;
}

/* The phone CTA is a link for the same reason its desktop twin is — see there. */
.sf-pdp-mbuy__cta {
  background: var(--brand-color-accent, #c4530a);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  /* `flex`, NOT `block`, since the rung was declared (2026-09-01): the label has
   * to be centred in a box whose height no longer comes from its padding. It is
   * still a flex CHILD of the row below either way — the display value here
   * governs its own inside, not how the row lays it out. */
  display: flex;
  /* `width: 100%` AND `margin-top: 12px` ARE RETIRED BY NAME (Ryan's FX7,
   * 2026-08-28). The button was the block; it is a flex child of
   * `.sf-pdp-mbuy__actions` now, so it takes what the stepper leaves — "the CTA
   * gives up width" is the ruling in Ryan's own terms — and the row above owns
   * the distance. A `width: 100%` left here would have fought `flex: 1` for the
   * same box, and a margin would have shortened the button under the row's
   * default stretch. */
  /* The primary rung, declared — the phone twin of the change above, and the
   * same reasoning. It drew 49 at 15px type; its desktop twin drew 52 at 16.
   * One action, one button. */
  align-items: center;
  flex: 1;
  font-family: inherit;
  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;
  text-decoration: none;
}

.sf-pdp-mbuy__assurance {
  align-items: flex-end;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  flex-direction: column;
  font-size: var(--text-caption);
  gap: 6px;
  margin-top: 10px;
}

.sf-pdp-mbuy__warranty {
  align-items: center;
  display: inline-flex;
  gap: 5px;
}

.sf-pdp-mbuy__warranty-mark {
  color: var(--brand-color-faint, #9a988f);
  display: inline-flex;
}

/* ------------------------------------------------------- phone cards ----- */

.sf-pdp-mcard {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 14px;
}

.sf-pdp-mcard--install {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-caption);
  gap: 12px;
}

/*
 * ─── W200/7: THE OTHER HALF OF "SQUISHED TOGETHER" (Ryan, 2026-09-06) ───────
 *
 * 9px is the board's own number and it was right for what the board drew: a
 * heading and two rows of 12.5px type, where the tallest thing in the card was
 * a 39px box. This is the ONE card on the phone rail with two stacked ACTION
 * rows, and since BTN-UNIFY both of those rows are a declared 40px control —
 * so the space BETWEEN the two buttons (9) was smaller than the space from
 * either button to the card's own edge (12). A card whose contents crowd each
 * other more than they crowd its border reads as squeezed however tidy the
 * numbers are, which is exactly what Ryan named.
 *
 * 12 IS THE CARD'S OWN BLOCK PADDING, not a new taste: the rule is that the
 * inner rhythm may not be tighter than the frame's, so the number is read off
 * the declaration on the line below it rather than chosen. It lifts the
 * heading-to-first-row space by the same 3px, which is the same statement.
 *
 * THE DESKTOP CARD IS DELIBERATELY NOT TOUCHED — flagged for Ryan. Its gap is
 * 11 against a 16 block padding, so the same reading applies in principle; but
 * its buttons are 252 wide with a three-line caption block beside them, the
 * defect is not visible at that rung, and he walked this on a phone. It is
 * `.sf-pdp-card--specs { gap: 16px }` if he wants it.
 */
.sf-pdp-mcard--specs {
  gap: 12px;
  padding: 12px 14px;
}

.sf-pdp-mcard__status {
  align-items: center;
  display: flex;
  font-size: var(--text-small);
  gap: 8px;
  margin: 0;
}

.sf-pdp-mcard--install .sf-pdp-mcard__status,
.sf-pdp-mcard--specs .sf-pdp-mcard__status {
  gap: 7px;
}

.sf-pdp-mcard__status--lead {
  margin-bottom: 5px;
}

.sf-pdp-mcard__mark {
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  flex: none;
}

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

/*
 * THE STOCK HEADING IS A HEADING **AND** GREEN (Ryan's WP7, 2026-08-29).
 *
 * PA1 raised the phone cards' headings to `--text-emphasis` on
 * `.sf-pdp-mcard__heading`; the stock card's word wore `.sf-pdp-mcard__good`
 * alone, so it kept `.sf-pdp-mcard__status`'s 13.5 while installation and
 * specs went to 16. It wears both classes now — and the two rules disagree
 * about COLOUR, since `__heading` sets ink and `__good` sets green.
 *
 * RESOLVED BY SPECIFICITY, NOT BY SOURCE ORDER. A single-class pair would be
 * settled by whichever rule happens to be later in this file, which is a
 * decision nobody can see and a reordering can silently reverse. One compound
 * selector says which fact wins, in the one place a reader would look.
 */
.sf-pdp-mcard__heading.sf-pdp-mcard__good {
  color: var(--brand-color-good, #1f7a4d);
}

/*
 * THE PHONE CARD'S OTHER STATUS — the desktop rule's twin; the rationale
 * for the accent is written there. Compound for the same reason WP7's
 * green one is: `__heading` sets an ink too, and which fact wins must be
 * a decision a reader can see rather than a source order.
 */
.sf-pdp-mcard__warn {
  color: var(--brand-color-accent, #c4530a);
  font-weight: 700;
}

.sf-pdp-mcard__heading.sf-pdp-mcard__warn {
  color: var(--brand-color-accent, #c4530a);
}

.sf-pdp-mcard__note {
  color: var(--brand-color-ink-soft, #44464a);
  font-weight: 400;
}

.sf-pdp-mcard--specs .sf-pdp-mcard__note {
  font-size: var(--text-caption);
}

/*
 * THE PHONE'S HALF OF RULING PA1 (Ryan, 2026-08-28), at the phone's own step:
 * 13.5 → 16px (`--text-emphasis`), where the desktop card goes 14.5 → 18. Both
 * are two rungs up their own scale, which is what makes the two compositions the
 * same ruling rather than the same number.
 *
 * "Mobile already wraps and that's fine" — his words, so nothing here forces a
 * line: the phone card's controls are already a separate bordered row under the
 * heading, so there was no row to break in the first place.
 *
 * THE MARKS SCALE WITH IT, in `pdp-rail-mobile.tsx`: 15×12 → 18×15 (truck),
 * 13×13 → 16×16 (wrench), 15×10 → 18×12 (ruler) — 16/13.5 on each drawn
 * dimension, rounded to whole pixels.
 */
.sf-pdp-mcard__heading {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-emphasis);
  font-weight: 700;
}

/* THE SUPPORTING CLAUSE HOLDS ITS SIZE (ruling PA1), the same split the desktop
 * card makes — declared rather than inherited now that the two differ. The specs
 * card's own smaller note is one rule above and still wins on order. */
.sf-pdp-mcard__note {
  font-size: var(--text-small);
}

.sf-pdp-mcard__controls {
  align-items: center;
  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  display: flex;
  gap: 8px;
  padding-top: 8px;
}

/* The install card's zip row is the card's own second line, not a footer under
 * a rule — the board draws no divider above it. */
.sf-pdp-mcard--install .sf-pdp-mcard__controls {
  border-top: 0;
  padding-top: 0;
}

.sf-pdp-mzip {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  flex: 1;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  /* `min-width: 0` so the flexible field can be narrower than its own
   * min-content and the row cannot push the document sideways at 320. */
  min-width: 0;
  padding: 10px 12px;
}

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

.sf-pdp-mzip--fixed {
  flex: none;
  padding: 12px 10px;
  /* Board 88, plus 20 of padding and 2 of border. */
  width: 110px;
}

/* The phone ghost — the compact rung, declared, for the reasons its desktop twin
 * `.sf-pdp-ghost` carries. This one was the worst offender in the audit: 12.5/600
 * with `padding: 10px 14px` measured 37, 41 AND 42 across three surfaces, and
 * the `--grow` modifier below moved its type as well as its box. */
.sf-pdp-mghost {
  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 14px;
  /* W200/7 — the phone twin of the underline fix. `.sf-pdp-mbuy__cta` and
   * `.sf-pdp-buybar__cta` are the precedent: both are anchors drawn as buttons
   * and both have carried this declaration since they were built. The ghosts
   * were the family that had never had an anchor in it until W184 put one
   * there, which is exactly how long the omission survived. */
  text-decoration: none;
}

/* THE MODIFIERS SET WIDTH, NOT SIZE (2026-09-01). `--grow` used to also cut the
 * type to 13.5 and re-pad the box; a modifier that changes what rung a button is
 * on is how one class came to draw three heights. */
.sf-pdp-mghost--grow {
  flex: 1;
  /* Same reason as the field beside it: a flex item's automatic minimum is its
   * min-content, and a nowrap label would floor the row above 320. */
  min-width: 0;
  padding: 0;
  text-align: center;
}

/*
 * ─── W200/7: THE BOX HOLDS THE RUNG'S WORDS ON ONE LINE (Ryan, 2026-09-06) ──
 *
 * Ryan: the two specs controls are "squished together". Half of that is the
 * spacing between them (see `.sf-pdp-mcard--specs` below); this is the other
 * half, and it is BTN-UNIFY's own arithmetic catching up with a board number.
 *
 * The board drew this box 168 wide for a 12.5px/600 label, and both labels fit.
 * BTN-UNIFY put every compact control on the rung's 14.5/600 — correctly; that
 * is the standard and it is not moving — and MEASURED at 14.5/600 in Schibsted
 * Grotesk the two labels need 164.5 and 171.8 of ink. So "View full
 * specifications" still fit by 3.5px and "Download submittal pkg" MISSED BY
 * 3.75, wrapped to two lines, and drew 34px of text inside a 40px box with 3px
 * of air above and below it. That is the squeeze, and it is why only one of the
 * two buttons looked wrong.
 *
 * 190 = 172 (the longer label, rounded up) + 8 a side + 2 of border. The eight
 * is a real inner margin rather than the 1.9px the old box had left over: a
 * word that ENDS 2px from an ink border reads as overflow even when it fits.
 * `min-height` still resolves the height, so the rung is untouched and btngate
 * reads 40 either way — this pass changes the box's width and nothing else
 * about what a button is.
 *
 * IT IS DERIVED, NOT CHOSEN, which is the part to keep: the number answers to
 * the longest label at the rung's type. A future label longer than "Download
 * submittal pkg" re-derives it; a future type rung re-derives it. Both would
 * have re-derived the board's 168 too, if anyone had known that is what it was.
 */
.sf-pdp-mghost--wide {
  flex: none;
  padding: 0;
  text-align: center;
  width: 190px;
}

.sf-pdp-murgency {
  align-items: center;
  background: var(--brand-color-accent-soft, #fbefe5);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  display: flex;
  font-size: var(--text-caption);
  gap: 6px;
  justify-content: center;
  margin: 0;
  padding: 7px 9px;
}

.sf-pdp-murgency__mark {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
}

.sf-pdp-murgency__clock {
  font-family: var(--brand-font-mono, monospace);
  font-weight: 500;
}

.sf-pdp-mcard__foot {
  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  display: flex;
  flex-direction: column;
  font-size: var(--text-caption);
  gap: 5px;
  margin-top: 4px;
  padding-top: 12px;
}

.sf-pdp-mcard__foot-label {
  color: var(--brand-color-ink-soft, #44464a);
}

.sf-pdp-mcard__foot-links {
  color: var(--brand-color-muted, #6c6a62);
}

.sf-pdp-mcard__action {
  align-items: center;
  display: flex;
  gap: 10px;
}

.sf-pdp-mcard__caption {
  color: var(--brand-color-muted, #6c6a62);
  flex: 1;
  font-size: var(--text-micro);
}

/*
 * THE EYEBROW'S CATEGORY LINK (Ryan, 2026-08-26) — a real anchor drawn as
 * NOTHING.
 *
 * With no title band on a product page, this is the way back up the catalog the
 * breadcrumb used to be, and the ruling is that it costs the board zero pixels:
 * no underline, no colour, no weight of its own. Everything is inherited from
 * the eyebrow around it, so the row rasterises exactly as `pdpgate`'s `head`
 * crop recorded it.
 *
 * IT IS NOT INVISIBLE TO EVERYONE. It is a link in the accessibility tree with
 * the category's own name as its text, it takes focus in order, and it shows a
 * focus ring — which is where the affordance is actually needed. A reader with
 * a mouse discovers it on hover, where the accent is the only concession.
 */
.sf-pdp-rail__eyebrow-link,
.sf-pdp-mrail__eyebrow-link {
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-decoration: none;
}

.sf-pdp-rail__eyebrow-link:hover,
.sf-pdp-mrail__eyebrow-link:hover {
  color: var(--brand-color-accent-strong, #9c420a);
}

/* Focus ring: see "the focus ring" at the top of this sheet, which carries
 * every control on this page and the ground each one's ring lands on. */

/* -------------------------------------------------------- the buy bar ----- */


/*
 * THE BUY BAR — Ryan's words-spec, 2026-08-26. `pdp-buy-bar.tsx`'s header
 * carries the behaviour; this is its box.
 *
 * PLACEMENT: TOP AT BOTH WIDTHS (Ryan's ruling 8, 2026-08-27), which reverses
 * the phone half of the earlier proposal and of MobileA.dc.html's own drawing
 * ("STICKY BUY BAR — PINNED TO VIEWPORT BOTTOM ON SCROLL"). His words: "
 * everything else we've been doing has been pinned at the top… it'll act also as
 * a title for the page." The bar was already the title-band stuck-line
 * vocabulary — name left with its responsive shortening, cart-and-price accent
 * button right — so what changed is the edge it pins to and the direction it
 * arrives from, and both are now one declaration instead of two.
 *
 * THE RESTING PAGE IS UNCHANGED. The trigger is still "the rail's CTA has left
 * the viewport", so a phone at rest still shows photo-to-chrome with no band at
 * all. FLAGGED: if what Ryan meant was a title band PRESENT from the top of the
 * page, that reverses the ruled photo-to-chrome look and is a separate change on
 * top of this one.
 *
 * IT IS A RUNG IN THE OFFSET CASCADE NOW, and that is the cost of the move: a
 * pinned bar at the top is something every other pinned thing on the page has to
 * clear. `pdp-buy-bar.tsx` publishes `--sf-pdp-buybar-h` — its height while it
 * is shown, 0 otherwise — and the Description section's chapter rail adds that
 * term to its own `top`. Without it the bar is drawn straight over the chip
 * band, which is 44px of navigation a reader can see but not reach.
 *
 * `position: fixed` rather than `sticky`: the bar is not part of any column's
 * flow and has no containing block it should be released by. It also makes the
 * bar its own containing block for the absolutely-positioned name probe inside
 * it, which is the sr-span overflow trap (playbook §5) paid for in advance.
 *
 * z-index 30: over the page and over the title band (20), under the chrome
 * (40) — the header is opaque and pinned, and a buy bar that slid over the nav
 * row would cover the phone number this whole site is selling.
 */
.sf-pdp-buybar {
  background: var(--brand-color-surface, #ffffff);
  border-bottom: 1px solid var(--brand-color-line, #e8e7e3);
  bottom: auto;
  box-shadow: 0 6px 18px rgb(23 24 26 / 8%);
  left: 0;
  /* HIDDEN, NOT ABSENT. `visibility` is what takes the button out of the tab
   * order and out of the accessibility tree while the rail's own CTA is on
   * screen — the same discipline chrome.css applies to the header's
   * crossfading contact pair, and the reason this is not `opacity` alone. */
  visibility: hidden;
  opacity: 0;
  position: fixed;
  right: 0;
  /* `--sf-pinned-offset` is the same rung the sticky gallery reads, so the 920
   * cut's shorter chrome (50px) carries the buy bar with it; the `0px` fallback
   * is for a brand that never loads this chrome. */
  top: var(--sf-pinned-offset, 0px);
  z-index: 30;
}

.sf-pdp-buybar[data-shown="true"] {
  opacity: 1;
  visibility: visible;
}

/*
 * The arrival. The boards carry appearance and never motion (playbook §2), so
 * this is the smallest honest thing: the bar slides in from the edge it is
 * pinned to over 160ms, the same duration and easing the delivery modal's
 * panel uses.
 *
 * THE `visibility` STEP IS DELAYED ON THE WAY OUT and immediate on the way in,
 * which is what lets the fade be seen in both directions: `visibility` is not
 * an interpolable property, so without the delay the bar would vanish at the
 * first frame of its own exit.
 *
 * Reduced motion gets no transition at all — not a shorter one — so the bar
 * appears and disappears discretely. That is the whole of what this block does,
 * which is why turning it off is a matter of not declaring it.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-pdp-buybar {
    /* IT ARRIVES FROM THE EDGE IT PINS TO, which is the top at both widths now
     * (ruling 8) — this was `100%` while the phone bar sat at the bottom. */
    transform: translateY(-100%);
    transition:
      opacity 160ms ease-out,
      transform 160ms ease-out,
      visibility 0s linear 160ms;
  }

  .sf-pdp-buybar[data-shown="true"] {
    transform: translateY(0);
    transition:
      opacity 160ms ease-out,
      transform 160ms ease-out,
      visibility 0s linear 0s;
  }
}

/*
 * ═══ THE BAR'S LINE IS THE TITLE BAND'S STUCK LINE (Ryan's ruling, 2026-08-27)
 * ═══
 *
 * "Look at where we have a sticky header somewhere else, match that font size,
 * formatting, padding — keep this consistent." THE PRECEDENT IS `chrome.css`'s
 * "the band's stuck state" block, and every number below is that block's,
 * measured off a live stuck category band rather than read out of the sheet.
 *
 * ─── PARITY NOTE (the `sections.css` discipline) ────────────────────────────
 *
 * THESE VALUES ARE REPLICATED, NOT IMPORTED, and that is deliberate: chrome.css
 * is the platform's chrome and this pass does not touch it, so the alternative
 * — hanging `.sf-title-band` classes on a product page's buy bar — would make
 * the PDP a consumer of the band's internals and the band unable to move
 * without breaking a page that has no band on it.
 *
 * THE COST IS DRIFT, AND THIS PARAGRAPH IS WHERE IT IS DISCOVERABLE. The pairs,
 * with chrome.css's own custom property beside each:
 *
 *   desktop  title 26px            `--sf-band-stuck-title` (= `--text-section`)
 *            line  32px            `--sf-band-stuck-line`
 *            above 34px to the glyph box   `--sf-band-stuck-pad-top`
 *            below 20px + 1px rule         `--sf-band-stuck-pad-bottom`
 *            row gap 24px                  `.sf-title-band__row`
 *            TOTAL 87px                    `--sf-band-stuck-h`
 *   phone    title 21px            `--sf-band-stuck-title-m` (= `--text-product`)
 *            line  26px            `--sf-band-stuck-title-line-m`
 *            control 44px          `--sf-band-stuck-line-m`
 *            above 20px / below 12px + 1px `--sf-band-stuck-pad-*-m`
 *            TOTAL 77px                    `--sf-band-stuck-h`
 *
 * Measured on the live category band, 2026-08-27: bar bottom 54, band bottom
 * 141, title box 71→120 at 26px/32px, control 69→120 — an 87px strip with the
 * glyphs 34 below the bar and 21 above the rule. The phone band: 20 above a
 * 44px control, 12 + 1 below, title 21px/26px centred on it, ellipsised.
 *
 * ─── HOW 87 IS REACHED WITH A 48px BUTTON ON THE LINE ───────────────────────
 *
 * The band's control is 51px tall and its own 34px "pad-top" is measured to the
 * TITLE's line box, not to the control — the control simply hangs higher in the
 * same strip. This bar's control is the 48px accent button, so the arithmetic
 * that lands on the same three numbers is 18 + 48 + 20 + 1 = 87, with
 * `align-items: flex-end` putting the 32px title line on the button's bottom
 * edge: the glyph box then starts 18 + (48 − 32) = 34px below the bar's top and
 * ends 21px above its bottom. THE BAND'S RHYTHM EXACTLY, reached from this
 * bar's own control height rather than by copying a padding that assumed a
 * different one.
 *
 * ─── WHAT THE FONT ACTUALLY DID ────────────────────────────────────────────
 *
 * The brief said the font "comes DOWN from the current bar sizing to match".
 * MEASURED, IT GOES UP: the bar drew 21px (`--text-product`) and the band's
 * stuck title is 26px (`--text-section`). The precedent is what was asked for
 * by name, so the precedent is what is implemented — and it is FLAGGED here
 * and in the pass report rather than split the difference. What does come down
 * is the weight of the treatment: the bar drew its name in
 * `--brand-font-heading` at `-0.01em`, and the band's stuck title is
 * `--brand-font-body` at `-0.015em`. For produnk both tokens resolve to
 * Schibsted Grotesk, so on this brand the family is a no-op and the tracking is
 * the visible half.
 */
.sf-pdp-buybar__inner {
  /* The title line sits on the button's bottom edge — see the arithmetic above.
   * (The band's own stuck row is `flex-end` at this width for the same reason.) */
  align-items: flex-end;
  display: flex;
  /* `.sf-title-band__row`'s gap. */
  gap: 24px;
  /* 18 + the 48px button + 20 + the 1px rule = the band's 87px stuck strip. The
   * `min-height: 68px` this replaces was the rail's actions row, which is the
   * right precedent for a row of controls and the wrong one for a title bar.
   *
   * `padding-block`, NEVER the shorthand — this element is also `.sf-container`,
   * whose whole job is the page's horizontal gutter, and `padding: 18px 0 20px`
   * WIPES IT. Measured 2026-08-27 with the shorthand in place: the accent button
   * sat flush against the viewport's right edge, 64px outside the gutter every
   * other row on the page keeps. It is the shorthand-wipe bug class the playbook
   * names, arriving on the one element where it is invisible in a diff. */
  padding-block: 18px 20px;
  /* The name probe inside is absolutely positioned; this is the containing
   * block it is measured against. */
  position: relative;
}

.sf-pdp-buybar__name {
  color: var(--brand-color-ink, #17181a);
  /* The row's only flexible child, so its content box IS the space the price
   * button leaves — which is what `pickNameTier` measures against. */
  flex: 1 1 auto;
  /* THE BAND'S STUCK TITLE, token for token — see the parity note above. */
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-section);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 32px;
  min-width: 0;
  overflow: hidden;
  /* The floor under the shortest cut: a name that will not fit even at its
   * model word is trimmed rather than allowed to push the button off screen. */
  text-overflow: ellipsis;
  /* `overflow: hidden` hosting an absolutely-positioned descendant — playbook
   * §5, the sr-span escape. Positioned, so the probe's containing block is this
   * element and not the page. */
  position: relative;
  white-space: nowrap;
}

/*
 * THE THREE CUTS AT THEIR NATURAL WIDTH, so the tier is chosen from measured
 * numbers rather than a guess at the font's metrics. Out of flow, so it costs
 * the row nothing; `visibility: hidden` rather than `display: none` because a
 * box with no layout has no width to read.
 */
.sf-pdp-buybar__probe {
  left: 0;
  pointer-events: none;
  position: absolute;
  top: 0;
  visibility: hidden;
  white-space: nowrap;
}

.sf-pdp-buybar__cta {
  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;
  /* Never the thing that gives: the name shortens, the price does not. */
  flex: 0 0 auto;
  font-family: inherit;
  /* The primary rung, from globals.css §3 (Ryan's "a button is a button",
   * 2026-09-01). This button and `.sf-flow-btn` were the two already AT the
   * standard, so reading it from the tokens is a no-op here — they are what the
   * rest of the site was normalised to. */
  font-size: var(--button-text, 16px);
  font-weight: var(--button-weight, 700);
  gap: 10px;
  min-height: var(--control-height, 48px);
  padding: 0 22px;
  /* A link since the seam entered service — see `.sf-pdp-buy__cta`. */
  text-decoration: none;
}

/* Focus ring: see "the focus ring" at the top of this sheet, which carries
 * every control on this page and the ground each one's ring lands on. */

.sf-pdp-buybar__cta-mark {
  align-items: center;
  display: inline-flex;
}

/* Money is a mono duty (tokens §2), inside a button as anywhere else. */
.sf-pdp-buybar__cta-price {
  font-family: var(--brand-font-mono, monospace);
}

/*
 * THE PLACEMENT BLOCKS ARE GONE (ruling 8). There were two — a `min-width: 768`
 * one putting the bar at the top and a `max-width: 767.98` one putting it at the
 * bottom with its rule and shadow flipped — and the whole of the first is now
 * the base rule above, because the bar is at the top at every width. The phone's
 * `env(safe-area-inset-bottom)` padding went with the bottom edge it was
 * clearing; the top of a phone viewport has no home indicator on it.
 *
 * PHONE: the row tightened to the phone's measure, which is all that is left of
 * a width-specific buy bar.
 */
@media (max-width: 767.98px) {
  /*
   * THE PHONE'S STUCK LINE, and it is the band's 77px one: 20 above a 44px
   * control, 12 + the 1px rule below, with the title centred on the control
   * rather than sitting on its baseline — a 26px title box on a 44px control
   * reads as having fallen off it, which is the reasoning chrome.css gives for
   * `align-items: center` in its own phone stuck row.
   *
   * THE BUTTON COMES DOWN TO 44px, from the 48 it keeps at desktop. That is the
   * band's control height, it is what makes the total 77 rather than 81, and it
   * is still the platform's touch-target floor — 44 is the floor, not a value
   * below it.
   */
  .sf-pdp-buybar__inner {
    align-items: center;
    /* `.sf-title-band__row`'s phone gap. */
    gap: 14px;
    /* `padding-block` — see the desktop rule for the gutter this must not wipe. */
    padding-block: 20px 12px;
  }

  .sf-pdp-buybar__name {
    /* `--sf-band-stuck-title-m` and its line, which is `--text-product`. */
    font-size: var(--text-product);
    line-height: 26px;
  }

  .sf-pdp-buybar__cta {
    /* `--sf-band-stuck-line-m` — the band's own control height. */
    min-height: 44px;
    padding: 0 16px;
  }
}

/* ---------------------------------------------------- the landing CTA ----- */

/*
 * THE LANDING CTA (Ryan's idea 2026-08-26, MOVED INTO THE RAIL by his v2 ruling
 * the same day) — the way back up the catalog, for the visitor who has none.
 *
 * The behaviour, the first-paint script and the polarity are in
 * `pdp-landing-cta.tsx`. What is here is only the box.
 *
 * WHERE IT SITS. First child of `.sf-pdp-rail__head` and of
 * `.sf-pdp-mrail__head` — above the eyebrow, which is the line that already
 * names the category. Version 1 was a paper band under the chrome; that band is
 * gone in both compositions, so the photograph now meets the chrome for every
 * arrival at every width, which is what the ruling was for.
 *
 * THE HEAD IS A FLEX COLUMN with an 8px gap (6px on the phone), and this slot
 * adds its own bottom margin on top of it. The extra is deliberate: the eyebrow
 * and the name are one block that reads as a unit, and a control set at the
 * eyebrow's own line spacing would join that unit instead of sitting above it.
 *
 * HIDDEN WITH `display: none`, not with `visibility` or a transform. The slot
 * occupies real vertical space and the whole rail below it moves when it goes,
 * so the two states have to be "in the layout" and "not in the layout" — the
 * a11y-visibility discipline that governs the buy bar is about a control that
 * keeps its box, which is the opposite problem.
 *
 * NOTHING ANIMATES. It is present or it is not, decided before the first paint,
 * so there is nothing for `prefers-reduced-motion` to turn off.
 */
.sf-pdp-landing-cta {
  display: block;
  /* On top of the head's own 8px gap: 16px to the eyebrow. */
  margin-bottom: 8px;
}

.sf-pdp-landing-cta[data-arrival="internal"] {
  display: none;
}

/*
 * THE QUIET OUTLINED BUTTON. `ink` on `surface` inside a 1px `line-mid` border
 * at the platform's 6px control radius — the site's secondary-CTA language
 * (tokens §3: accent is commerce, ink is wayfinding), which is what this is: it
 * goes to a page, it does not buy anything.
 *
 * QUIETER THAN `.sf-pdp-ghost`, on purpose. The rail's own ghost buttons ("See
 * delivery timeline", "View full specifications") are outlined in `ink` because
 * each of them is a step in the sale. This one is an escape hatch offered to
 * somebody who may not want it, sitting above the product's own name, so it
 * takes the `line-mid` border the rest of the rail's framing uses and lets the
 * name below it be the loudest thing in the column.
 *
 * CONTENT-SIZED ON DESKTOP, which is the rail's own vocabulary: the eyebrow,
 * the name, the lede and the spec rows are all left-aligned and none of them is
 * stretched to the column. `inline-flex` does that without a width. 40px rather
 * than 48: the small-button rung, because this is an offer and not the page's
 * business.
 */
.sf-pdp-landing-cta__button {
  align-items: center;
  /* TRANSPARENT, NOT `surface` — the ghost shows its ground through, which is
   * the trap `.sf-flow-ghost` recorded: white on the near-white grounds this
   * button lands on is a max-channel delta of 4, so it reads as a filled button
   * that happens to match rather than as an outline. Nothing visibly changed;
   * what changed is that it can no longer be wrong on a tinted card. */
  background: transparent;
  border: 1px solid var(--brand-color-line-mid, #dcdad3);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  display: inline-flex;
  /* The compact rung, from globals.css §3 — this button was already ON it (see
   * the note above, which reasoned its way to 40 independently), so the tokens
   * only make that agreement structural. */
  font-size: var(--button-text-sm, 14.5px);
  font-weight: var(--button-weight-sm, 600);
  gap: 8px;
  min-height: var(--control-height-sm, 40px);
  padding: 0 16px;
  /* globals.css underlines every anchor; this one is a button. */
  text-decoration: none;
}

.sf-pdp-landing-cta__button:hover {
  border-color: var(--brand-color-ink-soft, #44464a);
}

/* Focus ring: see "the focus ring" at the top of this sheet, which carries
 * every control on this page and the ground each one's ring lands on. */

.sf-pdp-landing-cta__arrow {
  color: var(--brand-color-muted, #6c6a62);
}

@media (max-width: 767.98px) {
  .sf-pdp-landing-cta {
    /* The phone head's gap is 6px, so the same 16px reads as 10 here. */
    margin-bottom: 10px;
  }

  .sf-pdp-landing-cta__button {
    /* The phone's measure is the whole gutter-to-gutter row, and the rail's own
     * controls (the install CTA, the buy button) already take it. A 40px button
     * floating at the left of a 390px column reads as an orphan beside them. */
    display: flex;
    justify-content: center;
  }
}

/* ======================================================= below the fold == */

/*
 * PDPBelow.dc.html, drawn at 1440. Four sections: customer photos (the SHARED
 * module — nothing of it is dressed here), the engineering section, the reviews
 * and the answers.
 *
 * THE PREFIX IS STILL `sf-pdp-`, deliberately. `sf-pdpb-` would have been a NEW
 * share of the `sf-` namespace, and the chrome sweep in
 * `tests/category-page.test.ts` claims every `sf-` name it is not told to
 * exclude — so a new prefix is an edit to another lane's test file. Everything
 * below the fold is `sf-pdp-sec-`, `sf-pdp-eng-`, `sf-pdp-rev-` or
 * `sf-pdp-ans-`, all of which the existing exclusion already covers and all of
 * which `tests/pdp-page.test.ts`'s own sweep now sees (its markup list grew by
 * the files this pass added).
 *
 * BOARD VALUES SHIP VERBATIM (playbook §3). The odd numbers below — 9px well
 * captions, 5px chip padding, 93.9% bar fills, a 250px rail against an 860px
 * measure — are measured, not sloppy.
 *
 * THE BOARD IS CONTENT-BOX AND THIS SITE IS BORDER-BOX, so anywhere the drawing
 * carries an explicit width or height ALONGSIDE padding or a border, the
 * arithmetic is written beside it (the discipline `.sf-pdp-zip` established at
 * the top of this sheet). Below the fold every such box — the checkbox, the
 * review photo well, the panel avatar, the histogram track — is a square or a
 * strip with NO padding, so the numbers pass through unchanged. The one place
 * it bites is the selected chip, and that is noted where it happens.
 *
 * RESPONSIVE IS DERIVED, NOT DRAWN. There is no mobile PDPBelow board — the
 * canvas holds one artboard for this surface, at 1440. So every rule under a
 * media query here is this pass's own judgement, made to one principle: the
 * two-column shapes (rail + column, results + panel) stack and nothing else
 * changes. It is FLAGGED as derived rather than presented as transplant, and it
 * is gated by `tools/overflow.mjs` and by computed-style assertions rather than
 * by a pixel diff, because there is no drawing to diff against.
 */

/* --------------------------------------------------- the section shell -- */

/*
 * THE PLATFORM SECTION RHYTHM, at the board's own number: 80px between
 * sections. The board writes it as `margin: 80px 64px 0` — the 64 is the page
 * gutter, which `.sf-container` supplies, so only the 80 is a section rule.
 */
.sf-pdp-sec {
  /*
   * THE BOARD'S OWN ROOT SIZE, and it is load-bearing rather than tidy. The
   * board's `body` carries no `font-size`, so every box on it that does not
   * declare one inherits 16px; globals.css sets this site's body to 17. Nothing
   * on this surface is DRAWN at the inherited size — every element that holds
   * text names its own — but a block's STRUT is its own font-size, so an
   * unstyled wrapper around a 14.5px link is a 20px line box on the board and a
   * 21px one here. Measured: the sticky photo panel and the search card each
   * came out a pixel out for exactly that reason.
   */
  font-size: var(--text-emphasis);
  /* The boards declare no leading, so a line box that does not name one is
   * `normal` — the same reset `.sf-pdp` makes for the page above. */
  line-height: normal;
  margin-top: 80px;
}

/*
 * ─── THE ORDER CHANGED, SO THE TWO SPACING RULES DID (ruling O1, 2026-08-27) ─
 *
 * The board's order was photos, description, reviews, answers. Ryan's is
 * DESCRIPTION, REVIEWS, PHOTOS, ANSWERS — the gallery above the fold is real
 * photography now, and opening the page's second half with a second photo grid
 * was "too much photos".
 *
 * TWO NUMBERS HAD TO FOLLOW IT, and they are the only two:
 *
 *   THE 48px BELOW THE BUY GRID belonged to whatever is FIRST, not to the photo
 *   module. PDPBelow draws it between the grid and the top of the second half;
 *   it is now written against the wrapper's first section, so it stays that
 *   distance whatever ends up there. `:first-child` rather than naming the
 *   Description section: the number is a property of the position.
 *
 *   THE PHOTO MODULE TAKES THE SECTION RHYTHM in the middle of the run, because
 *   that is what it is now — one section among four, with 80px over it exactly
 *   like the Reviews and Answers heads above and below it. Its own 84px (from
 *   photos.css, the CATEGORY board's number) would read as a fifth number in a
 *   column of three 80s. The module is CONSUMED, not modified: this is one
 *   declaration in this page's sheet naming the module's class as a descendant
 *   of this page's wrapper, so nothing in photos.css changes and no other
 *   consumer can see it.
 */
.sf-pdp-below .sf-ph {
  padding-top: 80px;
}

/*
 * THE 48px BELOW THE BUY GRID IS THE WRAPPER'S NOW (ruling PB1, 2026-08-28).
 *
 * It used to be written against the first SECTION, because the below-fold was
 * four full-bleed siblings and "whichever lands first has to carry it" — two
 * selectors, one for a `.sf-pdp-sec` (which spaces by margin) and one for the
 * consumed photo module (which spaces by padding). Both retire: there is one box
 * over the whole second half of the page now, the nav is inside it, and the
 * distance is a property of THAT box. A first-child rule would also have missed
 * entirely — the flow's first child is the chapters' measure wrapper, which is
 * neither of the two things those selectors named.
 */
.sf-pdp-below {
  margin-top: 48px;
}

/* ------------------------------------------ the nav and the flow, one grid -- */

/*
 * ─── THE NAV OWNS THE LEFT RAIL FOR THE WHOLE BELOW-FOLD (ruling PB1) ────
 *
 * Ryan, 2026-08-28: "Remove the Description section header. The sticky left nav
 * now navigates ALL the sections — every description chapter PLUS Reviews,
 * Customer photos and Answers."
 *
 * THE GRID HAD TO MOVE OUT ONE LEVEL for that to be buildable at all. A sticky
 * box travels only within its CONTAINING BLOCK, and inside the engineering
 * section that block ended at the last chapter — the same lesson this surface
 * paid for twice already (the desktop rail that "had never actually stuck", and
 * the phone band released after 44px). So the two columns are declared HERE, on
 * a wrapper that spans all four sections, and the nav is a grid item of it.
 *
 * `align-items` IS LEFT AT `stretch`, deliberately and against the instinct that
 * wrote `start` on the old section grid: the nav column must be the height of
 * the ROW — which is the flow's — or the sticky inside it has nowhere to
 * travel. That is `align-self: stretch` from the old rule, promoted to the
 * default because there is no second item that must not stretch.
 *
 * ─── THE RAIL'S WIDTH (ruling PB3) ──────────────────────────────
 *
 * "Shrink the rail to fit its longest label so the content column breathes."
 * MEASURED, not guessed: the widest of the ten entries is "Rust Armor & looks"
 * — see the measurement recorded on `.sf-pdp-nav__item`, which is where the
 * padding and the mark that pad it are declared. The old rail was 250px against
 * seven chapter labels; 190 is the measured content plus its padding, and the
 * 60px it gives back go to the content column at every rung.
 */
.sf-pdp-below__grid {
  display: grid;
  gap: 40px;
  grid-template-columns: 190px minmax(0, 1fr);
}

/*
 * EACH SECTION'S OWN CONTAINER IS NEUTRALISED, in one declaration.
 *
 * Every section below the fold carries a `.sf-container` inside it — that is
 * how a full-bleed section gets the page gutter, and it is the shape the SHARED
 * photo module uses too. With the gutter now supplied one level up, a second
 * container inside the flow would indent every section by another 64px and cap
 * it at 1440 inside a column that is already narrower than that.
 *
 * SO THE INNER ONES BECOME PLAIN BLOCKS. It is a rule in THIS page's sheet
 * naming another sheet's class as a descendant of this page's wrapper — the
 * same device `.sf-pdp-below .sf-ph` already uses for the section rhythm — so
 * nothing in photos.css or chrome.css changes and no other consumer can see it.
 * `--sf-gutter` is deliberately NOT reset: the inner container still declares
 * it, at the same value the outer one does, and the full-bleed recipes inside
 * these sections read it.
 */
.sf-pdp-below__flow .sf-container {
  margin-inline: 0;
  max-width: none;
  padding-inline: 0;
  width: auto;
}

/*
/*
 * THE HEAD IS A STACK NOW: the head ROW, then whatever hangs under it.
 *
 * IT WAS A FLEX ROW of [title + subhead] and [action], aligned `flex-end` — so
 * the action sat level with the BOTTOM of the subhead, a line and a half below
 * the headline it belongs to, and on a phone it stacked under the wrapped
 * subhead three lines from its own title. Ryan's ruling 11 (and A1 for Answers)
 * moves it into the headline row. The stack is what is left.
 */
.sf-pdp-sec__head {
  display: flex;
  flex-direction: column;
}

/*
 * `.sf-pdp-sec__head--sticky` IS RETIRED BY NAME (Ryan's ruling PB1,
 * 2026-08-28). It was the Description head, turned into a pinned full-bleed
 * band by ruling E1 so a reader scrolling the chapters could still see which
 * part of the page they were in. THAT HEAD IS REMOVED, and the nav that pinned
 * under it now names all ten sections by itself — so the band has nothing left
 * to announce, and the `--sf-pdp-enghead-h` rung it published for every sticky
 * `top` beneath it goes with it.
 */

/*
 * THE SECTION-HEAD RIGHT SLOT, and it is the pattern `.sf-ph__head-row` already
 * draws one section above this one (photos.css): the header and its right-hand
 * control are ONE centred row with `space-between` and no spacer element.
 *
 * BOTH ITEMS SHRINK, for the reason that rule gives: held rigid at 320 the
 * heading keeps one line and the BUTTON takes the whole squeeze, which tears
 * "Write a review" in half. Shrinkable, the heading wraps and the control keeps
 * its words.
 */
.sf-pdp-sec__head-row {
  align-items: center;
  display: flex;
  gap: 24px;
  justify-content: space-between;
}

/*
 * The 54px indent is the platform's own (tokens §3: "icon-header sub-text
 * indents 54px to clear the icon circle") and it is also exactly what the board
 * draws — the 40px circle plus the head's 14px gap.
 */
.sf-pdp-sec__sub {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  margin: 6px 0 0 var(--space-icon-indent);
}

/* NO `padding-bottom` ANY MORE. The 2px existed to lift the button off the
 * subhead's baseline in the old `flex-end` row; in a centred row it is 2px of
 * nothing pushing the button up. */
.sf-pdp-sec__action {
  font-size: var(--text-body);
}

/*
 * The board's `.ghost`: an ink-outlined button. Content-box on the board and
 * border-box here, but it carries no explicit width or height, so the padding
 * expands the box identically either way and no arithmetic is owed.
 */
/* THE COMPACT RUNG, DECLARED (2026-09-01) — it drew 40 in one section and 44 in
 * another off the same `padding: 12px 22px`, which is the padding-derived-height
 * failure one more time. The type it already had IS the rung's, so only the box
 * moved. */
.sf-pdp-sec__ghost {
  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;
}

/*
 * THE MARK IS A BARE `<svg>`, not a wrapped one, and that is the board's own
 * shape: `margin-right: 8px; vertical-align: -2px` on an inline glyph inside an
 * inline-block button. Wrapping it in a span would either leave it inline (and
 * pick up the line box's 3px descender — playbook §5) or blockify the button
 * into a flex row, which is a different geometry from the one the board drew.
 */
.sf-pdp-sec__ghost svg {
  margin-right: 8px;
  vertical-align: -2px;
}

/* ---------------------------------------------------------- the nav ------ */

/*
 * THE BELOW-THE-FOLD SECTION NAV (Ryan's ruling PB1, 2026-08-28).
 *
 * It was `.sf-pdp-eng__rail*` — the engineering section's chapter rail — and
 * every one of those names is RETIRED with the ruling that generalised it. What
 * is below is the same drawing under the name it now deserves: it navigates ten
 * sections, not seven chapters, and it lives in the composition rather than
 * inside one section (see `.sf-pdp-below__grid`, which is where its column and
 * the sticky it travels in are declared).
 *
 * THE PIN IS ON `__inner`, INSIDE THE STRETCHED NAV. The nav is the grid item
 * whose containing block is the whole below-the-fold; the band is what it
 * carries. Splitting the two is what made the old rail's stickiness travel once
 * `align-self: stretch` gave the nav the row's height, and the split is
 * unchanged.
 *
 * THE OFFSET IS THE CASCADE, not a number: the chrome (54 while stuck), the
 * title band (0 on a product page, which has none — written anyway so the rule
 * survives a page that grows one) and the buy bar, which pins at the top at
 * every width since ruling 8. THE FOURTH RUNG IS GONE (ruling PB1):
 * `--sf-pdp-enghead-h` was the Description head band, and there is no
 * Description head.
 *
 * THE BOARD'S +20 STAYS: on desktop this is a COLUMN beside the text rather than
 * a band across it, and a column resting 20px under the chrome reads as the
 * start of a new thing rather than as a second row of chrome. (The phone rung is
 * flush — see its own rule.)
 */
.sf-pdp-nav__inner {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: calc(
    var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px) +
      var(--sf-pdp-buybar-h, 0px) + 20px
  );
}

/*
 * `display: contents` — the links are direct flex children of the sticky
 * column, which is what puts the 2px gaps between them. The wrapper exists for
 * the mobile rung, where it becomes the scroller.
 */
.sf-pdp-nav__links {
  display: contents;
}

/*
 * ONE ENTRY, AND THE MEASUREMENT THAT SET THE RAIL'S WIDTH (ruling PB3).
 *
 * MEASURED OFF THE RENDER, not estimated: the widest of the ten labels is "Rust
 * Armor & looks" at 129.91px (`--text-support`, 14.5px, brand body face),
 * against "Height adjustment" at 129.70, "Frame & mounting" at 122.14 and
 * "Customer photos" at 115.73. The entry's box is that plus the 14px mark, the
 * 9px gap and 14px of padding each side: 129.91 + 14 + 9 + 28 = 180.91. The
 * column is 190, which is the measured longest entry with 9px of slack for a
 * label a future section brings — and 60px less than the 250 the old rail took
 * for seven chapter labels. Every one of those 60px goes to the content column,
 * at every rung.
 *
 * THE MARK IS THE SAME COMPONENT the section's own header circle draws, one
 * size down because the nav's type is — which is the ruling-9 pair, unchanged.
 * Same flex reasoning as everywhere else on this page: a link is inline by
 * default, so an svg inside one hangs on the line box unless the link is a flex
 * row.
 */
.sf-pdp-nav__item {
  align-items: center;
  border-radius: var(--radius-control);
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-support);
  gap: 9px;
  padding: 9px 14px;
  text-decoration: none;
}

.sf-pdp-nav__item svg {
  flex: none;
  /* The mark reads a step quieter than the words at rest, so the label is still
   * what the eye lands on; on the filled section both are `surface` and the
   * opacity would grey the glyph against the ink, so it is lifted back. */
  opacity: 0.72;
}

.sf-pdp-nav__item--on svg {
  opacity: 1;
}

.sf-pdp-nav__item:hover {
  background: var(--brand-color-well, #f1f0ec);
}

/* Ink fill is the site's wayfinding-active state (tokens §4). */
.sf-pdp-nav__item--on,
.sf-pdp-nav__item--on:hover {
  background: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
  font-weight: 600;
}

/* `.sf-pdp-eng__rail`, `__rail-inner`, `__rail-links`, `__rail-item` and
 * `__rail-item--on` are RETIRED BY NAME with ruling PB1 — the rail became this
 * nav. `.sf-pdp-eng__rail-note` went earlier (ruling 8, 2026-08-27). */

/* ------------------------------------------------------- description ----- */

/*
 * THE DESCRIPTION IS A RUN OF CHAPTERS AND NOTHING ELSE (ruling PB1).
 *
 * `.sf-pdp-eng` was a two-column grid — 250px of rail beside the reading column.
 * The rail left (see the nav above) and the section head left with it, so what
 * remains is the MEASURE: the wrapper is a plain block, and the 860px below is
 * the only thing it still does.
 */
.sf-pdp-eng {
  margin-top: 0;
}

/*
/*
 * THE 860px MEASURE. The board's own annotation says why — body copy holds a
 * ~72-character reading measure and the figures share the column — and the
 * annotation is not rendered because it is the designer talking to a reader of
 * the board. This declaration is the part of it that was real.
 */
.sf-pdp-eng {
  max-width: 860px;
}

/* `.sf-pdp-eng__column` IS RETIRED BY NAME (ruling PB1): it was the right-hand
 * cell of a two-column grid, and there is one column now. The measure it carried
 * moved up to the wrapper, which is the only box left between the flow and the
 * chapters. */

/*
 * "SCROLLING IS CHEAP" (Ryan's ruling E3, 2026-08-27): generous space between
 * chapters. 34 -> 56, which is the platform's own next step up and puts the
 * hairline in the middle of real air rather than acting as the only separator
 * a reader gets. Carried to the phone identically — the ruling says "same
 * treatment both widths" and this is the number that makes the bigger header
 * below read as a header rather than as a crowded line.
 *
 * ─── AND THE AIR ABOVE IS THE SECTION RHYTHM NOW (Ryan's FX6, 2026-08-28) ──
 *
 * "More space ABOVE the chapter block — the 56 reads too close in the flesh
 * with the promoted headers." It did, and the reason is what ruling PB2 did to
 * these blocks rather than anything about the 56: a chapter used to open with a
 * muted mono line, and it opens with a PLATFORM SECTION HEADER now — a 40px
 * circle and a 26px title, the same head the Reviews section three blocks down
 * draws. A top-level section starting 56px after the previous one's last line
 * is a section that reads as a subsection.
 *
 * SO THE TOP TAKES `--space-section` (96px), which is the platform's own rung
 * between two sections and is what these blocks became. The BOTTOM keeps E3's
 * 56: it is the chapter's own trailing air, not a boundary, and Ryan did not
 * ask for more of it. The asymmetry is the point rather than a compromise —
 * the hairline now sits 56px after the previous chapter and 96px before the
 * next one's eyebrow, so it reads as the CLOSE of what is above it instead of
 * as a line floating between two blocks with equal claim on it.
 *
 * MEASURED, previous chapter's box to the next eyebrow's box: 57 -> 97 (the
 * odd pixel is the hairline itself), and the full content-to-content distance
 * 113 -> 153.
 *
 * ONE NUMBER AT BOTH WIDTHS, still — E3's "same treatment both widths" is
 * unchanged, and Ryan reported this one at 390. The phone rung of the spacing
 * scale is 56px, which is the value he has just called too close, so stepping
 * the phone to it instead would have been the defect with a token's name on it.
 */
.sf-pdp-eng__chapter {
  border-top: var(--border-hairline);
  padding: var(--space-section) 0 56px;
}

/*
 * ─── NO RULE ABOVE THE FIRST ONE (Ryan's ruling PB4, 2026-08-28) ───────
 *
 * "The line plus the downshift reads odd — the first header should sit even with
 * the top." A hairline is a SEPARATOR between two chapters, and there is nothing
 * above the first one to separate it from: with the Description head removed
 * (ruling PB1) the line was drawing the top edge of the below-the-fold, which is
 * a job no rule on this page has.
 *
 * THE PADDING GOES WITH IT, which is the "downshift" half of what he saw: a rule
 * with 56px under it is a rule that pushes its own header down the page. The
 * first chapter's header now starts where the section starts — level with the
 * nav's first entry beside it — and the 48px this wrapper already carries
 * (`.sf-pdp-below`'s own margin) is the whole of the air above it.
 */
.sf-pdp-eng__chapter:first-child {
  border-top: 0;
  padding-top: 0;
}

/*
 * ─── THE EYEBROW IS THE SECTION'S NAME AGAIN (ruling PB2, 2026-08-28) ────
 *
 * A short history, because this line has now been three things and the last one
 * is the reason the first two ended:
 *
 *   IT WAS THE CHAPTER'S NAME, set 10.5px muted mono over a 21px claim — the
 *   quietest type on the page above the loudest, so the chapter NAME was what a
 *   reader skipped. Ruling 9 gave it a mark; ruling E3 then inverted the pair,
 *   promoting the name to 21px ink and stepping the claim down to 18.
 *
 *   IT IS "DESCRIPTION" NOW. Ruling PB2 promotes the chapter's name again — all
 *   the way to the PLATFORM SECTION HEADER (`.sf-sec-head`, sections.css: the
 *   40px tinted circle and the 26px title, the same head Reviews and Answers
 *   wear) — and puts the section's own name above it as the eyebrow. E3's
 *   intent is kept and raised: the name is bigger than it has ever been, the
 *   mark is the same 22px glyph now inside its circle, and the claim is still
 *   its 18px standfirst. What goes is the SHOUT — "not all-caps", the ruling's
 *   own words.
 *
 * SO THIS IS THE HOUSE EYEBROW, not a bespoke one: mono, `muted`, `--text-micro`
 * and 0.12em of tracking, which is the declaration `.sf-pdp-ans__eyebrow` and
 * `.sf-ph__eyebrow` already share. It carries no mark and needs no flex row: the
 * glyph moved into the header's circle, which also retires the descender trap
 * (playbook §5) the old flex row existed to dodge.
 *
 * ─── IT SHOUTS, AND IT LINES UP WITH THE WORDS (Ryan's PB-polish, 08-28) ──
 *
 * TWO CORRECTIONS TO THE ABOVE, both of them Ryan's, and the flag this comment
 * used to carry is answered by the first of them.
 *
 *   CAPS. "Go caps, keep consistency." The flag read: every other eyebrow on
 *   the site is AUTHORED in capitals and this one was authored as
 *   "Description". It is authored as "DESCRIPTION" now
 *   (`pdp-below-content.ts`), which is the consistency he asked for taken at
 *   the level the rest of the site keeps it. NO `text-transform` here, still,
 *   and now for a better reason than before: a transform would leave this the
 *   one eyebrow whose drawn case and authored case disagree.
 *
 *   IT SITS OVER THE WORDS, NOT OVER THE CIRCLE. "Currently it starts above
 *   the icon circle and reads odd — align its left edge with the title text's
 *   left edge." The header under it is `.sf-sec-head`: a 40px circle, a 14px
 *   gap, then the title — so the title's left edge is 54px in, which is
 *   `--space-icon-indent`, the platform's own token for exactly this (tokens
 *   §3: "icon-header sub-text indents 54px to clear the icon circle"). Written
 *   through the token, and it is the THIRD consumer of it: `.sf-pdp-sec__sub`
 *   and `.sf-home-live__sub` are the two sub-lines that already hang off a
 *   section head at this indent. An eyebrow ABOVE the head and a sub-line
 *   BELOW it are the same column of words either way.
 *
 *   The mobile step is the same arithmetic on the smaller head — 34px circle
 *   plus its 11px gap — and it is written where those two rules write it, in
 *   the phone block at the foot of this sheet.
 */
.sf-pdp-eng__eyebrow {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  line-height: 1.2;
  /*
   * ─── IT HAS TO READ AS ATTACHED (Ryan's FX6, 2026-08-28) ─────────────────
   *
   * "The eyebrow is not close enough to the title — it reads as not attached,
   * like it's own thing." 10px is why, and the 10 was never the whole distance:
   * the header under it is a FLEX ROW 40px tall (the circle) with the title's
   * line box centred in it, so the row contributes leading of its own above the
   * words. MEASURED, and it is a flat 4.00px at 390 and at 1440 alike — the
   * circle steps down to 34 on the phone and the title steps down with it, so
   * the leftover centres to the same figure.
   *
   * THE HOUSE NUMBER IS 8, MEASURED BETWEEN LINE BOXES. `.sf-pdp-ans__eyebrow`
   * is the reference pair — eyebrow to the line it labels, 8px box and 8px line
   * box, because there is no row between them to add anything. This eyebrow was
   * paying that 8 twice over: 10 declared plus 4 the header row inserted, for a
   * 14.6px gap where every other eyebrow on the site draws 8.
   *
   * SO THE DECLARED VALUE IS THE HOUSE GAP MINUS WHAT THE ROW ALREADY GIVES:
   * 8 - 4 = 4px, landing 8.6px between the two line boxes, which is the same
   * optical attachment the answers eyebrow has. FLAGGED: there is no 4px
   * spacing token to write it through — the 4 is derived from a measurement of
   * this header rather than taken off the scale, so `tools/pdpbody.mjs` asserts
   * the GAP (the thing the ruling is about) rather than the margin, and a
   * change to the circle's size or the title's leading fails there instead of
   * drifting quietly.
   */
  margin: 0 0 4px var(--space-icon-indent);
}


.sf-pdp-eng__claim {
  /*
   * ─── AIR UNDER THE HEADER (Ryan's PB-polish, 2026-08-28) ─────────────────
   *
   * "More vertical space between each chapter title and its first line" — he
   * named "Safety & warranty" over "Warranted for dunking and hanging. Padded
   * for everything else.", which is this line. The gap was ZERO: the head is a
   * 40px flex row and the claim's margin was `0`, so a 26px section header sat
   * directly on an 18px/700 standfirst. That reads as one crowded block rather
   * than as a header and the sentence under it, and the crowding got worse
   * when ruling PB2 promoted these heads — a bigger header on the same nothing.
   *
   * 14px, AND IT IS A TOKEN RATHER THAN A NUMBER: `--space-after-header` is the
   * platform's own value for this (tokens §3, "14px under section headers"),
   * declared in globals.css since the scale was written and — until this
   * declaration — never once consumed. Picking the spacing token whose
   * documented meaning IS this gap is what keeps the seven chapter heads and
   * every future section head on one rhythm.
   *
   * ONE VALUE AT BOTH WIDTHS, which is the ruling's "both widths": the token is
   * flat, the phone's head steps down in size but not in rhythm, and a second
   * number here would be a mobile divergence nobody asked for.
   *
   * IT IS FOLDED INTO THE `margin` SHORTHAND BELOW rather than written as a
   * `margin-top` here: this rule already sets `margin: 0`, and a longhand above
   * a shorthand is the wipe bug class the playbook names (§6) — the value would
   * simply not exist.
   */

  /*
   * NO TRACKING. globals.css gives every h1/h2/h3 `letter-spacing: -0.015em`,
   * and PDPBelow.dc.html draws these headings with none — measured at -0.315px
   * on a 21px claim, which re-shapes the whole line and read 1.1–2.1% on every
   * engineering chapter before this declaration landed. The section TITLE is a
   * different case and keeps the platform's tracking: it is the shared
   * `.sf-sec-head__title`, which three other boards do draw tracked.
   */
  letter-spacing: normal;

  color: var(--brand-color-ink-soft, #44464a);
  /* globals.css balances every h1/h2/h3, which wraps a claim where the
   * algorithm likes rather than where the board drew it. Same override and the
   * same reason as `.sf-pdp-rail__name`, where it was worth 14% of a region. */
  font-family: var(--brand-font-body, system-ui, sans-serif);
  /* THE SUMMARY LINE "SIZES DOWN A TOUCH" (ruling E3): 21 -> 18, one step, and
   * one step of ink with it — the claim is the standfirst under the header
   * above it now, not the loudest thing in the chapter. Its weight stays at
   * 700, so it is still unmistakably a claim and not body copy. */
  font-size: var(--text-card-header);
  font-weight: 700;
  line-height: 1.3;
  margin: var(--space-after-header) 0 0;
  text-wrap: wrap;
}

.sf-pdp-eng__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  line-height: 1.65;
  margin: 10px 0 0;
}

.sf-pdp-eng__lead {
  color: var(--brand-color-ink, #17181a);
}

.sf-pdp-eng__figure {
  margin-top: 16px;
}

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

.sf-pdp-eng__well-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  /* 9px, below the 13.5 type floor — the deliberate placeholder-caption
   * exemption every media well on every board uses (tokens §2). */
  font-size: 9px;
  text-align: center;
}

.sf-pdp-eng__caption {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 6px 0 0;
}

.sf-pdp-eng__pair {
  display: grid;
  gap: 12px;
  grid-template-columns: 1fr 1fr;
}

.sf-pdp-eng__modes {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: 16px;
}

.sf-pdp-eng__mode {
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  padding: 12px 14px;
}

.sf-pdp-eng__mode-title {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 700;
  margin: 0;
}

.sf-pdp-eng__mode-body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  line-height: 1.5;
  margin: 4px 0 0;
}

/*
 * `clip` and not `hidden`: a rounded box that clips its rows would kill a
 * sticky descendant the day one lands in it (tokens gotcha #1).
 *
 * AND CONTENT-BOX, which is the board's own model and worth 2px here: the board
 * draws `max-width: 480px` with a 1px border on a content-box canvas, so its
 * table is 482 wide and every figure in the right-hand column sits 2px further
 * right than a 480px border-box one. Measured: 0.405% on this chapter, all of it
 * in the three mono value rows. Keeping the board's box model rather than
 * writing 480 + 1 + 1 means the number here IS the board's number.
 */
.sf-pdp-eng__table {
  border: var(--border-hairline);
  box-sizing: content-box;
  border-radius: var(--radius-control);
  margin-top: 16px;
  max-width: 480px;
  overflow: clip;
}

.sf-pdp-eng__table-head {
  background: var(--brand-color-well, #f1f0ec);
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  font-family: var(--brand-font-mono, monospace);
  /* The board sets `class="eyebrow"` AND an inline 12.5px, and the inline value
   * wins — so this row is a caption-sized eyebrow, not a micro one. */
  font-size: var(--text-caption);
  justify-content: space-between;
  letter-spacing: 0.12em;
  padding: 9px 14px;
}

.sf-pdp-eng__table-row {
  border-top: var(--border-hairline);
  display: flex;
  font-size: var(--text-support);
  justify-content: space-between;
  padding: 10px 14px;
}

.sf-pdp-eng__table-label {
  color: var(--brand-color-ink-soft, #44464a);
  font-weight: 400;
}

.sf-pdp-eng__table-label--strong {
  color: var(--brand-color-ink, #17181a);
  font-weight: 700;
}

.sf-pdp-eng__table-value {
  color: var(--brand-color-ink-soft, #44464a);
  font-family: var(--brand-font-mono, monospace);
}

.sf-pdp-eng__table-value--good {
  color: var(--brand-color-good, #1f7a4d);
}

.sf-pdp-eng__table-foot {
  border-top: var(--border-hairline);
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin: 0;
  padding: 8px 14px;
}

/* ----------------------------------------------------------- reviews --- */

/*
 * A CARD is the board's `.card`: surface fill, 1px `line`, control radius. It
 * is written out on each of the four cards below rather than made a shared
 * class, because three of them differ (padding, gap, and the two that take the
 * accent border), and a base-plus-modifier pair for four one-off boxes is more
 * indirection than declaration.
 */
.sf-pdp-rev__summary {
  align-items: start;
  display: grid;
  gap: 24px;
  grid-template-columns: 2fr minmax(0, 1fr);
  margin-top: 44px;
}

.sf-pdp-rev__ai {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 22px 26px;
}

.sf-pdp-rev__ai-head {
  align-items: baseline;
  display: flex;
  gap: 16px;
  justify-content: space-between;
}

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

.sf-pdp-rev__ai-provenance {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
}

/*
 * ─── THE SUMMARY CARD'S TITLE (Ryan's MP2, 2026-08-29) ─────────────────────
 *
 * "Add a title inside the section: 'Summary of all reviews'."
 *
 * IT IS `.sf-pdp-rev__mphotos-title`'s DECLARATION, to the value — support
 * type, 600, full ink. That rule is this section's own sub-head ("Photos from
 * reviews"), and the two label the same kind of thing: a block inside the
 * section. Said twice rather than shared because the two blocks are siblings
 * and neither owns the other — the same call `.sf-pdp-buy__items-eyebrow`
 * makes against the options eyebrow on the buy card.
 *
 * NO BOTTOM MARGIN OF ITS OWN: the card is a `gap: 16px` column, so a margin
 * here would be spacing on top of spacing (the bug class the config row's own
 * note names).
 */
.sf-pdp-rev__ai-title {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 600;
  margin: 0;
}

.sf-pdp-rev__ai-body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  line-height: 1.6;
  margin: 0;
}

.sf-pdp-rev__strong {
  color: var(--brand-color-ink, #17181a);
}

/*
 * ─── TWO SUB-SECTIONS OF ONE SECTION (Ryan's ruling R2, 2026-08-27) ─────────
 *
 * "The AI summary and the pros/cons/best-for are two sub-sections of one
 * section — a slightly darker background band on the pros/cons, more
 * separation; it's getting tight."
 *
 * SO THE CHIPS GET THEIR OWN GROUND, and a hairline above it where the two
 * sub-sections meet. Between them they say "a different thing starts here"
 * without a second heading or a second card.
 *
 * ─── AND THE GROUND IS `paper` NOW (Ryan's ruling PR2, 2026-08-28) ──────
 *
 * "Too dark, too low-contrast — my eye doesn't like it." It was `well` #F1F0EC,
 * one step further down the platform's neutral ladder than it needed to be
 * against a `surface` #FFFFFF card: a 14-per-channel drop, which reads as a grey
 * panel rather than as a second sheet of the same paper. `paper` #FBFBFA is the
 * next rung up the SAME ladder — the page's own ground, four per channel under
 * the card — so the band still separates and no longer sits on the reader.
 *
 * THE ARITHMETIC, since the eye is what is being argued with (computed, not
 * estimated): band-to-card contrast goes 1.140:1 → 1.035:1 — it is a TINT, not
 * a boundary, and the HAIRLINE is what draws the edge, which gets stronger
 * against the lighter ground (1.085:1 on well → 1.195:1 on paper). And every
 * ink on the band improves: the chip label's `ink` #17181A reads 17.158:1 on
 * paper against 15.580:1 on well, and the muted #6C6A62 count 5.233:1 against
 * 4.752:1. Nothing on this band loses contrast by getting lighter; the only
 * thing that weakens is the band's own tint, which is the ruling.
 *
 * IT IS A SUB-THRESHOLD CHANGE, which is exactly the class of edit playbook §4
 * says a pixel gate is BLIND to: paper against well is 10–14 per channel and
 * `well` against `surface` was 14 — both under the acceptance threshold of 24.
 * So it is asserted by computed style in `tools/pdpbelow-gate.mjs` and read at
 * threshold 2, never by the acceptance number alone.
 *
 * IT REACHES THE CARD'S EDGES. The negative margin is the card's own padding
 * (22/26 desktop, 16/18 on the phone, restated at that rung) so the band is a
 * band rather than a tinted rectangle floating inside a white one, and the
 * bottom corners take the card's inner radius — its outer 6px less its 1px
 * border — because the card does not clip and a square corner would poke out of
 * a rounded one.
 *
 * THE SEPARATION IS THE OTHER HALF: 6px of margin over the card's own 16px flex
 * gap makes 22px between the summary's last line and the band's edge, and the
 * band's own 20px of padding puts another 20 between that edge and the first
 * group label.
 *
 * AT THE PHONE RUNG the chips inside this band scroll sideways (ruling 13) and
 * bleed by the CARD's padding; that rule is written against the same numbers,
 * under the 767.98 query, and the two are read together.
 */
.sf-pdp-rev__groups {
  background: var(--brand-color-paper, #fbfbfa);
  border-top: var(--border-hairline);
  border-radius: 0 0 5px 5px;
  display: grid;
  gap: 12px;
  margin: 6px -26px -22px;
  padding: 20px 26px 22px;
  /*
   * `minmax(0, 1fr)`, NOT `1fr`. A bare `fr` floors at its content's MIN-CONTENT
   * width, so the row is as wide as the widest chip row can be — which is fine
   * while the chips wrap and is +321px of sideways document the moment they stop
   * (ruling 13). Measured by `tools/overflow.mjs` at every width ≤767.
   */
  grid-template-columns: minmax(0, 1fr);
}

.sf-pdp-rev__group-label {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-small);
  font-weight: 600;
  margin: 0 0 7px;
}

.sf-pdp-rev__group-note {
  color: var(--brand-color-muted, #6c6a62);
  font-weight: 400;
}

.sf-pdp-rev__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.sf-pdp-rev__chip {
  align-items: center;
  background: none;
  border: var(--border-control);
  /* 12px, the board's own radius on these chips — not the pill the platform's
   * filter chips wear, and not normalised to one. */
  border-radius: 12px;
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-support);
  gap: 7px;
  padding: 5px 11px;
}

/*
 * THE SELECTED STATE IS THE SITE'S SELECTION LANGUAGE (tokens §1: "1.5px accent
 * border = the selection language — never normalize it to 1px"). No board draws
 * a selected theme chip, so this is the PLATFORM rule applied rather than a
 * transplant.
 *
 * BORDER-BOX ARITHMETIC, and it is the one place below the fold that owes any:
 * the border grows by 0.5px on each side, so the padding gives back the same
 * 0.5px and the chip does not move its neighbours when it is pressed.
 */
.sf-pdp-rev__chip--on {
  border: var(--border-selected);
  font-weight: 600;
  padding: 4.5px 10.5px;
}

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

.sf-pdp-rev__aggregate {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 22px 24px;
}

/*
 * CENTRED, NOT BOTTOM-ALIGNED (Ryan's ruling R3, 2026-08-27).
 *
 * MEASURED: at 390 the caption beside the stars wrapped to two lines, which
 * made the side column 60px against the score's 44 — and in a `flex-end` row
 * that put the 4.9 at the bottom of a box whose top half is the stars it is
 * supposed to sit beside. Ryan named both halves of the fix: shorten the
 * caption (done in the content, where the sentence is) AND centre the figure.
 *
 * BOTH, DELIBERATELY. The shortened caption fits one line at 390 and at every
 * width above it; at 320 the measure is 161px and nothing sensible fits, so
 * `center` is what keeps the ruling true where the words cannot.
 */
.sf-pdp-rev__score-row {
  align-items: center;
  display: flex;
  gap: 12px;
}

/*
 * THE 44px MONO SCORE. Above the type scale's 40 and below its 64 — the board's
 * own display-sm step, and the same "money moment" treatment the buy card's
 * 24px mono total gets: a figure set big enough to be the thing you read first.
 */
.sf-pdp-rev__score {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-display-sm);
  font-weight: 700;
  line-height: 1;
}

/* NO `padding-bottom` ANY MORE (ruling R3). The 4px lifted the stars off the
 * bottom of a `flex-end` row; in a centred row it is 4px of nothing pushing the
 * pair up out of centre. */
.sf-pdp-rev__score-side {
  min-width: 0;
}

.sf-pdp-rev__score-stars {
  color: var(--brand-color-accent, #c4530a);
  font-size: 17px;
  letter-spacing: 1px;
}

/* The unfilled tail of any star row, wherever one is drawn. */
.sf-pdp-stars__rest {
  color: var(--brand-color-line-mid, #d8d6d0);
}

.sf-pdp-rev__score-caption {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 2px 0 0;
}

.sf-pdp-rev__bars {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

/*
 * EACH BAR WAS A BUTTON, because the board's own caption said so ("Tap a bar to
 * add that rating to the search"). Ryan retired that on 2026-08-27 (ruling 14):
 * "no searching by 2-star". The rows are `<div>`s now.
 *
 * THE BUTTON RESET WENT WITH THE BUTTON — `background`, `border`, `cursor`,
 * `font-family`, `padding`, `text-align` and `width` were every one of them
 * undoing a UA default that a `<div>` never had. What is left is the board's row
 * geometry, unchanged, which is why the drawing does not move. The `--on`
 * modifier went too: nothing could set it once nothing could press it.
 */
.sf-pdp-rev__bar {
  align-items: center;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-support);
  gap: 10px;
}

.sf-pdp-rev__bar-stars {
  text-align: right;
  width: 14px;
}

.sf-pdp-rev__bar-star {
  color: var(--brand-color-accent, #c4530a);
}

.sf-pdp-rev__bar-track {
  background: var(--brand-color-well, #f1f0ec);
  border-radius: var(--radius-control);
  flex: 1;
  height: 9px;
  overflow: hidden;
  /* A clipping box with no positioned ancestor is how an abs-positioned
   * descendant escapes into the DOCUMENT's overflow (playbook §5). Nothing is
   * absolute in here today; this is the standing precaution the rule asks for. */
  position: relative;
}

.sf-pdp-rev__bar-fill {
  background: var(--brand-color-accent, #c4530a);
  display: block;
  height: 100%;
}

.sf-pdp-rev__bar-count {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-mono, monospace);
  text-align: right;
  width: 48px;
}

/* `.sf-pdp-rev__bars-note` IS GONE with the behaviour it advertised — a caption
 * saying "tap a bar" over bars nothing can tap is worse than no caption. */

.sf-pdp-rev__facts {
  border-top: var(--border-hairline);
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-small);
  gap: 4px;
  padding-top: 12px;
}

.sf-pdp-rev__fact-figure {
  color: var(--brand-color-ink, #17181a);
}

.sf-pdp-rev__search {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 28px;
  padding: 18px 22px;
}

/*
 * THIS CARD ALREADY SPENDS MP5'S AIR (Ryan's MP5, 2026-08-29).
 *
 * `sections.css` gives every configuration row a 14px lead so it stops butting
 * against the field above it — measured at 0 on the photo module and on the
 * answers section. THIS card is the surface that already read 14, and it reads
 * it from the `gap` two declarations up: a margin inside a gapped flex column
 * ADDS to the gap, so the shared rule would make this one row 28. It gives the
 * margin back, and the air stays exactly what Ryan did not report as broken.
 */
.sf-pdp-rev__search > .sf-sec-config {
  margin-top: 0;
}

/*
 * THE DRAWN FIELD. The board's resting state is a 1.5px `line-mid` box with the
 * placeholder in `muted`; its typed state takes the accent border and a 3px
 * accent-soft halo. Both are the board's, and the halo is a `box-shadow`
 * because that is what the board draws — it must not be confused with the focus
 * ring, which is an `outline` and is declared once at the top of this sheet.
 */
.sf-pdp-rev__field {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: 1.5px solid var(--brand-color-line-mid, #d8d6d0);
  border-radius: var(--radius-control);
  display: flex;
  gap: 10px;
  /* THE RIGHT PADDING RESERVES THE X's ROOM (Ryan's DD-C, 2026-08-28).
   * The button is absolutely positioned and takes no width in the row, so
   * "text can never run under it" is the field's to guarantee. */
  padding: 14px 44px 14px 16px;
  /* Hosts the visually-hidden label — see the note on `.sf-pdp-rev__bar-track`. */
  position: relative;
}

.sf-pdp-rev__field--on {
  border-color: var(--brand-color-accent, #c4530a);
  box-shadow: 0 0 0 3px var(--brand-color-accent-soft, #fbefe5);
}

/* THE FLUSH SEAM (Ryan's ruling R5, 2026-08-27 — this field has a suggestion
 * list now). The panel sits on the field's bottom border with no top border of
 * its own, so the two read as ONE control while it is open; the flat corners
 * belong to the box that owns them, which is why this is here and not in the
 * shared panel's sheet. Both sibling ask fields carry the identical rule. */
.sf-pdp-rev__field[data-open="true"] {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.sf-pdp-rev__field-mark {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex: none;
}

/*
 * THE BARE INPUT INSIDE A DRAWN FIELD — tokens §3's ONE blessed case for
 * suppressing the UA outline: the ring moves to the FIELD, because that is the
 * box the reader is looking at. The `:has()` rule is in the focus block above.
 */
.sf-pdp-rev__field-input {
  background: none;
  border: 0;
  color: var(--brand-color-ink, #17181a);
  flex: 1;
  font-family: inherit;
  font-size: var(--text-emphasis);
  min-width: 0;
  outline: none;
  padding: 0;
}

.sf-pdp-rev__field-input::placeholder {
  color: var(--brand-color-muted, #6c6a62);
}

.sf-pdp-rev__showing {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
}

.sf-pdp-rev__showing-left {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* `.sf-pdp-rev__showing-all` IS RETIRED BY NAME (Ryan’s DD-B, 2026-08-28):
 * "the Showing all reviews line is REMOVED". What is left of the row is the
 * word that introduces a pinned theme, drawn only when there is one. */
.sf-pdp-rev__showing-label {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
}




/*
 * THE CELL AND THE LABEL ARE TWO BOXES, as the board draws them. The cell is
 * what `margin-left: auto` pushes right; the label is an INLINE-FLEX inside it,
 * and that nesting is what gives the row its 24px height (see the note beside
 * the markup). Collapsing the pair to one flex child reads 4px short on the
 * whole card.
 */
/*
 * THE PINNED CHIP. 1.5px accent, as every selection on this site is, and the
 * board draws it exactly that way here — this one IS a transplant rather than
 * the platform rule applied.
 *
 * IT LOST ITS SHARED HALF AND GOT IT BACK. This rule was
 * `.sf-pdp-ans__pin, .sf-pdp-rev__pin { … }` until 2026-08-27, when ruling A2
 * retired the answers' Featured pill — and deleting the whole rule took the
 * REVIEWS pin's drawing with it. Caught by `tools/pdpbelow-gate.mjs`'s
 * `revsearchon`, which read 2.558% against a board crop 8px taller: the pin had
 * silently become a plain 1px ink chip.
 *
 * THE ORPHAN SWEEP CANNOT SEE THIS CLASS OF MISTAKE, which is worth writing
 * down: `.sf-pdp-rev__pin` still appeared in this sheet's focus-ring roster, so
 * it was never an unused class — it was a USED class with no drawing. The gate
 * is what catches that, and only because the crop it compares against is a
 * picture rather than a list of names.
 */
.sf-pdp-rev__pin {
  align-items: center;
  background: none;
  border: var(--border-selected);
  border-radius: 12px;
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-support);
  font-weight: 600;
  gap: 6px;
  padding: 5px 11px;
}

.sf-pdp-rev__pin-x {
  color: var(--brand-color-muted, #6c6a62);
  font-weight: 400;
}

/*
 * "Clear" is drawn as a quiet link and IS a button: it changes state on this
 * page rather than going anywhere, and a link that goes nowhere is the shape
 * every keyboard reader has learned to distrust. `.sf-pdp-quiet` gives it the
 * link's colour and underline; these three declarations undo the button.
 *
 * Same restoration as the pin above — it shared its rule with the answers'
 * Clear, which ruling A2 retired.
 */


/*
 * ─── `.sf-pdp-rev__check-cell` IS RETIRED BY NAME (Ryan's FX4, 2026-08-28) ──
 *
 * "The With photos checkbox sits outside the config disclosure — move it INTO
 * the config form as a table row." The cell was what pushed the checkbox to the
 * right of the row beside the search field (`margin-left: auto`), and there is
 * no such row any more: the control is a cell of the DD-A grid, which places it
 * by the grid's own columns. The mobile rule that undid the auto margin when
 * that row wrapped (`.sf-pdp-rev__check { margin-left: 0 }`) retires with it —
 * there is nothing left to wrap.
 *
 * WHAT THE CELL WAS ALSO DOING, and where it went. The old note said the cell
 * and the label were two boxes on purpose: an inline-flex label inside a block
 * gave the row a 24px line box, and collapsing them read 4px short. That was a
 * fact about a FLEX ROW; in the grid the row's height is the taller of the two
 * cells and the label column carries the section's own supporting type, so the
 * shape is the grid's to set. MEASURED after the move rather than assumed —
 * `tools/pdpbelow-gate.mjs`'s two reviews-search crops are re-recorded here,
 * attributed.
 */

/*
 * ─── THE PHOTOS CONTROL, TWICE RETIRED (Ryan's MP3 then WP9, 2026-08-29) ─
 *
 * MP3 retired the checkbox and FIVE names with it: `.sf-pdp-rev__check`,
 * `__check-input`, `__check-box`, the `:checked` tick drawn in CSS, and
 * `__check-count`, along with the FX4 note that explained where `__check-cell`
 * went.
 *
 * WP9 RETIRES WHAT REPLACED IT, and four more names: `.sf-pdp-rev__toggle`,
 * `__toggle-option`, `__toggle-option--on` and `__toggle-count`. They drew two
 * `aria-pressed` chips in a row with an 8px gap, and Ryan's reading is that the
 * gap is the problem — "the chip pair reads multi-select". The note that stood
 * here refusing a shared track ("a track would be a second selection language
 * on a page that already has one") is SUPERSEDED by name: the thing it was
 * protecting was exactly the drawing that misread.
 *
 * WHAT DRAWS HERE NOW is `.sf-seg` in sections.css — the site's segmented
 * either/or, authored for this ruling because the ancestor (photos.css's
 * three-way black segmented scope control) was retired by ruling P4 and
 * nothing segmented survived it. It is a SHARED pattern in the shared sheet,
 * like the pager and the configuration row before it, so the next either/or on
 * any surface draws the same control.
 */
.sf-pdp-rev__results {
  align-items: start;
  display: grid;
  gap: 24px;
  grid-template-columns: 2fr minmax(0, 1fr);
  margin-top: 24px;
}

/*
 * THE COUNT IS THE ONLY THING LEFT IN EITHER HEAD, AND IT RIGHT-ALIGNS.
 *
 * `flex-end`, NOT `space-between`, and the correction is Ryan's own DD-B being
 * made true rather than a new taste. DD-B ruled "review count right-aligns
 * (like photos' galleries count)" and DD-A's note claimed the row got it for
 * free — "the row above is already `space-between`, so putting the button first
 * and the count second puts the count against the right edge". That was true
 * while there were TWO children. `space-between` with ONE child resolves to
 * `flex-start`, so from the moment DD-B moved the sort into the disclosure the
 * reviews count has been drawn on the LEFT, and ruling A4 moving the answers
 * configuration button into the rail would have done the same thing here.
 *
 * ONE DECLARATION SERVES BOTH, as it always did; what changes is that it now
 * says what both rulings asked for in the state both rows are actually in.
 */
.sf-pdp-ans__list-head,
.sf-pdp-rev__list-head {
  align-items: center;
  display: flex;
  gap: 16px;
  justify-content: flex-end;
  margin-bottom: 6px;
}

.sf-pdp-ans__count,
.sf-pdp-rev__count {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  margin: 0;
}

.sf-pdp-ans__count-figure,
.sf-pdp-rev__count-figure {
  color: var(--brand-color-ink, #17181a);
}

/*
 * THE ANSWERS' SORT CONTROL IS DRAWN AS TEXT and is a button anyway. The board
 * draws a bare span with a caret — a picture of a dropdown — and the house rule
 * is that a drawn control which cannot act says so. A `<span>` cannot say so: it
 * is not focusable and carries no state. So it is a button with every button
 * declaration undone, which paints identically to the board's span and can carry
 * `aria-disabled`.
 *
 * THE REVIEWS' SORT NO LONGER SHARES THIS RULE (Ryan's ruling R2, 2026-08-27).
 * It became a REAL choice over two real orders, so it is the platform's quiet
 * dropdown (chrome.css, `.sf-quiet-select`) with a static label beside it —
 * which is a row, not a button. The two are drawn separately now because they
 * genuinely are two different things; the day the answers feed can be reordered,
 * that control follows this one and the rule below is deleted.
 */
/*
 * "Filter by" AND ITS DROPDOWN, on one line (Ryan's ruling A2, 2026-08-27).
 *
 * IT WAS A BUTTON PRETENDING TO BE A DROPDOWN — the board drew a bare span with
 * a caret, and the house rule made it an `aria-disabled` button so it could say
 * it did nothing. It is a real quiet select over a real state now, so the whole
 * of that arrangement (the button reset, the ink `__sort-value`, the drawn ▾)
 * is gone; the control's own sheet is chrome.css.
 *
 * The label is a plain span: the dropdown names itself for a listener
 * (`filterAriaLabel`), so this word is sighted framing and must not be read out
 * a second time.
 */


/*
 * ─── THE CONFIGURATION BUTTON AND ITS ROW (Ryan's ruling PN1, 2026-08-28) ──
 *
 * "Same configuration-button pattern as the photos section: the button toggles
 * the filter-by control, and the result count right-aligns."
 *
 * THE COUNT RIGHT-ALIGNS BY ARITHMETIC, not by a new declaration: the row above
 * is already `space-between`, so putting the button first and the count second
 * puts the count against the right edge. That is why nothing here touches
 * `.sf-pdp-ans__list-head` — the ruling is a markup order, and the row was
 * already written to serve it.
 *
 * THE BUTTON IS THE QUIET SQUARE this site draws for a glyph-only control: the
 * control border, the control radius, ink on surface, and 34px square so the
 * touch target clears the 30px floor the chips take. Pressed open it inverts to
 * the ink fill — the same wayfinding-active state the nav entries take (tokens
 * §4), which is what says "this is the thing holding that row open".
 */
/*
 * `.sf-pdp-ans__config` IS RETIRED BY NAME (Ryan's DD-A, 2026-08-28), and its
 * `--on` state with it. It is `.sf-sec-config__button` in sections.css now,
 * shared with the photo module's and the reviews section's.
 *
 * ITS 34x34 IS PART OF WHAT DD-A FIXED. Two copies of this button each carried
 * their own square — 34 here, 46 on the photo module — so each agreed with the
 * field beside it only at the width where that number happened to match. The
 * shared rule declares no size at all: the row stretches the button to the
 * field's height and `aspect-ratio: 1` makes it square against whatever that
 * turns out to be, which is what Ryan asked for after seeing it squished on a
 * phone.
 */



/*
 * THE ROW IT OPENS. One line under the head row, the controls left-aligned
 * where the button that revealed them is — a disclosure's contents belong under
 * the disclosure, not across the page from it. `flex-wrap` because the row can
 * hold two controls at the photo section's width and one here: a wrap is the
 * honest answer at 320 rather than a squeeze.
 */
/*
 * `.sf-pdp-ans__controls`, `.sf-pdp-ans__filter` and
 * `.sf-pdp-ans__filter-label` ARE RETIRED BY NAME (DD-A). The disclosed row is
 * the shared `.sf-sec-config` grid, whose label column is as wide as the
 * longest label across every pair in it — which is the layout Ryan ruled after
 * a flex row with `flex-wrap` reported back that the two controls could not
 * share a line.
 */




/* "Sort by" and the dropdown, on one line. The label is a plain span: the
 * control names itself for a listener (`sortAriaLabel`), so this word is
 * sighted framing and must not be read out a second time. */




.sf-pdp-rev__theme-answer {
  background: var(--brand-color-surface, #ffffff);
  /* The accent border is the board's, and it is the same "this is the AI's" cue
   * the answers panel wears. */
  border: 1px solid var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 10px;
  padding: 20px 24px;
}

.sf-pdp-rev__quote {
  border-left: 3px solid var(--brand-color-line, #e8e7e3);
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  line-height: 1.55;
  margin: 0;
  padding: 6px 14px;
}

.sf-pdp-rev__quote-meta {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-small);
  margin-top: 4px;
}

.sf-pdp-rev__quote-stars {
  color: var(--brand-color-accent, #c4530a);
  font-size: 12px;
  letter-spacing: 1px;
}

.sf-pdp-rev__item {
  border-top: var(--border-hairline);
  display: flex;
  gap: 20px;
  padding: 22px 0;
}

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

.sf-pdp-rev__item-head {
  align-items: center;
  display: flex;
  gap: 12px;
}

.sf-pdp-rev__stars {
  color: var(--brand-color-accent, #c4530a);
  flex: none;
  font-size: var(--text-body);
  letter-spacing: 1px;
}

.sf-pdp-rev__item-title {
  /*
   * NO TRACKING. globals.css gives every h1/h2/h3 `letter-spacing: -0.015em`,
   * and PDPBelow.dc.html draws these headings with none — measured at -0.315px
   * on a 21px claim, which re-shapes the whole line and read 1.1–2.1% on every
   * engineering chapter before this declaration landed. The section TITLE is a
   * different case and keeps the platform's tracking: it is the shared
   * `.sf-sec-head__title`, which three other boards do draw tracked.
   */
  letter-spacing: normal;

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

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

.sf-pdp-rev__verified {
  color: var(--brand-color-good, #1f7a4d);
}

.sf-pdp-rev__item-body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  line-height: 1.6;
  margin: 10px 0 0;
}

.sf-pdp-rev__item-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.sf-pdp-rev__tag {
  border: var(--border-hairline);
  border-radius: 12px;
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-caption);
  padding: 3px 9px;
}

.sf-pdp-rev__reply {
  background: var(--brand-color-paper, #fbfbfa);
  border-left: 3px solid var(--brand-color-nav-text, #d9e8ff);
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-support);
  margin: 12px 0 0;
  padding: 8px 12px;
}

.sf-pdp-rev__reply-lead {
  color: var(--brand-color-nav, #192950);
}

.sf-pdp-rev__item-foot {
  align-items: center;
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  font-size: var(--text-small);
  gap: 14px;
  margin-top: 12px;
}

/* The board's `.ghost-sm`, at its own 500 weight on the vote. */
/*
 * THE VOTES TAKE THE COMPACT RUNG (Ryan's "a button is a button", 2026-09-01).
 *
 * They drew 33px at 12.5, and the pair disagreed with ITSELF about weight — the
 * answer's vote was 600 and the review's 500, in the same drawn box, on the same
 * page. Both of those were in Ryan's count of "five font sizes, font weights".
 *
 * THE BOARD'S `.ghost-sm` IS THE THING BEING RETIRED HERE, and it is worth
 * saying why rather than just doing it: a third, smaller button size existed so
 * that a vote could look incidental. The ruling is that incidental-ness is not a
 * size — it is the GHOST treatment, which these keep. What they lose is the
 * private rung.
 */
.sf-pdp-ans__vote,
.sf-pdp-rev__vote {
  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);
}

/*
 * THE PHOTO STRIP (Ryan's ruling R4, 2026-08-27): one 120×90 well became
 * SEVERAL square thumbnails that scroll sideways and open the shared viewer.
 *
 * A SCROLLER AT EVERY WIDTH, not only on a phone. The column beside a review is
 * 120px wide on the desktop composition too, so three thumbnails have to travel
 * there as well — one rule, one behaviour, and no width at which the same strip
 * means two different things.
 *
 * THE SCROLLBAR IS HIDDEN AND THE SCROLLING IS NOT — the site's standing
 * treatment for a strip of tiles (photos.css's filter row, sections.css's quote
 * strip): both spellings, because they are different engines, and both are
 * PAINT. The strip still takes a wheel, a drag, a swipe and the arrow keys.
 *
 * IT IS NOT A TAB STOP, and that is Chrome's own rule rather than a choice: a
 * scroller with focusable children is not made focusable, and every tile in here
 * is a button. `tools/focusgate.mjs` enumerates it by that rule and agrees.
 */
.sf-pdp-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;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  /* THE COLUMN THE BOARD DREW ONE WELL IN. The strip is exactly as wide as the
   * well it replaces, so the review's own measure beside it does not move. */
  width: 120px;
}

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

/*
 * 120 SQUARE with no padding, so the content box and the drawn box agree.
 * "Square-ish" is the ruling's word and square is what it means: a strip of
 * mixed aspect ratios reads as a mistake rather than as a variety.
 *
 * THE BUTTON RESET IS FOUR DECLARATIONS — `background` is set below, so what is
 * left is the border, the padding and the pointer. The tint and the mono caption
 * are the same placeholder every board draws.
 */
.sf-pdp-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 (2026-08-31, the real library). The
   * `<img>` below is absolutely positioned so the mono well stays underneath
   * it, unconditionally, and a strip with no assets draws exactly the pixels it
   * always drew. Declared here rather than left to the page for the reason the
   * photo grid's tiles record: an absolutely positioned child whose ancestors
   * are all static takes the PAGE as its containing block — the sr-span
   * overflow trap (playbook §5), seen from the other side. */
  position: relative;
  scroll-snap-align: start;
  width: 120px;
}

/*
 * ONE REVIEW'S PHOTOGRAPH. Same rule as the photo grid's `.sf-ph__img`, and
 * deliberately a second declaration rather than a shared class: this is
 * pdp.css's strip and that is photos.css's grid, the two sheets are loaded
 * independently by different surfaces, and a class that crossed between them
 * would make either sheet unable to change its own tiles.
 */
.sf-pdp-rev__item-photo-img {
  border-radius: inherit;
  display: block;
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

.sf-pdp-rev__well-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, monospace);
  /* The placeholder-caption exemption again (tokens §2). */
  font-size: 9px;
}

.sf-pdp-rev__empty {
  border-top: var(--border-hairline);
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  margin: 0;
  padding: 22px 0;
}

/*
 * ─── BOTH PAGERS RETIRE INTO THE SHARED ONE (ruling PP5, 2026-08-28) ─────
 *
 * "Make the pager IDENTICAL across Reviews, Customer photos and Answers —
 * consistency is King." `.sf-pdp-ans__pager*`, `.sf-pdp-rev__pager*`,
 * `.sf-pdp-ans__page*` and `.sf-pdp-rev__page*` are RETIRED BY NAME: the drawing
 * they held is `.sf-sec-pager*` in `sections.css` now, and the markup is
 * `SectionPager`. Every declaration moved unchanged except the two the sheets
 * disagreed about, each settled with its reason at the promotion.
 *
 * ─── AND THE REVIEWS PAGER GETS ITS BUFFER (Ryan's ruling PR3, 2026-08-28) ─
 *
 * "There's no buffer between the reviews pager and the Customer photos section
 * — it sits right on the line." What he is seeing is two rules meeting: the
 * pager's own hairline, and then the next section's head 80px later with nothing
 * between the pager's last button and the bottom of the reviews section. The
 * pager was the final element of the list column, so its buttons ended the
 * section flush.
 *
 * 40px UNDER IT, which is the page's own section rhythm halved — the distance a
 * closing element takes from the edge of its section everywhere else on this
 * page (the answers list takes the same). It is on the REVIEWS section's own
 * results block rather than on the shared pager, because it is a property of
 * where this pager sits and not of what a pager is: the answers pager is the
 * last thing on the page before the footer, which owns its own space (ruling
 * A3), and the photo pager already has the grid's own air under it.
 */
.sf-pdp-rev__list > .sf-sec-pager {
  margin-bottom: 40px;
}

/*
 * ─── THE PHONE'S PHOTO STRIP (Ryan's ruling R4, 2026-08-27) ─────────────────
 *
 * Below the 1024 cut the panel below stops being a rail and stacks under the
 * whole list, where it "was getting lost". At that rung the photographs move to
 * the TOP of the section instead — one scrolling row directly under the
 * count/sort bar, wearing the same hairline the first review wears, so the
 * sequence reads bar / rule / photographs / rule / first review.
 *
 * `display: none` ABOVE THE CUT, and the panel is `display: none` below it —
 * the two placements are never both drawn, and the desktop rail is untouched
 * because Ryan likes it there. The rung is 1023.98 rather than the phone cut
 * because 1024 is exactly where the panel stops being a rail: the ruling's
 * reason is "it stacks at the bottom", and that is true of every width below
 * this one.
 *
 * MINIMAL, WHICH IS THE RULING'S OWN WORD: a title, then square tiles that
 * scroll sideways. Same tile as a review's own photo strip — 120px, `well`
 * ground, inner radius — so a reader meets one kind of review photograph on
 * this page and not two.
 *
 * THE ESTABLISHED SCROLLER, down to the hidden bar (Ryan, 2026-08-26: the
 * overlay bar paints across the things the reader is choosing) and the
 * `position: relative` that keeps an abs-positioned descendant from escaping
 * into the document's overflow (playbook §5).
 */
.sf-pdp-rev__mphotos {
  border-top: var(--border-hairline);
  display: none;
  padding-top: 16px;
}

.sf-pdp-rev__mphotos-title {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 600;
  margin: 0 0 10px;
}

.sf-pdp-rev__mphotos-strip {
  display: flex;
  gap: 8px;
  /* Full bleed to the page's edges, gutter given back as padding, so the first
   * tile starts on the text's left edge and the last can reach the screen. */
  margin-inline: calc(-1 * var(--sf-gutter));
  overflow-x: auto;
  padding-inline: var(--sf-gutter);
  position: relative;
  scrollbar-width: none;
}

.sf-pdp-rev__mphotos-strip::-webkit-scrollbar {
  display: none;
}

/*
 * THE NEXT WINDOW OF THE STRIP (Ryan's server-pagination ruling, 2026-08-31).
 *
 * The strip is a WINDOW now — 24 photographs, server-rendered, with the rest a
 * press away — so it needs a control that says so. It is drawn as the section's
 * quiet link (`.sf-pdp-quiet` carries the type, the colour and the underline),
 * and this rule is only its PLACE: a block on its own line under the row, so it
 * cannot be mistaken for one more tile at the end of the scroller.
 *
 * IT MATCHES NOTHING ON THE AUTHORED PAGE. Nine photographs fit in one window,
 * so the element is never rendered there — which is why adding this declaration
 * moves no recorded pixel.
 */
.sf-pdp-rev__mphotos-more {
  display: inline-block;
  margin-top: 10px;
}

.sf-pdp-rev__mphoto {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border: 0;
  border-radius: var(--radius-inner);
  /*
   * ─── `content-visibility: auto` WAS BUILT HERE AND TAKEN BACK OUT ──────
   *
   * MP4's second half asks that the strip not load everything at once, and
   * skipping the render of off-screen tiles looked like the CSS half of that.
   * It is NOT shipped, and the reason is measured rather than theoretical:
   * with it declared, every tile outside the viewport draws EMPTY in a
   * document-coordinate `Page.captureScreenshot` — which is how every pixel
   * gate on this page captures. The placeholder labels vanished from the
   * capture while being perfectly fine on screen. That is a gate the next
   * pass would have had to argue with, bought for no saving this strip can
   * bank today: its tiles hold a mono label, not a photograph.
   *
   * WHAT IS SHIPPED for MP4's second half is `loading="lazy"` on the tile's
   * `<img>` (see `pdp-reviews.tsx`), which is where the bytes actually are.
   * FLAGGED for the day the real feed lands: at the corpus this section
   * quotes (916 reviews with photographs) this row is ~900 DOM buttons, and
   * a DOM WINDOW — not a rendering hint — is the answer to that. It is a new
   * interaction and gets its behaviour written in words first (Rule zero).
   */
  cursor: pointer;
  display: flex;
  flex: none;
  height: 120px;
  justify-content: center;
  /* The tile crops its photograph to the square, so the rounded corners are
   * the tile's. `position: relative` with it, per playbook §5 — an
   * `overflow != visible` box that could ever host an abs-positioned
   * descendant needs to be that descendant's containing block, or the
   * descendant escapes into the document's scrollable overflow. */
  overflow: hidden;
  padding: 0;
  position: relative;
  width: 120px;
}

/* THE PHOTOGRAPH, when a tile has one. It fills the tile and the tile keeps
 * the frame: `object-fit: cover` so a landscape review photograph is cropped
 * to the square rather than letterboxed into it, which is what the media
 * viewer behind the tile does with the same frame. */
.sf-pdp-rev__mphoto-img {
  height: 100%;
  object-fit: cover;
  width: 100%;
}

.sf-pdp-rev__photos {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  padding: 18px 20px;
  position: sticky;
  /* THE WHOLE OFFSET CASCADE, buy bar included — it pins at the top at every
   * width since ruling 8, so every sticky on this page has to clear it. */
  top: calc(
    var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px) +
      var(--sf-pdp-buybar-h, 0px) + 20px
  );
}

.sf-pdp-rev__photos-head {
  align-items: baseline;
  display: flex;
  gap: 12px;
  justify-content: space-between;
  margin-bottom: 10px;
}

.sf-pdp-rev__photos-title {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 600;
  margin: 0;
}

.sf-pdp-rev__photos-count {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
  white-space: nowrap;
}

.sf-pdp-rev__photos-grid {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.sf-pdp-rev__photo-tile {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border-radius: var(--radius-inner);
  display: flex;
  height: 96px;
  justify-content: center;
  /* The photograph's containing block (2026-08-31) — same reason as
   * `.sf-pdp-rev__item-photo`'s. */
  position: relative;
}

/* The rail's photograph, filling its well. See `.sf-ph__img` for the rule. */
.sf-pdp-rev__photo-tile-img {
  border-radius: inherit;
  display: block;
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

/*
 * THE SIZE IS ON THE ANCHOR, NOT ON THE BLOCK, and that is the board's own
 * shape rather than tidiness: the board's wrapper inherits the page's 16px and
 * sets 14.5 on the `<a>` inside it, so the line box is the 16px STRUT (20px) and
 * not the link's own 18. Moving the declaration up to the block reads 2px short,
 * which is 2px of the panel's height.
 */
.sf-pdp-rev__photos-link {
  margin: 12px 0 0;
}

/*
 * "See all photos in these reviews" IS A BUTTON NOW (ruling R6) and is drawn
 * exactly as the quiet link it replaces. The reset is the four declarations a
 * `<button>` needs to stop looking like one; `.sf-pdp-quiet` supplies the rest.
 */
.sf-pdp-rev__photos-all {
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  padding: 0;
}

.sf-pdp-rev__photos-all,
.sf-pdp-rev__photos-link a {
  font-size: var(--text-support);
}

/* `.sf-pdp-rev__photos-note` IS GONE (ruling R7) — "Photos follow the search —
 * filter to 'Stable' and you see stable hoops" explained a behaviour a reader
 * watches happen. */

/* ----------------------------------------------------------- answers --- */

/*
 * THE ASK FIELD IS THE AI EMBED'S FIELD NOW (Ryan's ruling A3, 2026-08-27), and
 * the card it used to sit inside is gone with the pill row it held. What is left
 * is the DRAWN FIELD itself — the box the reader types in — which is why these
 * declarations moved from `.sf-pdp-ans__field` up onto the form.
 *
 * `position: relative` is load-bearing: the suggestion dropdown is absolutely
 * positioned against this box, and a menu whose containing block fell through to
 * the page would hang off the document (playbook §5).
 */
/*
 * --- A4: THE SECTION IS TWO COLUMNS ON DESKTOP -----------------------------
 *
 * Ryan, 2026-08-27: "the AI embed + search move to a PINNED RIGHT RAIL (sticky
 * once the answers scroll), long AI answers welcome in the rail; the FAQ list =
 * the search results, left column. Fixes the narrowed-for-readability column
 * leaving a dead right side."
 *
 * THE DEAD RIGHT SIDE IS THE MEASUREMENT THE RULING RESTS ON. Ruling A3 capped
 * the results at an 860px reading measure inside a content column that is
 * 1,082px wide once the below-the-fold nav has taken its 190 + 40 - so 222px of
 * this section was nothing at every desktop width. The rail is exactly that
 * space put to work.
 *
 * 360px IS THE FLOW RAIL'S OWN WIDTH, less its gap. The order flow's rail was
 * ruled to 380/gap-32 on 2026-08-27 ("give the dense order room to breathe");
 * this column carries prose rather than a priced order and sits inside a column
 * that is already 230px narrower than the flow's page, so it takes the 360 the
 * flow's steps drew before that ruling and the same 32px gap. MEASURED: 1,082 -
 * 32 - 360 = 690 for the results, which is inside the 860 cap they were already
 * capped to and above the ~640 where an answer's body starts to look like a
 * column of a newspaper.
 *
 * THE ORDER IS SWAPPED IN THE DRAWING, NOT IN THE DOM. The embed is FIRST in
 * markup - ruling A3's whole point is that the reader asks at the TOP - and
 * `grid-column` puts it on the right here. A DOM order matching the desktop
 * picture would put the ask field below the list on a phone, which is the panel
 * this section already deleted once.
 */
.sf-pdp-ans__grid {
  display: grid;
  gap: 32px;
  grid-template-columns: minmax(0, 1fr) 360px;
}

/*
 * THE TWO PLACEMENTS, TOGETHER AND BEFORE THE QUERY THAT UNDOES THEM.
 *
 * They are one fact - which track each column takes - so they are one place,
 * and that place has to be ABOVE the `max-width: 1239.98px` block: a media
 * query carries no extra specificity, so a placement declared after it wins on
 * source order at every width. That is not a hypothetical; the first cut wrote
 * the results' placement two hundred lines further down and the stacked rungs
 * drew the ask field UNDER the whole list.
 */
.sf-pdp-ans__results {
  grid-column: 1;
  grid-row: 1;
}

.sf-pdp-ans__embed {
  grid-column: 2;
  grid-row: 1;
  /* THE MEASURE CAP IS RETIRED HERE (A4). It was 860 - "the same measure as the
   * list it fills" - which was the right answer while the field ran across the
   * top of a single column. In the rail the track width IS the measure. */
  margin-top: 26px;
}

/*
 * THE PIN IS ON AN INNER BOX, NOT ON THE COLUMN.
 *
 * MEASURED THE HARD WAY TWICE ALREADY ON THIS PAGE (`.sf-pdp-nav`, and the
 * order flow's one-screen band): a sticky element travels only within its
 * CONTAINING BLOCK, and a grid item's containing block is its own grid area. A
 * sticky grid item is therefore pinned inside a box exactly its own height and
 * releases on the first pixel of scroll. The column stays a stretched grid item
 * - `align-items` is left at its `stretch` default so the area is the ROW's
 * height, which is the results column's - and this box sticks inside it.
 *
 * THE OFFSET IS THE CASCADE, not a number, and it is the SAME EXPRESSION the
 * below-the-fold nav rail uses twelve hundred lines up: the chrome, the title
 * band (0 on a product page, written anyway so the rule stays correct on a page
 * that grows one), the buy bar, and the rail's own 20px of air. Two rails on one
 * page that pinned at different heights would read as a mistake.
 */
.sf-pdp-ans__embed-inner {
  position: sticky;
  top: calc(
    var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px) +
      var(--sf-pdp-buybar-h, 0px) + 20px
  );
}

/*
 * --- WHERE THE RAIL STOPS, AND HOW THE CUT WAS DERIVED (A4) ---
 *
 * "Mobile keeps the stacked arrangement." The cut is 1239.98 - a rung that is
 * already on this site's ladder (chrome.css: 767.98 / 833.98 / 919.98 / 1023.98
 * / 1039.98 / 1239.98) - and it is MEASURED rather than borrowed from the rails
 * beside it.
 *
 * THE FLOOR IS THIS PAGE'S OWN PRECEDENT. The reviews section two sections up
 * already runs a results column beside a rail, and its narrowest is 428px at
 * 1024. So the bar for the answers results column is "no narrower than the one
 * this page already ships". Measured, with the rail on:
 *
 *     width   content col   results col
 *     1024        666           274      <- far under the 428 floor
 *     1100        742           350
 *     1200        842           450      <- clears it
 *     1240        882           490
 *     1440       1082           690      (the layout cap; 1600 is identical)
 *
 * The crossing is ~1178. Taking a LADDER rung above it rather than inventing a
 * 1177.98 keeps the page's boundaries countable, and 1239.98 buys 62px of
 * margin over the floor for a content edit that runs long.
 *
 * THE FIRST CUT WAS 1023.98 - the rung every other rail on this page falls at -
 * and the sweep is what refused it: at 1024 the answers list would have been
 * 274px, which is narrower than the flow's ORDER RAIL. Two rails falling at
 * different widths is a smaller cost than a 274px column of body copy.
 *
 * THE STACK IS THE ONE RULING A3 SHIPPED: the ask field FIRST, the results under
 * it. It comes free, because the rail is first in the DOM all along - dropping
 * the explicit placements is the whole of the change.
 */
@media (max-width: 1239.98px) {
  .sf-pdp-ans__grid {
    gap: 0;
    grid-template-columns: minmax(0, 1fr);
  }

  .sf-pdp-ans__embed,
  .sf-pdp-ans__results {
    grid-column: auto;
    grid-row: auto;
  }

  /*
   * AND THE PIN GOES WITH IT. A sticky box in a single-column flow follows the
   * reader down past content it has nothing to do with - the exact failure
   * ruling R4 names one section up, where the photo panel "was getting lost"
   * stacked under the list. Here it would be worse: the block that pinned would
   * be the ask field, over the answers it had just found.
   */
  .sf-pdp-ans__embed-inner {
    position: static;
  }
}

.sf-pdp-ans__ask {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: flex;
  gap: 10px;
  /* THE RIGHT PADDING RESERVES THE X's ROOM (Ryan's DD-C, 2026-08-28) — "so
   * text can never run under it". The button is absolutely positioned and takes
   * no width in the row, so the reserve is the field's to make. */
  padding: 14px 44px 14px 16px;
  position: relative;
}

/* The field and its menu read as ONE box while the menu is open — the category
 * embed's own treatment, and the reason the menu below has no top border. */
.sf-pdp-ans__ask[data-open="true"] {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* THE ACCENT BORDER FOLLOWS FOCUS, not a filter — the R1 lesson from the
 * reviews field, applied here before it could be made twice. */
.sf-pdp-ans__ask:focus-within {
  border-color: var(--brand-color-accent, #c4530a);
}


.sf-pdp-ans__field-mark {
  align-items: center;
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex: none;
}

.sf-pdp-ans__field-input {
  background: none;
  border: 0;
  color: var(--brand-color-ink, #17181a);
  flex: 1;
  font-family: inherit;
  font-size: var(--text-emphasis);
  min-width: 0;
  outline: none;
  padding: 0;
}

.sf-pdp-ans__field-input::placeholder {
  color: var(--brand-color-muted, #6c6a62);
}




/*
 * ONE COLUMN NOW (ruling A3). This was the 2fr/1fr grid that put the written
 * answers on the left and the AI panel on the right; the panel is gone, so the
 * list has the section's whole measure. The class stays because the list is
 * still a block with its own top margin, and renaming it would be churn.
 *
 * IT KEEPS ITS 860px CEILING — the same reading measure the Description
 * section's column holds, and for the same reason the board gives there: body
 * copy past ~72 characters stops being readable. The list used to get that
 * measure by having a panel beside it.
 */
.sf-pdp-ans__results {
  display: grid;
  gap: 24px;
  /* A4: THE LEFT TRACK. It was the only column in the section; it is now the
   * RESULTS column with the rail beside it, and the 860px cap it kept from
   * ruling A3 is left declared rather than removed — at the desktop rung the
   * track resolves to 690 and the cap does nothing, and it goes on doing its
   * job at the rungs below the rail's cut where this is the whole width.
   *
   * ITS GRID PLACEMENT IS DECLARED UP WITH THE RAIL'S, not here. Media queries
   * add no specificity, so a placement written BELOW the query that undoes it
   * wins on source order — measured: the stacked rung drew the ask field under
   * the whole list, which is the panel ruling A3 deleted, restored by a
   * cascade accident. */
  grid-template-columns: minmax(0, 1fr);
  margin-top: 24px;
  max-width: 860px;
}

.sf-pdp-ans__item {
  border-top: var(--border-hairline);
  padding: 24px 0;
}

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

.sf-pdp-ans__question {
  /*
   * NO TRACKING. globals.css gives every h1/h2/h3 `letter-spacing: -0.015em`,
   * and PDPBelow.dc.html draws these headings with none — measured at -0.315px
   * on a 21px claim, which re-shapes the whole line and read 1.1–2.1% on every
   * engineering chapter before this declaration landed. The section TITLE is a
   * different case and keeps the platform's tracking: it is the shared
   * `.sf-sec-head__title`, which three other boards do draw tracked.
   */
  letter-spacing: normal;

  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;
  line-height: 1.3;
  margin: 0;
  text-wrap: wrap;
}

/*
 * THE SHORT ANSWER IS THE BOLD LEAD SENTENCE of this paragraph — the board's
 * pass-2 fix for the double title it used to draw (a bold short answer above a
 * body that repeated it).
 */
.sf-pdp-ans__body {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  line-height: 1.65;
  margin: 12px 0 0;
}

.sf-pdp-ans__lead,

/*
 * ─── THE SEPARATOR NEVER STARTS A LINE (Ryan's MP8, 2026-08-29) ────────────
 *
 * "'Watch the installation video · Pier kit hole pattern diagrams' — when the
 * pair wraps, the dot separator makes line 2 read as NESTED under line 1."
 *
 * It did. Each link is a flex item carrying its own leading "· ", so a wrap put
 * the dot at the START of line 2, where the eye reads it as a bullet and the
 * second link as a child of the first.
 *
 * THE FIX IS THAT THE DOT COSTS NO LAYOUT AND IS CLIPPED WHERE IT WOULD BE
 * WRONG, which is the one CSS-only route that holds at EVERY width rather than
 * at a chosen rung:
 *
 *   · the row spends its separator space as a `column-gap`, so the items
 *     themselves are what wrap and the gap is what disappears at a line end;
 *   · the dot is `position: absolute`, hung to the LEFT of its own item — so it
 *     is not a box the wrapping algorithm can place anywhere;
 *   · the row is `overflow: clip`, so a dot hanging left of an item that starts
 *     a line falls outside the row's box and is not painted. An item mid-line
 *     hangs its dot into the gap, which is inside the box, and it paints.
 *
 * `overflow-clip-margin` IS WHY THIS IS `clip` AND NOT `hidden`. A plain
 * `overflow: hidden` would also clip the focus RING these links take — 2px of
 * outline at a 2px offset, so 4px beyond the border box, on the top and bottom
 * lines and on any link at the left edge. The clip margin holds exactly that
 * 4px open, so the ring is untouched and the dot (which starts 10px out) is
 * not. `overflow: hidden` would also have made this a scroll container, which
 * is the sr-span overflow trap the playbook records; `clip` is not one.
 *
 * ARITHMETIC, so the spacing is the one that shipped: the retired rule drew
 * `margin: 0 10px` around the dot, so a same-line pair read
 * 10 + dot + 10. The gap is 24px and the dot hangs 10px from its own item's
 * left edge, which puts the same 10px on each side of it.
 *
 * NOT `white-space: nowrap` on the items: a single link longer than the measure
 * would then overflow, and `overflow: clip` would CUT IT OFF rather than let it
 * wrap. The links wrap internally; only the dot is pinned.
 */
.sf-pdp-ans__links {
  align-items: center;
  column-gap: 24px;
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-body);
  margin: 14px 0 0;
  overflow: clip;
  /* The focus ring's own extent: 2px of outline at a 2px offset. */
  overflow-clip-margin: 4px;
  row-gap: 4px;
}

/* The containing block the dot is hung from — its OWN item, so "left of me" is
 * a position the item can be asked for at any width. */
.sf-pdp-ans__links > * {
  position: relative;
}

.sf-pdp-ans__link-sep {
  color: var(--brand-color-muted, #6c6a62);
  margin-right: 10px;
  position: absolute;
  right: 100%;
}

.sf-pdp-ans__helpful {
  align-items: center;
  display: flex;
  gap: 10px;
  margin-top: 14px;
}

.sf-pdp-ans__helpful-prompt {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
}

.sf-pdp-ans__empty {
  border-top: var(--border-hairline);
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-body);
  margin: 0;
  padding: 24px 0;
}

/*
 * THE BOTTOM AI PANEL IS GONE (Ryan's ruling A3, 2026-08-27) and with it every
 * rule that drew it: `.sf-pdp-ans__panel` and its head, mark, name, tagline,
 * blurb, examples, footnote, answer, provenance and reply field. PDPBelow.dc.html
 * drew all of them, in two states, and the whole surface is replaced by the ask
 * field's own conversation ABOVE the list — the category table's embed grammar,
 * with a search tool in place of the highlight tools.
 *
 * The panel's own sticky `top` went too, which removes one consumer from the
 * offset cascade.
 *
 * ─── THE EMBED'S CONVERSATION ───────────────────────────────────────────────
 *
 * SCOPE HONESTY, recorded here as well as in `pdp-answers-embed.tsx`: these are
 * the SECOND copy of the embed's drawing — `category.css` holds the first, under
 * `sf-cat-embed-`. Extracting a shared core out from under a live surface was
 * ruled out of this pass, so the vocabulary is matched deliberately (same card,
 * same bubble geometry, same dot rhythm) and the promotion is FLAGGED as due:
 * two copies is the bar this codebase uses (see the head of sections.css).
 */

/* --------------------------------------------------- the suggestion menu -- */

/*
 * THE WHOLE BLOCK IS RETIRED BY NAME (DD-UNIFY, Ryan's ruling 2026-08-28).
 *
 * What stood here: `.sf-pdp-ans__menu`, `.sf-pdp-ans__menu-list`,
 * `.sf-pdp-ans__menu-eyebrow`, `.sf-pdp-ans__option`, its `:last-child` and its
 * `--active`/`:hover` pair — a hand copy of `.sf-cat-embed__*` in category.css,
 * declaration for declaration, and the duplication this file's own header
 * flagged as due for promotion.
 *
 * THE FLAG IS DISCHARGED for the dropdown's half: both are `.sf-suggest*` in
 * chrome.css now, verbatim, and a third surface (the photo module's facet card)
 * converged onto them. `tools/aigate.mjs` and `tools/pdpbelow-gate.mjs` hold
 * the crops that prove this drawing did not move.
 *
 * `.sf-pdp-ans__option:focus-visible` LEFT THE RING ROSTER at the top of this
 * sheet with it — an option row is not focusable in the combobox pattern (focus
 * stays in the field and `aria-activedescendant` names the row), so that
 * selector was dead ink even before the class went.
 *
 * WHAT STAYED HERE is the FIELD's half of the seam — `.sf-pdp-ans__ask`, its
 * `[data-open="true"]` radius rule and `__field-input` — and the conversation
 * below, which is still a second copy and is still flagged.
 */

/* ---------------------------------------------------- the conversation -- */

/*
 * THE SECOND COPY IS GONE, AND THE FLAG ABOVE IS FULLY DISCHARGED (the
 * shared-embed-core extraction, 2026-08-28).
 *
 * The block a few lines up said: "WHAT STAYED HERE is the FIELD's half of the
 * seam ... and the conversation below, which is still a second copy and is
 * still flagged." This is that copy, retired by name:
 *
 *   `.sf-pdp-ans__convo`, `__convo:focus`, `__from-user`, `__bubble`,
 *   `__from-ai`, `__ai-mark`, `__answer`, `__answer-strong`,
 *   `__answer-provenance`, `__thinking`, `__dots`, `__dot`,
 *   `__dot:nth-child(2)`, `__dot:nth-child(3)`, `@keyframes sf-pdp-ans-think`,
 *   its reduced-motion rule, `__convo-actions` and `__continue`.
 *
 * They are `.sf-embed*` in `sections.css` now - ONE drawing for the category
 * table's embed, this one, and the reviews search that ruling R5 turns into a
 * third. `.sf-pdp-ans__continue:focus-visible` left the ring roster at the top
 * of this sheet with the button.
 *
 * TWO OF THEM DID NOT MOVE, THEY DIED (Ryan's AI1): `__from-user` and
 * `__bubble` drew the reader's own question a second time, and the ruling
 * leaves the query in the field it was typed in.
 *
 * WHAT STAYED IN THIS SHEET is the FIELD's box - `.sf-pdp-ans__ask`, its
 * `[data-open]` radius rule, its mark and its input - plus the section's own
 * two-column grid and the rail's pin, which are ruling A4's and are declared
 * beside `.sf-pdp-ans__embed`.
 */


/* ------------------------------------------- visually-hidden field names -- */

/*
 * Three labels that name a field a reader can already see the purpose of. Each
 * field is drawn with its own placeholder and its own magnifier, and a
 * placeholder is not an accessible name — it disappears the moment anything is
 * typed. So the name is real and clipped.
 *
 * `.sf-pdp-sr` JOINED THEM on 2026-08-26 with the gallery's openers, and for the
 * same reason one rule rather than a second one: those buttons are transparent
 * boxes over a photograph, so their whole accessible name is text nothing draws.
 * ONE visually-hidden rule per sheet, which is what keeps the `clip-path`
 * treatment below from being re-derived (and got wrong) somewhere else in it.
 *
 * CLIPPED, NOT MOVED OFF-SCREEN. A `left: -9999px` span is an out-of-flow box
 * whose containing block is whatever is positioned above it, which is exactly
 * how a hidden span grows the DOCUMENT's scrollable overflow (playbook §5).
 * `clip-path` keeps the box where it is and paints none of it; every field that
 * hosts one is `position: relative` besides.
 */
.sf-pdp-ans__field-label,
/* `.sf-pdp-ans__reply-label` left with the panel's reply field (ruling A3). */
.sf-pdp-rev__field-label,
.sf-pdp-sr {
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* --------------------------------------------- derived responsive rungs -- */

/*
 * NO BOARD DRAWS ANY OF THIS. PDPBelow.dc.html is a single 1440 artboard, so
 * everything below is derived, and it is kept to the one move that a two-column
 * layout has to make: stack. The rungs are the ladder chrome.css already
 * publishes (1023.98 and 767.98), so a reader following the site's breakpoints
 * finds these where the others are and no new cut enters the ladder.
 */

@media (max-width: 1023.98px) {
  /*
   * THE 250px RAIL beside an 860px measure is 1,150px of content before any
   * gutter, so below the sticky-pin cut the two columns stack and the rail goes
   * over the column it names.
   *
   * AND IT BECOMES A CHIP STRIP RATHER THAN A LIST, which is the one derived
   * decision here that is more than "stack it". Stacked as a column the rail is
   * 323px of navigation before the first word of the first chapter — measured,
   * on a 390px screen. As a horizontal scroller it is 36px, and it is not a new
   * shape: it is the site's own chip-row pattern, the one `.sf-ph__filters`
   * already uses for the photo module's tags, down to the hidden scrollbar
   * (Ryan, 2026-08-26: the overlay bar paints across the pills the reader is
   * choosing).
   *
   * IT PINS AT THIS WIDTH (Ryan's ruling 7, 2026-08-27), where the note in this
   * comment used to say it must not. The objection was that a horizontal strip
   * pinned under the chrome "would sit on top of the chapter it points at" —
   * true of a TRANSPARENT strip. Pinned, it is a BAND: it carries the page's
   * paper, a hairline under it, and the chapters scroll beneath it exactly as
   * they scroll beneath the chrome. Ruling 8 is what made that affordable — with
   * the rail note gone the sticky column is nothing but the chips, so the band
   * is 44px rather than 44 plus a paragraph.
   *
   * THE OFFSET IS THE CASCADE, not a number. `--sf-pinned-offset` is the chrome
   * (54 while stuck) and `--sf-band-stuck-h` is the title band under it — 0 on a
   * product page, which HAS no title band (Ryan, 2026-08-26), and written anyway
   * so the rule stays correct on a page that grows one. It is flush rather than
   * the desktop rail's +20: a band hanging 20px below the nav is the gap the
   * flow lane measured and killed.
   *
   * STICKY AND A SIDEWAYS SCROLLER AT ONCE, which is the trap this had to clear:
   * `position: sticky` resolves against the nearest SCROLLPORT ancestor, so a
   * sticky element INSIDE a scroller pins to that scroller and not to the page.
   * The two are on different elements here — `rail-inner` is the sticky band and
   * `rail-links` is the scroller inside it — which is the same split the
   * comparison table's sideways pin uses, and the reason it works there.
   *
   * IT REMOVES NO CAPABILITY: the strip still scrolls with a wheel, a drag and
   * the arrow keys, the spy still moves the ink fill along it, and it is not a
   * tab stop under Chrome's rule (it holds seven focusable links —
   * `tools/focusgate.mjs` enumerates it by that rule and agrees).
   */
  /*
   * THE GRID BECOMES A BLOCK, AND THAT IS WHAT MAKES THE PIN TRAVEL.
   *
   * MEASURED, NOT ASSUMED. Stacking the grid to one column and marking the band
   * sticky released it after 44px — it read `top: -2091` at chapter three. A
   * sticky box travels only within its CONTAINING BLOCK, and a grid item's is
   * its own grid area: the rail's row is exactly as tall as the chips, so the
   * band was pinned inside a 44px box and left it immediately. Same rule the
   * order flow's one-screen band hit, and the same class of surprise.
   *
   * As a BLOCK the two children are ordinary blocks of `.sf-pdp-eng`, whose box
   * runs the whole seven chapters — so the band pins from the first chapter to
   * the last, which is the length the ruling means. The stacking order and the
   * 24px between them are the same as the grid's; nothing else moves.
   */
  .sf-pdp-below__grid {
    display: block;
  }

  .sf-pdp-below__flow {
    margin-top: 24px;
  }

  /*
   * THE PIN IS ON THE `<nav>`, not on the band inside it: the nav is the block
   * whose containing block is the whole below-the-fold, and the band is what it
   * carries.
   *
   * OVER THE SECTIONS, UNDER THE CHROME — and, since Ryan's DD-E (2026-08-28),
   * OVER THE POPUPS TOO. It read 3, which was above the sections it covers and
   * below the chrome above it, and that was the whole of the reasoning until
   * Ryan opened the reviews sort-by with this band on screen and watched the
   * panel paint straight through it. His ruling: "popups sit above their local
   * content but BELOW pinned/sticky chrome — a page-flow popup never covers
   * navigation", fixed at the policy level rather than by bumping one surface.
   *
   * 10 IS THE IN-PAGE PINNED RUNG on chrome.css's ladder (the block at the top
   * of its quiet-dropdown section carries the whole thing), and it is the rung
   * the category page's AI-embed stack already takes — which is what "the rung
   * the other in-page pinned bands take" was reaching for when it said 3. The
   * popups are at 5, the title band at 20, the buy bar at 30, the header at 40:
   * this band is above what it covers and under everything that covers it.
   *
   * (The z-index 4 that used to sit above it belonged to the Description head
   * band, which retires with ruling PB1 — there is nothing left for this band
   * to slide under.)
   */
  .sf-pdp-nav {
    position: sticky;
    /* THE OFFSET CASCADE: the chrome, the title band (0 on a product page and
     * written anyway), and the BUY BAR — which pins at the top on a phone too
     * (ruling 8) and would otherwise be drawn straight over this band.
     * `pdp-buy-bar.tsx` publishes the last term. The head band's
     * `--sf-pdp-enghead-h` rung is GONE with ruling PB1. Flush, with no gap:
     * the chrome and this band are one stack. */
    top: calc(
      var(--sf-pinned-offset, 54px) + var(--sf-band-stuck-h, 0px) +
        var(--sf-pdp-buybar-h, 0px)
    );
    z-index: 10;
  }

  .sf-pdp-nav__inner {
    /* THE BAND'S OWN GROUND. Full-bleed, and the gutter comes back as padding
     * on the scroller inside it — so the paper reaches both screen edges while
     * the first chip still starts on the text's left edge. The negative margin
     * is the OUTER container's gutter now (ruling PB1 moved the container one
     * level up), which is the same custom property at the same value. */
    background: var(--brand-color-paper, #fbfbfa);
    border-bottom: var(--border-hairline);
    gap: 0;
    margin-inline: calc(-1 * var(--sf-gutter));
    padding: 7px 0;
    position: static;
  }

  .sf-pdp-nav__links {
    display: flex;
    gap: 2px;
    overflow-x: auto;
    /* The gutter, given back — see the band's full bleed above. */
    padding-inline: var(--sf-gutter);
    /* A scroller with no positioned ancestor lets an abs-positioned descendant
     * escape into the DOCUMENT's overflow (playbook §5). Standing precaution. */
    position: relative;
    scrollbar-width: none;
    /*
     * SMOOTH BY DECLARATION, NOT BY SCRIPT. The spy asks for a smooth scroll
     * explicitly (`scrollTo({behavior})`), so this is belt and braces for the
     * one path that does not go through it — a chip pressed by the reader,
     * which the browser brings into view itself.
     */
    scroll-behavior: smooth;
  }

  .sf-pdp-nav__links::-webkit-scrollbar {
    display: none;
  }

  .sf-pdp-nav__item {
    flex: none;
    white-space: nowrap;
  }

  /*
   * A READER WHO ASKED FOR LESS MOTION GETS NONE. The spy's own scroll checks
   * the same query in script; this covers the declarative half.
   */
  @media (prefers-reduced-motion: reduce) {
    .sf-pdp-nav__links {
      scroll-behavior: auto;
    }
  }

  /* `.sf-pdp-ans__results` LEFT THIS LIST (ruling A3): it is one column at every
   * width now that the AI panel it sat beside is gone. */
  .sf-pdp-rev__results,
  .sf-pdp-rev__summary {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
   * THE PANEL IS NOT DRAWN AT THIS RUNG AT ALL (ruling R4). It used to stack
   * under the whole list — un-pinned, because a sticky box in a single-column
   * flow follows the reader down past content it has nothing to do with — and
   * that is the placement Ryan reported as lost. Its photographs are at the top
   * of the section instead, as `.sf-pdp-rev__mphotos`.
   *
   * `display: none` RATHER THAN A SECOND COPY OF THE MARKUP: the strip renders
   * the SAME `media` list and opens the SAME viewer, so nothing is duplicated —
   * what differs is the shape, and the shape is what a stylesheet is for.
   */
  .sf-pdp-rev__photos {
    display: none;
  }

  .sf-pdp-rev__mphotos {
    display: block;
  }

  /*
   * ─── THE HAIRLINE UNDER THE PHOTOS IS RETIRED (Ryan's MP4, 2026-08-29) ──
   *
   * "A hairline across the bottom of the photos = the scrubber — REMOVE. No
   * scrubber necessary."
   *
   * IT WAS NEVER A SCRUBBER, and finding that out is what made the fix safe.
   * MEASURED at 390: the strip's own box ends at its last tile (the block has
   * `padding-top` and no bottom padding), and the very next row of pixels is
   * `rgb(232, 231, 227)` across the full 360px measure — which is
   * `--border-hairline`, drawn by `.sf-pdp-rev__item`'s `border-top`. The
   * FIRST REVIEW CARD's separator, sitting flush on the bottom edge of the
   * photographs with nothing between them, reads exactly like a scroll track
   * under the row.
   *
   * SO WHAT RETIRES IS THAT ONE SEPARATOR, and only where it is wrong: a card
   * separator separates a review from a REVIEW, and what is above this one is
   * a strip of photographs. Every other card keeps its rule, and the rule is
   * scoped inside this rung — the adjacent-sibling selector would match at
   * every width otherwise (the strip is `display: none` above the cut but
   * still in the DOM), which would take the desktop list's first separator
   * away too.
   */
  .sf-pdp-rev__mphotos + .sf-pdp-rev__item {
    border-top: 0;
  }
}

@media (max-width: 767.98px) {
  /* The platform's mobile section rhythm (tokens §3: 48–56px), at the value the
   * other mobile boards draw. */
  .sf-pdp-sec {
    margin-top: var(--space-section-mobile);
  }

  /* The module is mid-page now (ruling O1), so it takes the mobile section
   * rhythm its neighbours take rather than the 32px that belonged to the top of
   * the below-fold run — and the 32 moves to whatever is first, below. */
  .sf-pdp-below .sf-ph {
    padding-top: var(--space-section-mobile);
  }

  /* The phone's own distance under the buy grid, on the wrapper that now owns
   * it — see the desktop rule for why the first-child pair retired. */
  .sf-pdp-below {
    margin-top: 32px;
  }

  /*
   * THE PHOTO SECTION KEEPS ITS FULL-BLEED GROUND (ruling PB1's one cost).
   *
   * At this rung the shared module paints its own surface band with a hairline
   * over it — the mobile boards' drawing — and that band has to reach both
   * screen edges. It used to: the section was a full-bleed child of the page
   * and its own `.sf-container` held the gutter. Under the new composition the
   * section sits INSIDE the wrapper's container, so its ground would stop at
   * the text edge and leave two gutters of page paper either side of it.
   *
   * THE STANDARD RECIPE, unchanged from every other full-bleed on this site:
   * break the gutter with a negative margin and give it straight back as
   * padding. One declaration, at the one rung where the module paints a ground.
   */
  .sf-pdp-below__flow .sf-ph {
    margin-inline: calc(-1 * var(--sf-gutter));
    padding-inline: var(--sf-gutter);
  }

  /*
   * THE ACTION STAYS ON THE HEADLINE'S ROW AT THIS WIDTH TOO (rulings 11 + A1).
   * It used to drop under the title — and then under the SUBHEAD, because the
   * subhead was in the same column — which put "Write a review" three lines
   * below the word "Reviews". Measured at 390: the 34px circle, its 11px gap and
   * a 21px "Reviews" come to 135px, and the ghost button is 150 — so both fit on
   * one 360px line with room, and the same holds for "Answers"/"Ask a question".
   *
   * THE BUTTON GIVES UP ITS SIDE PADDING to make that true at 320 as well, where
   * the two are 8px over. Nothing else about it changes.
   */
  .sf-pdp-sec__head-row {
    gap: 12px;
  }

  .sf-pdp-sec__ghost {
    padding: 10px 14px;
  }

  /* The circle is 34px here (sections.css's mobile step), so the indent that
   * clears it is 34 + the head's 11px gap. */
  .sf-pdp-sec__sub {
    margin-left: 45px;
  }

  /* THE CHAPTER EYEBROW TAKES THE SAME STEP (Ryan's PB-polish), and for the
   * same reason it is written beside that rule: it hangs off the same
   * `.sf-sec-head`, whose circle and gap are both smaller here, so the
   * desktop's 54 would over-indent it past the title it is supposed to sit
   * over. 34 + 11 = 45, derived from the head rather than invented. */
  .sf-pdp-eng__eyebrow {
    margin-left: 45px;
  }

  .sf-pdp-eng__modes,
  .sf-pdp-eng__pair {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
   * PROS / CONS / BEST-FOR SCROLL SIDEWAYS (Ryan's ruling 13, 2026-08-27):
   * "eleven vertical lines today = too much". Wrapped, the three groups fill
   * most of a phone screen with chips before the reader reaches the histogram.
   *
   * THE ESTABLISHED PILL SCROLLER, and it is the same rule `.sf-ph__filters`
   * draws for the photo module's tags: one line per group, hidden scrollbar,
   * full bleed with the gutter taken back as padding so the first chip starts on
   * the card's text edge and the last one can reach the screen edge.
   *
   * THE CARD'S PADDING IS WHAT IS BROKEN HERE, not the page gutter: these chips
   * live inside `.sf-pdp-rev__ai`, whose mobile padding is 18px. So the negative
   * margin is that, not `--sf-gutter` — and it is written as the same value the
   * card declares rather than as a variable, because the card's padding is a
   * drawn number and inventing a token for one use is a token nobody maintains.
   *
   * NOT A TAB STOP under Chrome's rule: each row holds focusable chips.
   */
  .sf-pdp-rev__chips {
    flex-wrap: nowrap;
    /* The scroller's own floor. A flex/grid item's automatic minimum size is its
     * content, so a nowrap row inside one refuses to be narrower than its chips
     * and the CLIP never happens — the second half of the same trap the grid
     * above carries. */
    min-width: 0;
    margin-inline: -18px;
    overflow-x: auto;
    padding-inline: 18px;
    /* Standing precaution: a scroller with no positioned ancestor lets an
     * abs-positioned descendant escape into the document's overflow (§5). */
    position: relative;
    scrollbar-width: none;
  }

  .sf-pdp-rev__chips::-webkit-scrollbar {
    display: none;
  }

  .sf-pdp-rev__chip {
    flex: none;
    white-space: nowrap;
  }

  /*
   * THE STARS GO ABOVE THE TITLE (Ryan's ruling R3, 2026-08-27), ALWAYS. He
   * considered keeping them inline for short titles and reversed it himself:
   * short titles are rare, and one pattern a reader can rely on beats two that
   * depend on a word count.
   */
  .sf-pdp-rev__item-head {
    align-items: flex-start;
    flex-direction: column;
    gap: 6px;
  }

  .sf-pdp-rev__ai,
  .sf-pdp-rev__aggregate,
  .sf-pdp-rev__search,
  .sf-pdp-rev__theme-answer {
    padding: 16px 18px;
  }

  /* THE BAND FOLLOWS THE CARD'S PADDING (ruling R2). Same rule as above at the
   * phone's 16/18 — written as the card's own numbers rather than as a token,
   * because the card's padding is a drawn value and a token for one use is a
   * token nobody maintains. */
  .sf-pdp-rev__groups {
    margin: 4px -18px -16px;
    padding: 16px 18px;
  }

  /* The photo strip beside a review is a 120px column on a 390px screen: it goes
   * under the review rather than taking a third of its measure. */
  .sf-pdp-rev__item {
    flex-direction: column;
    gap: 12px;
  }

  /*
   * FULL MEASURE, AND STILL A STRIP (ruling R4). The strip takes the review's
   * whole width here and the TILES keep their 120px square, so three of them
   * fill a 390px screen and the fourth is the thing the reader swipes to — which
   * is what makes it legible as a strip rather than as a row that happens to fit.
   */
  .sf-pdp-rev__item-photos {
    width: 100%;
  }

  /* THE "WITH PHOTOS" PHONE RULE IS RETIRED BY NAME (Ryan's FX4, 2026-08-28).
   * It undid a `margin-left: auto` that a wrapped row had put in the wrong
   * place; the control is in the configuration grid now and the row it wrapped
   * in is gone. */

  .sf-pdp-rev__showing {
    gap: 12px;
  }

  /* THE PAGER'S PHONE TREATMENT MOVED (ruling PP5, 2026-08-28). Ruling A2's
   * one-line pager — the trimmed label, the tighter buttons, the scroller at
   * 320 — is declared once in `sections.css` beside the pattern, and the photo
   * module's pager takes it too now. Every selector that stood here retires with
   * the classes it named. */
}
