/*
 * Storefront chrome stylesheet — header/nav, title band, footer.
 *
 * Served as a static asset and pulled in by `<ChromeStyles>` rather than
 * `import`ed, and that is deliberate. Both alternatives put this stylesheet
 * into EVERY brand's HTML: appending to `globals.css` re-hashes the URL every
 * brand already links, and importing a .css from a component attaches the chunk
 * to the shared `[brand]` layout segment, so Next emits a <link> to it on pages
 * whose markup it cannot match (a dynamic import does not avoid this — measured
 * 2026-08-24). A hoisted <link> to a public asset is rendered only by the
 * components that need it, which is what keeps a brand that wears different
 * chrome byte-identical.
 *
 * Every colour, size, radius and border reads a token: `--brand-color-*` and
 * `--brand-font-*` come from `brands/<brand>/brand.json` via `buildThemeCss()`;
 * `--text-*`, `--radius-*`, `--control-*`, `--border-*` and `--space-*` are the
 * platform constants declared in globals.css (docs/storefront-tokens-and-patterns.md
 * §2–§3). Nothing here hard-codes a value that has a token.
 *
 * Class prefix `sf-` ("storefront") keeps these rules from ever matching markup
 * rendered by another chrome.
 */

/* ----------------------------------------------------- the pinned offset -- */

/*
 * How much of the viewport's top edge the pinned nav row owns.
 *
 * The header's scrolled state parks a solid bar over the page, and anything the
 * PAGE sticks to the top of the viewport — a product render frame, a checkout
 * summary card, the target of a `#fragment` jump — lands underneath it unless it
 * is told the bar is there. So the row's own height is published here, once, and
 * read by `scroll-padding-top` below and by the page-level sticky rules in
 * globals.css (`top: calc(32px + var(--sf-pinned-offset, 0px))`).
 *
 * It is declared on `:root` rather than on `.sf-header` because its readers are
 * outside the header's subtree — and, in globals.css's case, outside this
 * stylesheet entirely. A brand that does not wear this chrome never loads this
 * file, so the `var()` fallback of `0px` leaves its pages at exactly the offset
 * they had before this variable existed.
 *
 * The value tracks `.sf-header__bar`'s height at every cut: 54px here, 50px in
 * the 919.98 recipe, and 54px again at the mobile cut, where the bar IS the
 * header.
 */
:root {
  --sf-pinned-offset: 54px;
}

/* Fragment jumps and `scrollIntoView()` land below the pinned row rather than
 * under it. Declared on `html` because that is the scrolling element. */
html {
  scroll-padding-top: calc(var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px));
}

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

/*
 * THE PLATFORM FOCUS INDICATOR (Ryan's ruling, 2026-08-26). One treatment, on
 * every interactive control on the storefront, at every surface.
 *
 * The bar: a perimeter indicator AT LEAST 2px THICK with AT LEAST 3:1 contrast
 * against the colours adjacent to it — WCAG 2.2 SC 1.4.11 Non-text Contrast (AA)
 * for the indicator itself, and the SC 2.4.13 Focus Appearance metric (AAA) for
 * the 2px perimeter. (SC 2.4.11 Focus Not Obscured is a separate, AA obligation
 * about the indicator being SCROLLED OUT OF SIGHT — the sticky chrome's
 * `scroll-padding-top` above is what holds it.) What this replaces: a 1px border
 * colour-swap with a 0.5px spread shadow, which Ryan read as too thin.
 *
 * A control may draw whatever else it likes on focus — the two AI fields keep
 * their board-drawn accent border and half-pixel spread, which IS the site's
 * selection language — but that styling ADDS to the ring, it never replaces it.
 * The UA's own `:focus-visible` outline may be suppressed only where the ring is
 * drawn on the box the reader is actually looking at instead (a bare input
 * inside a styled field).
 *
 * THREE CONTEXTS, because "3:1 against adjacent colours" is a claim about the
 * ground, and this site has three grounds. Measured against the palette:
 *
 *   LIGHT   `--sf-focus-ring` — accent #C4530A on the light grounds it lands on:
 *           paper #FBFBFA 4.42:1 · surface #FFFFFF 4.58:1 · well #F1F0EC 4.01:1 ·
 *           accent-soft #FBEFE5 4.05:1 · line-mid #D8D6D0 3.15:1. All clear 3:1.
 *
 *   DARK    `--sf-focus-ring-on-dark` — accent-on-dark #FFB27A on nav blue
 *           #192950 is 8.07:1. The plain accent on that blue is 3.11:1: it
 *           passes on paper, but with 0.11 of headroom, and #FFB27A is the token
 *           whose documented job (§1) is exactly this. The header and footer
 *           wore logo-orange-bright #FF7529 here from the fidelity pass —
 *           5.31:1, compliant, but that token's documented duty is the logo dot
 *           and the active-nav underline ONLY, so the one-orange rule and this
 *           ruling's consistency both point at the same swap.
 *
 *   MEDIA   `--sf-focus-ring-on-media` + `--sf-focus-halo` — a control sitting on
 *           a PHOTOGRAPH has no known adjacent colour, so no single ink can be
 *           promised 3:1 against it. Accent alone covers only grounds lighter
 *           than L=0.638 or darker than L=0.027 — a hole across the whole middle
 *           of the range. The pair does not have that hole: a white halo clears
 *           3:1 against every ground up to L=0.300 and an ink ring clears it
 *           against every ground from L=0.127, and those two ranges OVERLAP, so
 *           whatever the photograph is doing under the control, one of the two
 *           bands contrasts. The halo is a 4px spread shadow and the 2px ink
 *           outline is painted over its outer half, which is what makes the pair
 *           read as one ring rather than two.
 *
 * Declared on `:root` and read by the sibling sheets (category/home/delivery),
 * each of which repeats the value as a `var()` fallback exactly once, in its own
 * focus-ring block — same discipline as `--sf-pinned-offset` above, so a sheet
 * loaded without this one still draws a compliant ring.
 *
 * NOT extended to globals.css or to anything prodink renders: that is a separate
 * ruling, flagged rather than taken.
 */
:root {
  --sf-focus-ring: 2px solid var(--brand-color-accent, #c4530a);
  --sf-focus-ring-on-dark: 2px solid var(--brand-color-accent-on-dark, #ffb27a);
  --sf-focus-ring-on-media: 2px solid var(--brand-color-ink, #17181a);
  --sf-focus-halo: 0 0 0 4px var(--brand-color-surface, #ffffff);
  --sf-focus-ring-offset: 2px;
  /* Drawn INSIDE the box, for a control whose ring would otherwise land off the
   * viewport or under a neighbour: a full-bleed scroller, a menu item flush
   * inside its panel. */
  --sf-focus-ring-inset: -2px;
}

/*
 * THIS SHEET'S CONTROLS, in one place rather than four rules beside four
 * components. The chrome has exactly two grounds — the blue header/footer and
 * the paper title band — so it has exactly two groups.
 *
 * The element lists are written out rather than left at `a, button`: the search
 * control is a `<button>` today and the cart is an `<a>`, but the day either
 * becomes an `<input>` or a `<select>` is not the day to rediscover this rule.
 */
.sf-header a:focus-visible,
.sf-header button:focus-visible,
.sf-header input:focus-visible,
.sf-header select:focus-visible,
.sf-header textarea:focus-visible,
.sf-header [tabindex]:focus-visible,
.sf-footer a:focus-visible,
.sf-footer button:focus-visible,
.sf-footer input:focus-visible,
.sf-footer select:focus-visible,
.sf-footer textarea:focus-visible,
.sf-footer [tabindex]:focus-visible {
  outline: var(--sf-focus-ring-on-dark);
  outline-offset: var(--sf-focus-ring-offset);
}

/*
 * THE LIGHT RING, and the two surfaces that need it INSIDE a dark one.
 *
 * The browse dropdown hangs off the blue header and is therefore matched by the
 * `.sf-header` block above — but it is a WHITE panel, and #FFB27A on white is
 * 1.85:1. It is listed here, after that block and at the same specificity, so
 * source order hands its controls the light ring the panel's own ground needs.
 * The mobile takeover is genuinely on the nav blue and keeps the dark one.
 *
 * `.sf-crumbs button` is the parentless crumb that pops that dropdown: a crumb
 * that is a control rather than a link, on the same paper ground as its
 * siblings.
 */
.sf-crumbs a:focus-visible,
.sf-crumbs button:focus-visible,
.sf-nav-menu a:focus-visible,
.sf-nav-menu button:focus-visible,
.sf-quiet-select__control:focus-visible {
  outline: var(--sf-focus-ring);
  outline-offset: var(--sf-focus-ring-offset);
}

/* Inset: a menu item is flush against its panel's border on three sides, and an
 * outward ring would be drawn half underneath it. */
.sf-quiet-select__item:focus-visible {
  outline: var(--sf-focus-ring);
  outline-offset: var(--sf-focus-ring-inset);
}

/*
 * THE FIELD'S CLEAR TAKES AN INSET RING, for the same reason: it sits INSIDE a
 * drawn field, hard against its right padding, so an outward ring would be
 * drawn half on the field's border and half on the ground outside it. Inset, it
 * is wholly on the field's own fill — `surface` or `paper`, where accent
 * measures 4.01:1 (tokens §3).
 */
.sf-field-clear:focus-visible {
  outline: var(--sf-focus-ring);
  outline-offset: var(--sf-focus-ring-inset);
}

/* ------------------------------------------------------------- container -- */

/*
 * BREAKPOINT CONVENTION: a named cut belongs to the WIDER side of itself.
 *
 * So a viewport of exactly 768 gets the tablet layout, not the mobile one —
 * iPad Mini portrait is a tablet — and the queries that switch to the narrower
 * treatment are written `max-width: 767.98px`, the largest value below it that
 * a fractional device-pixel ratio can still land on. Same for the header's own
 * cuts: 919.98, 1023.98, 1039.98, 1239.98.
 *
 * The container's 1024 rung moves with the header's, or a viewport of exactly
 * 1024 would be a hybrid — desktop masthead against a tablet gutter.
 *
 * 834 was the last cut written the old way. It now reads `max-width: 833.98px`
 * in the container rung below and in the footer's own query, so no exception
 * remains: 834 is iPad Pro 11in portrait, and that device takes the wider
 * treatment of itself — the 40px gutter and the five-up footer.
 */

/*
 * The storefront's own measure. Wider than globals.css `.container` (1120px)
 * because the design boards are drawn at 1440 with 64px gutters.
 *
 * The gutter is a variable because the header's experts strip and its sticky
 * badge are positioned OUT of flow and still have to land on the same content
 * edge the rows do.
 */
.sf-container {
  --sf-gutter: 64px;
  margin: 0 auto;
  max-width: 1440px;
  padding: 0 var(--sf-gutter);
  width: 100%;
}

@media (max-width: 1023.98px) {
  .sf-container {
    --sf-gutter: 40px;
  }
}

@media (max-width: 833.98px) {
  .sf-container {
    --sf-gutter: 32px;
  }
}

/*
 * THE PHONE RUNG IS 15, AND IT IS RULED (Ryan, 2026-08-26: "15px everywhere.
 * Settled.").
 *
 * This declaration is the source of the number. globals.css `.container`
 * mirrors it for the pages that wear this chrome without using `.sf-container`,
 * and every sheet that takes the gutter back out for a full-bleed element reads
 * it through `var(--sf-gutter)` — so a page's content edge, the header's burger
 * and the footer's columns are the same edge by construction rather than by
 * three matching literals.
 *
 * It was 20 until 2026-08-25 and 14 until today. Both moves were made here and
 * mirrored in globals.css in the same stroke, deliberately: the mirror reaches
 * prodink's mobile pages, which is within the ruling both times — the whole
 * point of the number is that every brand's phone gutter is the same one.
 */
@media (max-width: 767.98px) {
  .sf-container {
    --sf-gutter: 15px;
  }
}

/* ---------------------------------------------------------------- header -- */

/*
 * The FULL header is the default state on every page: a two-level bar on brand
 * blue — masthead over nav row — with the experts strip anchored to its bottom
 * right, and the mobile bar below them as the small-screen swap.
 *
 * The SCROLLED state is the same header, stuck. `position: sticky` at a
 * negative offset of exactly the masthead's own height lets the masthead scroll
 * away as it always did and brings the header to rest with its nav row — and
 * nothing else — pinned to the top of the viewport. That is the entire scrolled
 * state: no second bar, no handoff, nothing animated into place. The row a
 * reader is looking at before the scroll is the same element, carrying the same
 * links, after it.
 *
 * `top` is negative rather than 0 because THE STICKY ELEMENT IS THE WHOLE
 * HEADER, which has to be free to travel up by the masthead before it pins.
 * Sticking the nav row on its own would mean hoisting it out of
 * `.sf-header__full`, away from the scrim and the experts strip it shares a box
 * with — and a sticky element's range ends with its containing block, so a nav
 * row left inside that 138px box would unpin again a few dozen px down the
 * page. The header's containing block is the page-tall `.shell`, so pinning it
 * lasts the whole document.
 *
 * The offset reads the masthead's height from the same variable the masthead is
 * drawn with, so the two cannot drift apart.
 *
 * BUT THE PIN IS GATED ON `data-driven`, and that is the whole point of the
 * attribute. Sticking is only half of the scrolled state: the other half — the
 * experts layer fading away, the badge travelling down onto the row, the row's
 * own contact pair arriving — is the `--sf-morph` scrub, which only exists while
 * `<HeaderSticky>` is publishing it. Pinned with the morph stuck at 0 is not a
 * plainer header, it is a broken one: the experts photo and pitch paint at full
 * opacity OVER a nav row that has been pulled up to the top of the viewport,
 * the badge sits where the masthead used to be (off screen), and the row's own
 * pair is still `visibility: hidden`. That frame is what a reader gets with
 * scripting off, and what Chrome paints for anyone whose scroll position is
 * restored before hydration.
 *
 * So the header is STATIC until the driver mounts and hands it `data-driven`,
 * and static means the full two-level header renders correctly at any scroll
 * depth — it simply scrolls away like any other block. The driver removes the
 * attribute again when it unmounts, so the two facts cannot get out of step.
 */

.sf-header {
  /*
   * THE PROGRESS VARIABLE. `<HeaderSticky>` writes it, once per animation
   * frame, as `clamp(scrollY / masthead-height, 0, 1)`: 0 with the page at the
   * top, 1 the instant the header finishes travelling and pins. Every morph
   * below is a `calc()` off it, so the whole choreography is scrubbed by the
   * scroll wheel and is reversible frame by frame — there is no timed
   * animation to get out of step with the scroll, and no state to unwind.
   *
   * It is declared here at 0 so the header renders as the full header before
   * the script runs, or if it never does.
   *
   * (CSS `animation-timeline: scroll()` would express this natively, but it is
   * not yet across the browsers this storefront serves. The variable is the
   * portable scrub.)
   */
  --sf-morph: 0;

  /*
   * The row runs on its OWN clock, finishing at 36% of the travel.
   *
   * Two reasons, and the first is a bug the linear version had: the badge comes
   * to rest at a fixed 64..136, and the nav row rises underneath it, so until
   * the nav has slid past 136 the badge is sitting on top of the first nav
   * item. On one clock the nav only clears at the very end and "Browse Hoops"
   * spends the middle of the scroll half-hidden. Front-loading the row gets it
   * out from under the badge before the row has risen far enough for the two to
   * meet at all.
   *
   * The nav's cap height rises past the badge's lower edge at about p=0.26, and
   * whatever the nav has not travelled by then is its clearance. There is no
   * collision there at any setting — sampled from the rendered pixels at that
   * exact position, at both 1440 and 1000, the badge and the first nav word do
   * not share a single painted row: the circle's last ink is a row or two above
   * the nav's cap, and the leftmost pixel in the nav's band is the nav's own B.
   * But the badge is a circle in a SQUARE box, and any box-based check reads that
   * near-miss as 11.8px at 0.4. 36% keeps the conservative measure over the 24px
   * margin too, so the automated sweep stays a real gate instead of a caveat, and
   * the difference in feel between a third and four tenths is not perceptible.
   *
   * The second reason is what it does to the end of the scroll: with the row
   * settled early, the rest is nothing but the header travelling the last of the
   * way up and the experts pair handing over. The arrival is calm because there
   * is nothing left to arrive.
   *
   * Everything that has to stay in lockstep with the HEADER'S OWN travel — the
   * badge's translate, which cancels it exactly — stays on the linear clock.
   */
  --sf-morph-row: clamp(0, calc(var(--sf-morph) / 0.36), 1);

  /*
   * And a third, for the experts pair, finishing at 60%.
   *
   * It deliberately does NOT wait for the photo to finish fading first. Delaying
   * it was tried and measured: holding the pair still until the photo has gone
   * leaves it in its un-slid position while the row rises to meet it, and
   * between 1121 and 1180 the pair then lands ON the cart (-24.6px at 1140).
   * Sliding from the start crosses the photo instead — and that crossing is a
   * dissolve between two things that are both already fading, not a collision
   * between two solid ones.
   *
   * Landing by 60% leaves the pair legible, collapsed and right-aligned for the
   * rest of the travel, sitting exactly where the slim bar's own pair will
   * appear — which is what makes a handoff between two elements read as one
   * thing moving.
   */
  --sf-morph-pitch: clamp(0, calc(var(--sf-morph) / 0.6), 1);

  /*
   * Where the contact pair changes hands. The pitch's copy finishes its slide at
   * 0.6 — see above — so from 0.6 to 0.85 the two copies are superimposed, and
   * that is the ONLY window in which either one may be part-transparent. Before
   * it the outgoing copy is at full opacity (it is the thing the eye is
   * following); after it the incoming copy is. Both are driven by `--sf-morph`
   * and nothing else, so there is no discontinuity at the pin and the one-pixel
   * gap between `--sf-morph` reaching 1 and `data-stuck` flipping cannot flash
   * anything.
   *
   * `<HeaderSticky>` reads the start value off this element on mount so the
   * number lives in one place: it is the point at which the incoming copy has
   * to become `visible` in order to paint at all.
   */
  --sf-crossfade-start: 0.6;
  --sf-crossfade-end: 0.85;

  /* --- the morph's measured endpoints, per breakpoint --- */

  /* How far the badge travels DOWN the header while the header travels up, so
   * that it comes to rest on the slim bar. Measured: the badge sits 9px into an
   * 82px masthead and has to end 9px below the pinned row's top edge, i.e. 91px
   * into the header — a shift of 82px. Because that equals the masthead height
   * the badge does not move on screen at all: it holds ~9px from the top of the
   * viewport while everything above and around it scrolls away. */
  --sf-logo-shift: 82px;
  /* 68 / 72 — the board's slim-bar badge over the masthead badge. */
  --sf-logo-scale: 0.9444;
  /* Badge (68) + the 8px the board leaves beside it + the pinned row gap (28). */
  --sf-nav-clearance: 104px;
  /* The experts photo (232) and the gap it leaves behind (20): what the pitch
   * slides across once the photo has gone. */
  --sf-pitch-shift: 252px;
  /* The slim bar's Chat link (69.31) plus the pair's 16px gap — how far left the
   * phone number moves so Chat can land beside it instead of under it. */
  --sf-contact-collapse: -85.31px;
  /* The pitch's phone line (20) plus its 5px stack gap: one line up. */
  --sf-chat-lift: -25px;
  /* The last of the three: the pitch is bottom-anchored to the header and the
   * slim bar's pair is centred in the row, which leaves them 16.5px apart. */
  --sf-pitch-drop: 16.5px;

  --sf-masthead-height: 82px;
  background: var(--brand-color-nav, #192950);
  /* The boards declare no line-height anywhere in the chrome, so every line box
   * is `normal`. Without this the 1.65 body leading in globals.css cascades in
   * and grows every stacked pair — the SINCE 1984 / Our Story block and the
   * experts pitch both drift several px per line. */
  line-height: normal;
  /* The badge revealed on the pinned row hangs below it into the page, so the
   * chrome paints above whatever follows it and must never clip its own
   * overflow. `z-index` is inert while the header is static and takes effect
   * with the pin below, which is the only state that needs it. */
  position: static;
  z-index: 40;
}

/* The pin itself, gated on the driver — see the block above. */
.sf-header[data-driven="on"] {
  position: sticky;
  top: calc(-1 * var(--sf-masthead-height));
}

/* Focus ring: see "the focus ring" at the top of this sheet, which carries
 * every control in the chrome. */

/*
 * The board's scrim: a bottom-anchored wash that settles the nav row, masked
 * away to the right so it never dirties the experts photo.
 */
.sf-header__scrim {
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.32) 0%,
    rgba(0, 0, 0, 0.32) 46%,
    rgba(0, 0, 0, 0) 100%
  );
  bottom: 0;
  height: 106px;
  left: 0;
  -webkit-mask-image: linear-gradient(to right, #000000 50%, transparent 72%);
  mask-image: linear-gradient(to right, #000000 50%, transparent 72%);
  opacity: calc((0.6 - var(--sf-morph)) / 0.6);
  pointer-events: none;
  position: absolute;
  right: 0;
  will-change: opacity;
}

/* Anchors the experts strip, which is out of flow and spans both rows. */
.sf-header__full {
  position: relative;
}

/* --- masthead row --- */

/* Above the nav row (1) and the experts layer (2), because the badge leaves
 * this row and comes to rest ON the nav row. Nothing else in the masthead ever
 * reaches down there — it has faded out by then. */
.sf-header__masthead {
  align-items: center;
  display: flex;
  gap: 12px;
  height: var(--sf-masthead-height);
  position: relative;
  z-index: 3;
}

/* The board nudges the whole badge cluster down off the row's centre line. */
.sf-header__masthead > * {
  margin-top: 8px;
}

/* Fades out over the first half of the scrub, and is gone well before the row
 * pins so the end of the travel is calm. */
.sf-header__since {
  display: flex;
  flex-direction: column;
  gap: 3px;
  justify-content: center;
  opacity: calc((0.55 - var(--sf-morph)) / 0.55);
  will-change: opacity;
}

.sf-header[data-stuck="true"] .sf-header__since {
  visibility: hidden;
}

.sf-header__masthead .sf-header__eyebrow {
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-small);
  font-weight: 700;
  letter-spacing: 0.08em;
}

.sf-header__story {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-size: var(--text-caption);
  font-weight: 400;
  text-decoration: underline;
}

.sf-header__story:hover {
  color: var(--brand-color-surface, #ffffff);
}

/* --- the badge, in all three bars --- */

.sf-header__logomark {
  display: block;
  filter: drop-shadow(0 4px 10px color-mix(in srgb, var(--brand-color-ink, #17181a) 30%, transparent));
  flex: none;
}

.sf-header__logomark img {
  display: block;
  height: auto;
}

/* The masthead badge sits on the flat bar, so the board leans on its shadow
 * a little harder than the compact one's. */
/*
 * THE ONE BADGE. There is no second copy revealed on the pinned row any more:
 * this element escapes the masthead and lands on the slim bar itself.
 *
 * `transform-origin: left top` is what makes the two channels independent — the
 * scale leaves the badge's left and top edges where the translate put them, so
 * `translateY` alone decides the resting height and `scale` alone decides the
 * size. At `--sf-morph: 1` the box is left 64 / top 91 / 68px square, which is
 * exactly where the old revealed badge sat, so the slim bar is unchanged.
 *
 * Transform only — the badge never lays out — and it is a flex item, which is
 * what lets `z-index` lift it over the row it comes to rest on.
 */
.sf-header__logomark--full {
  filter: drop-shadow(0 4px 10px color-mix(in srgb, var(--brand-color-ink, #17181a) 35%, transparent));
  position: relative;
  transform: translateY(calc(var(--sf-morph) * var(--sf-logo-shift)))
    scale(calc(1 + (var(--sf-logo-scale) - 1) * var(--sf-morph-row)));
  transform-origin: left top;
  z-index: 2;
}

.sf-header__logomark--full img {
  width: 72px;
}

.sf-header__wordmark {
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-emphasis);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* --- nav row --- */

/*
 * The one element that is the nav bar in BOTH states — 54px tall in both, which
 * is the height the board draws the slim pinned bar at. The board draws the
 * unpinned row 52px, but a row that changed height on pinning would push every
 * page down 2px as it stuck; the board's own slim bar is the state with the
 * tighter tolerances, so it is the one that gets the exact number.
 */
.sf-header__bar {
  /*
   * The row's own measurements, interpolated by the scrub. These are the one
   * part of the choreography that is NOT transform-only, and deliberately so:
   * the board draws the slim bar's type half a px smaller and its gaps 4px
   * tighter, which moves every item in the row. Faking that with a scale would
   * render the pinned type off-pixel and lose the board; leaving it to a
   * transition at the pin makes the cart jump ~36px the moment the row sticks,
   * because the translate below is calibrated against the tightened metrics.
   * So they ride the scrub, and the layout they dirty is one fixed-height row
   * whose `contain: layout` ENFORCES that containment rather than merely
   * asserting it — the row's height is fixed at 54px (50px at the 920 cut) and
   * its size is decided from outside, so nothing the scrub does to the items
   * inside can reach the page below.
   */
  --sf-row-gap: calc(32px - 4px * var(--sf-morph-row));
  --sf-nav-gap: calc(32px - 4px * var(--sf-morph-row));
  --sf-row-font: calc(var(--text-body) + (var(--text-support) - var(--text-body)) * var(--sf-morph-row));
  --sf-search-size: calc(19px - 1px * var(--sf-morph-row));
  --sf-cart-glyph: calc(17px - 2px * var(--sf-morph-row));
  --sf-cart-gap: calc(9px - 1px * var(--sf-morph-row));
  --sf-cart-pad-y: calc(6px - 1px * var(--sf-morph-row));
  --sf-cart-pad-x: calc(11px - 1px * var(--sf-morph-row));
  --sf-cart-total-font: calc(var(--text-small) + (var(--text-caption) - var(--text-small)) * var(--sf-morph-row));
  align-items: stretch;
  /* The row is laid out from the outside in — full width, fixed height — so
   * layout containment costs it nothing and makes the paragraph above a rule
   * the engine keeps rather than a claim this file makes. */
  contain: layout;
  display: flex;
  font-size: var(--sf-row-font);
  gap: var(--sf-row-gap);
  height: 54px;
  position: relative;
  /* The shadow is the one thing still keyed to the discrete pinned state, so it
   * is the one thing still transitioned. */
  transition: box-shadow 160ms ease;
  z-index: 1;
}

/*
 * The row's left group slides clear of the badge that lands on top of it.
 * `translateX`, not padding: Ryan asked for a translate, and a transform keeps
 * the three of them off the layout path even though the metrics above are on
 * it. They move as one because they carry the same offset — the flex layout has
 * already put them in the right order and the right distances apart.
 *
 * The contact pair is NOT in this list: it is pushed to the far right by an auto
 * margin, which transforms do not disturb.
 */
.sf-header__bar .sf-nav,
.sf-header__bar .sf-iconbtn,
.sf-header__bar .sf-cart {
  transform: translateX(calc(var(--sf-morph-row) * var(--sf-nav-clearance)));
}

.sf-header__bar .sf-nav,
.sf-header__bar .sf-nav__list {
  gap: var(--sf-nav-gap);
}

.sf-nav {
  align-items: stretch;
  display: flex;
  min-width: 0;
}

.sf-nav__list {
  align-items: stretch;
  display: flex;
}

/*
 * On the board each nav item is itself a full-height flex item of the bar, so
 * its border-bottom lands ON the bar's bottom edge. Here a <li> sits between
 * the bar and the link, and a block-level link inside a stretched <li> is only
 * as tall as its own text — which floats the underline up into the middle of
 * the bar. Making the <li> a stretch container hands the link the full height
 * back.
 */
.sf-nav__list > li {
  align-items: stretch;
  display: flex;
}

.sf-nav__link {
  align-items: center;
  color: var(--brand-color-nav-text, #d9e8ff);
  display: flex;
  font-size: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.sf-nav__link:hover {
  color: var(--brand-color-surface, #ffffff);
}

/*
 * Active wayfinding: white label, orange underline flush to the bar's edge.
 *
 * The rule belongs to the active item ALONE — a transparent placeholder border
 * on every item would eat 3px off each one's content box and lift all the
 * inactive labels 1.5px above where the board centres them.
 */
.sf-nav__link[aria-current="page"] {
  border-bottom: 3px solid var(--brand-color-logo-orange, #ff5a00);
  box-sizing: border-box;
  color: var(--brand-color-surface, #ffffff);
  font-weight: 600;
}

/* --- search and cart --- */

.sf-iconbtn {
  align-self: center;
  background: none;
  border: 0;
  color: var(--brand-color-nav-text, #d9e8ff);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  padding: 0;
}

.sf-iconbtn:hover {
  color: var(--brand-color-surface, #ffffff);
}

/* The board draws both glyphs a px smaller on the pinned row. They carry `width`
 * and `height` ATTRIBUTES from the component, which these properties override —
 * and, unlike the attributes, can be interpolated by the scrub. */
.sf-header__bar .sf-iconbtn svg {
  height: var(--sf-search-size);
  width: var(--sf-search-size);
}

.sf-header__bar .sf-cart svg {
  height: var(--sf-cart-glyph);
  width: var(--sf-cart-glyph);
}

.sf-cart {
  align-items: center;
  align-self: center;
  background: var(--brand-color-logo-orange, #ff5a00);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  display: inline-flex;
  flex: none;
  gap: var(--sf-cart-gap);
  padding: var(--sf-cart-pad-y) var(--sf-cart-pad-x);
  position: relative;
  text-decoration: none;
}

.sf-cart:hover {
  background: var(--brand-color-logo-orange-bright, #ff7529);
}

.sf-cart__badge {
  background: var(--brand-color-surface, #ffffff);
  border-radius: var(--radius-floaty);
  box-shadow: 0 1px 3px color-mix(in srgb, var(--brand-color-ink, #17181a) 30%, transparent);
  color: var(--brand-color-accent, #c4530a);
  font-size: var(--text-micro);
  font-weight: 700;
  line-height: 1;
  padding: 2px 5.5px;
  position: absolute;
  right: -8px;
  top: -7px;
}

.sf-cart__total {
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--sf-cart-total-font);
}

/* --- experts strip --- */

/*
 * Out of flow and spanning both rows, because the photo is taller than the nav
 * row it sits in. The layer is full-bleed and its inner `.sf-container` does
 * the gutter maths, so the strip lands on the same content edge as the rows.
 */
.sf-experts-layer {
  bottom: 0;
  left: 0;
  pointer-events: none;
  position: absolute;
  right: 0;
  z-index: 2;
}

/*
 * The strip comes apart in the order Ryan called: the eyebrow goes first, the
 * photo next, and the contact pair is the last thing standing — it slides right
 * across the space the photo has vacated and collapses onto one line, so that
 * where it goes out the slim bar's own pair comes in.
 *
 * That is the WIDE state's choreography, and below 1240 there is none of it
 * left to run: the photo is already gone (there is nothing to slide across) and
 * the row's own pair is icons (there is no arrangement to collapse into). Both
 * shift variables go to zero there and the pitch simply holds its place and
 * dissolves. The three `--sf-*` endpoints are what say which it is doing, so
 * this transform is written once and means both.
 *
 * The handoff itself is the one place in this header still drawn twice. A true
 * single-element morph is not available for it: the full header says "Chat Now"
 * and the slim bar says "Chat" — different text, which no transform reaches —
 * and the pair also changes stacking direction and both font sizes between the
 * two states, and below 1240 the two copies are not even the same KIND of thing
 * (words against glyphs). What holds it together at every width is that both
 * copies are right-aligned on the same content edge and centred on the same
 * row, so the crossfade happens in one slot however differently the two sides
 * are drawn.
 */
.sf-experts__pitch {
  transform: translate(
    calc(var(--sf-morph-pitch) * var(--sf-pitch-shift)),
    calc(var(--sf-morph-pitch) * var(--sf-pitch-drop))
  );
}

.sf-experts__eyebrow {
  opacity: calc((0.45 - var(--sf-morph)) / 0.45);
  will-change: opacity;
}

.sf-experts__photo {
  opacity: calc((0.5 - var(--sf-morph)) / 0.5);
  will-change: opacity;
}

.sf-experts .sf-header__contact {
  opacity: calc(
    (var(--sf-crossfade-end) - var(--sf-morph)) /
      (var(--sf-crossfade-end) - var(--sf-crossfade-start))
  );
  will-change: opacity;
}

/* Chat rises one line to sit beside the number, and the number steps left to
 * make room for it — the slim bar's arrangement, reached before the handoff. */
.sf-experts .sf-contact--phone {
  transform: translateX(calc(var(--sf-morph-pitch) * var(--sf-contact-collapse)));
}

.sf-experts .sf-contact--chat {
  transform: translateY(calc(var(--sf-morph-pitch) * var(--sf-chat-lift)));
}

.sf-experts-layer__inner {
  display: flex;
  justify-content: flex-end;
}

.sf-experts {
  align-items: flex-end;
  display: flex;
  gap: 20px;
  pointer-events: auto;
}

.sf-experts__pitch {
  align-items: flex-end;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding-bottom: 12px;
  text-align: right;
}

.sf-experts__eyebrow {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
}

/* The pair stacks here rather than sitting in a row as it does in the bars. */
.sf-experts .sf-header__contact {
  align-items: flex-end;
  flex-direction: column;
  gap: 5px;
  margin-left: 0;
}

.sf-experts .sf-contact--phone {
  font-size: var(--text-emphasis);
}

.sf-experts .sf-contact--chat {
  font-size: var(--text-small);
}

.sf-experts__photo {
  flex: none;
  height: 106px;
  position: relative;
  width: 232px;
}

.sf-experts__photo img {
  display: block;
  height: 106px;
  object-fit: contain;
  object-position: bottom;
  width: 232px;
}

/* Name plates sit ON the photo; their offsets travel with it in brand config. */
.sf-experts__plate {
  background: var(--brand-color-surface, #ffffff);
  border-radius: 2px;
  bottom: 9px;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--brand-color-ink, #17181a) 25%, transparent);
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-micro);
  height: 18px;
  line-height: 18px;
  position: absolute;
  text-align: center;
  width: 37px;
}

/* --- the contact pair --- */

.sf-header__contact {
  align-items: center;
  display: flex;
  gap: 16px;
  margin-left: auto;
}

.sf-contact {
  align-items: center;
  display: inline-flex;
  gap: 8px;
  text-decoration: none;
  white-space: nowrap;
}

.sf-contact--phone {
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-body);
  font-weight: 700;
}

.sf-contact--chat {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-size: var(--text-support);
}

.sf-contact--chat .sf-contact__label {
  text-decoration: underline;
}

.sf-contact:hover {
  color: var(--brand-color-surface, #ffffff);
}

/* The presence dot: always on, because the AI answers around the clock. */
.sf-dot {
  background: var(--brand-color-good-on-dark, #3dd68c);
  border-radius: var(--radius-pill);
  flex: none;
  height: 7px;
  width: 7px;
}

/* ------------------------------------------------ header (pinned nav row) -- */

/*
 * What is left once the morph above is scrubbed by `--sf-morph`: the handful of
 * things that are genuinely DISCRETE rather than continuous.
 *
 * `<HeaderSticky>` still watches a sentinel in normal flow below the header and
 * still flips `data-stuck` at the scroll position the row pins at, because two
 * things cannot be interpolated. The drop shadow is one — it belongs to a bar
 * that is over the page, not to one still in it. The other is
 * VISIBILITY: an element faded to `opacity: 0` is still in the tab order and
 * still in the accessibility tree, so everything the morph fades out is also
 * hidden outright once the header is pinned, and the row's contact pair — the
 * one element that arrives rather than leaves — is kept out of the tree until
 * the same moment, then fades in over 160ms. That reveal is the only timed
 * animation left in the header, and it lands after the scrub has finished, so
 * the end of the travel stays calm.
 */

.sf-header[data-stuck="true"] .sf-header__bar {
  box-shadow: 0 6px 18px color-mix(in srgb, var(--brand-color-ink, #17181a) 25%, transparent);
}

/* --- the pair that arrives --- */

/*
 * Its opacity is the exact complement of the outgoing copy's, over the same
 * window, so the two cross while superimposed and the pair never dims as a
 * whole. Nothing here is transitioned and nothing here reads `data-stuck`: a
 * timed fade at the pin is what made the pair dip transparent through the slide
 * and then flash back at the end.
 *
 * `visibility` still has to be discrete — an `opacity: 0` element is in the tab
 * order and the accessibility tree — so the script flips `data-morph` at exactly
 * `--sf-crossfade-start`, where this copy's opacity is still 0. The two copies
 * are therefore both reachable only for the width of the crossfade itself.
 */
.sf-header__bar .sf-header__contact {
  opacity: calc(
    (var(--sf-morph) - var(--sf-crossfade-start)) /
      (var(--sf-crossfade-end) - var(--sf-crossfade-start))
  );
  pointer-events: none;
  visibility: hidden;
  will-change: opacity;
}

.sf-header[data-morph="on"] .sf-header__bar .sf-header__contact {
  visibility: visible;
}

.sf-header[data-stuck="true"] .sf-header__bar .sf-header__contact {
  pointer-events: auto;
}

/* --- what the morph has already faded to nothing --- */

.sf-header[data-stuck="true"] .sf-header__scrim,
.sf-header[data-stuck="true"] .sf-experts-layer {
  visibility: hidden;
}

/*
 * Someone who asked for less motion gets no scrub at all: the progress variable
 * is forced to its two endpoints and swaps between them at the pin. `!important`
 * is what lets a stylesheet outrank the inline value the script writes — the one
 * place in this file where that is the mechanism rather than a shortcut.
 */
@media (prefers-reduced-motion: reduce) {
  .sf-header {
    --sf-morph: 0 !important;
  }

  .sf-header[data-stuck="true"] {
    --sf-morph: 1 !important;
  }

  .sf-header__bar {
    transition: none;
  }

  /*
   * AND THE ARRIVING PAIR STAYS OUT OF THE TREE UNTIL THE ROW PINS.
   *
   * `data-morph` is written by the driver off the RAW scroll progress, which
   * reduced motion does not touch — it is the stylesheet that forces
   * `--sf-morph` to its endpoints. So from p=0.6 up to the pin, the row's copy
   * of the contact pair is `visibility: visible` at a computed opacity of 0
   * while the experts copy is still at full opacity: an invisible tab stop, and
   * a second announcement of the same phone number. Scrubbed, that window is
   * the crossfade itself and both copies are legitimately on screen; snapped, it
   * is nothing but a duplicate. `data-stuck` is the one flag that means the same
   * thing in both modes, so it is what gates the copy here.
   */
  .sf-header:not([data-stuck="true"]) .sf-header__bar .sf-header__contact {
    visibility: hidden;
  }
}

/* Zero-height mark in normal flow just below the header — the only thing left
 * that still reports the scroll position once the header itself is pinned. */
.sf-header__sentinel {
  height: 0;
}

/* ------------------------------------------------- header (photo shed) -- */

/*
 * THE FULL HEADER'S FIRST CUT: the salesmen's photo goes, and the phone number
 * and Chat Now stay written out.
 *
 * This is the reversal Ryan ruled after seeing the previous order. The pitch is
 * a sales line — the number is the point of it — so when the pitch and the cart
 * start to crowd each other, the 232px PHOTO is what buys the room back, not
 * the words. 252px (the photo plus its 20px gap) against ~154px for the widest
 * line of the pair: shedding the photo alone moves the pitch's whole cluster
 * back to the content edge and clears the cart by a margin nothing else in this
 * header has.
 *
 * The width is the one the icon conversion used to hold, because it is the
 * measured one: sweeping the whole morph, the pitch first crowds the cart at
 * ~1220 while the photo is still in place. With the photo gone at 1240 instead,
 * the same sweep reads 211px of clearance at 1220, and the written-out pair
 * stays over the 24px margin for another 200px of width (see the icon cut,
 * further down).
 *
 * With the photo gone the pitch has nothing to slide ACROSS either, so the
 * morph's slide collapses to zero at the same width, and the scrim can let its
 * mask run further right — there is no photo left for it to stay clear of.
 *
 * THE HANDOFF IS RE-DERIVED HERE, because both of its ends have changed. The
 * pitch no longer travels, and the pinned row's own pair is icons from this
 * width down, so there is no arrangement left for the outgoing copy to collapse
 * INTO: a step sized to the incoming icon pair (46px) lands the pitch's own
 * Chat Now on top of its own phone number, and a step sized to its own Chat Now
 * (114.3px) needs 268px of right-hand room and puts the number back on the cart
 * at ~1147. So below this width the pitch does not rearrange at all. It holds
 * its stacked shape, right-aligned on the content edge, and dissolves exactly
 * where the row's pair arrives: same slot, same crossfade window, one less
 * thing moving. Measured at 1030, the two copies then occupy the SAME box to
 * the pixel; at 768 they are 2px apart.
 *
 * `--sf-pitch-drop` still centres the stack on the pinned row's line, so the
 * two copies cross concentrically instead of one sitting low.
 */
@media (max-width: 1239.98px) {
  .sf-header {
    --sf-pitch-shift: 0px;
    --sf-contact-collapse: 0px;
    --sf-chat-lift: 0px;
    /* Measured: stacked and un-collapsed, the pair's centre rests 5.5px above
     * the pinned row's centre line. */
    --sf-pitch-drop: 5.5px;
  }

  .sf-experts__photo {
    display: none;
  }

  .sf-header__scrim {
    height: 96px;
    -webkit-mask-image: linear-gradient(to right, #000000 50%, rgba(0, 0, 0, 0) 75%);
    mask-image: linear-gradient(to right, #000000 50%, rgba(0, 0, 0, 0) 75%);
  }

  /*
   * THE PINNED ROW IS NOT PART OF THAT RULING, and this selector is what keeps
   * the two states apart.
   *
   * The slim bar is a 54px strip carrying five nav links, search, the cart and
   * the pair, and it goes to icons here — Ryan called that behaviour right. The
   * full header is a 136px block with a whole pitch of its own and none of that
   * pressure, so it keeps its words for another 200px of width. Until this pass
   * ONE rule served both, and the state with room was being compressed by the
   * state without.
   *
   * The clip keeps each label in the accessibility tree, so the phone link is
   * still announced as its number.
   */
  .sf-header__bar .sf-header__contact .sf-contact__label {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }
}

/* ------------------------------------------------------ header (tablet) -- */

/* Sheds, in order, exactly what the board sheds on the way down. */
@media (max-width: 1023.98px) {
  .sf-header {
    /* The badge sits 10px into a 74px masthead and ends 83px into the header. */
    --sf-logo-shift: 73px;
    /* 68 / 60. The board does not redraw the slim bar at this cut but does
     * shrink the masthead badge, so here the badge GROWS into its pinned size
     * instead of shrinking. */
    --sf-logo-scale: 1.1333;
    --sf-masthead-height: 74px;
    /* No `--sf-pitch-drop` at this cut, nor at the one below it: every width
     * they serve is below the full-bar icon cut, where the pair is a single
     * line of glyphs already resting ON the row's centre and the drop is
     * zeroed. */
  }

  .sf-header__masthead {
    gap: 11px;
  }

  .sf-header__masthead > * {
    margin-top: 6px;
  }

  .sf-header__logomark--full img {
    width: 60px;
  }

  .sf-header__masthead .sf-header__eyebrow {
    font-size: var(--text-caption);
  }

  /* Height is NOT redrawn here: the board's slim bar is unchanged at this cut,
   * and the row is that bar. The type is already at the support step in both
   * states, so the scrub has nothing to interpolate — it is named as a constant
   * rather than dropped, so every breakpoint declares the same set. */
  .sf-header__bar {
    --sf-row-font: var(--text-support);
    --sf-nav-gap: calc(24px + 4px * var(--sf-morph-row));
    --sf-cart-gap: 8px;
    --sf-cart-pad-y: 5px;
    --sf-cart-pad-x: 10px;
    --sf-cart-total-font: var(--text-caption);
  }

  .sf-experts__pitch {
    gap: 4px;
    padding-bottom: 10px;
  }

  .sf-experts .sf-contact--phone {
    font-size: var(--text-body);
  }

  .sf-experts .sf-contact {
    gap: 7px;
  }

  /* The pinned row is NOT redrawn at this cut — the board's note is explicit
   * that the full slim recipe still fits at ~1024, on 40px side padding alone.
   * It first gives way at the cut below. */
}

/*
 * The board's tablet recipe, drawn for the 834 cut and moved up to 900 because
 * that is where it is actually needed.
 *
 * Below about 900 the WIDE recipe was still in force at a width too narrow for
 * it — 104px of badge clearance, 28px gaps — and the pinned row's contact pair
 * ran into the cart from 880 down (-42.1px at 840), while 830, one step into the
 * tight recipe, had 109.9px to spare. The cliff was the breakpoint, not the
 * layout. The gap closes at 1px per px here too (37.9 at 920), spending the 24px
 * margin by 906, so the recipe starts at 920: 921 keeps ~39px on the wide
 * recipe, 920 gets ~164px on the tight one.
 *
 * `.sf-container`'s own gutter cut stays at 833.98: that is a page-wide measure and
 * nothing here depends on it.
 */
@media (max-width: 919.98px) {
  /* The row is redrawn at 50px in this recipe, so what it owns of the viewport's
   * top edge shrinks with it. */
  :root {
    --sf-pinned-offset: 50px;
  }

  .sf-header__logomark--full img {
    width: 52px;
  }

  .sf-header__story {
    font-size: var(--text-micro);
  }

  .sf-header {
    /* The badge sits 14px into the masthead and ends 82px into the header. */
    --sf-logo-shift: 68px;
    /* 56 / 52 — the slim bar's badge is redrawn smaller here too. */
    --sf-logo-scale: 1.0769;
    /* 56px of badge plus the row's own 14px gap, and 10px beside it. The board
     * drew 6px there; the extra 4 is what puts the conservative box measure of
     * the badge-to-nav clearance over the 24px margin at this cut, as 36% does
     * at the wider ones. */
    --sf-nav-clearance: 80px;
    /* No `--sf-pitch-drop` at this cut: every width it serves is below the
     * full-bar icon cut, which zeroes it. */
  }

  .sf-header__bar {
    --sf-row-font: var(--text-small);
    --sf-row-gap: calc(32px - 18px * var(--sf-morph-row));
    --sf-nav-gap: calc(18px - 4px * var(--sf-morph-row));
    --sf-cart-gap: calc(8px - 8px * var(--sf-morph-row));
    --sf-cart-pad-y: calc(4px + 1px * var(--sf-morph-row));
    --sf-cart-pad-x: 9px;
    height: 50px;
  }

  /* --- the pinned row at this cut --- */

  .sf-header[data-stuck="true"] .sf-header__bar .sf-header__contact {
    gap: 12px;
  }

  .sf-header[data-stuck="true"] .sf-header__bar .sf-contact--phone {
    font-size: var(--text-support);
  }

  /* The price gives way off the cart pill; the badge count stays. (This row's
   * contact labels went at 1240, and the pitch's at 1040.) This
   * leaves and rejoins the flow, which no transition can smooth, so at this cut
   * alone the row's compaction ends with a snap. */
  .sf-header[data-stuck="true"] .sf-cart__total {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }

}

/* ------------------------------------------ header (full-bar contact icons) -- */

/*
 * AND THIS is where the full header's pair finally gives up its text — 200px
 * below where it used to, and only because the numbers say so.
 *
 * Ryan's hope for the reordering above was that the photo's 252px would carry
 * the written-out pair all the way to the mobile cut. It carries it 200px of
 * the way, and then a BREAKPOINT — not the layout — stops it.
 *
 * Sweeping 1440 → 768 at 4px steps and twelve morph positions each, the
 * binding moment is p=0.4 — the row has translated its badge clearance and
 * pushed the cart 104px right, and the pitch, which no longer slides, has not
 * moved. The gap from the cart to the pitch's phone line closes at 1px per px
 * of width: 51.1px at 1060, 31.1px at 1040, 27.1px at 1036, 23.1px at 1032 and
 * 15.1px at 1024. The 24px margin is spent at ~1035, so 1040 is the cut.
 *
 * WHAT IS WORTH RECORDING is what happens 1px lower. At 1023 the tablet recipe
 * takes over — smaller masthead, 40px gutter, tighter row — and the same gap
 * jumps back to 68.3px, staying over the margin down to ~976 and, after the
 * 920 recipe hands back another 24px of badge clearance, again from 900 to
 * ~818. So the written-out pair FITS at nearly every width below this cut; what
 * does not fit is the 16px band from 1024 to 1039, where the wide recipe is
 * still in force at a width it has outgrown. A responsive ladder cannot hand a
 * shed back and take it away again, so the cut is the widest width that fails,
 * and the pair gives up its text ~60px earlier than the layout alone requires.
 * Closing that band would mean moving the 1023.98 recipe cut up to 1039.98 —
 * and that cut is shared with the page's container gutter, deliberately, so it
 * is a ruling rather than a tweak. Flagged, not taken.)
 *
 * The eyebrow goes with the text, as it always did and for the same measured
 * reason - but the mechanism is worth recording, because it is not width. With
 * the pair STACKED the pitch is three lines tall and the eyebrow rides above
 * the nav row entirely, sharing no rows with the cart at any morph position: it
 * cannot collide, whatever its width. Collapsing the pair to one line drops the
 * eyebrow INTO the row's band, where its 143.6px — the widest thing left in the
 * pitch — closes on the cart at 1px per px. The two sheds are one decision.
 */
@media (max-width: 1039.98px) {
  .sf-experts .sf-contact__label {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }

  .sf-experts__eyebrow {
    display: none;
  }

  /*
   * Two icons side by side, not stacked. The pitch stacks its pair because two
   * lines of TEXT read better that way over the salesmen photo; two glyphs do
   * not, and stacked they also make the pitch tall enough to foul the row.
   */
  .sf-experts .sf-header__contact {
    flex-direction: row;
    gap: 16px;
  }

  /*
   * AND THE ICONS SIT ON THE NAV ROW'S OWN LINE.
   *
   * Ryan: "not vertically aligned properly — skewed down too far. Have them be
   * inline with the text in the nav items." The pitch is bottom-anchored to the
   * header, which is right for a stacked block hanging beside a photo and wrong
   * for a single line of glyphs: 12px of resting padding put the pair's centre
   * 7.5px BELOW the nav row's, so the icons read as sagging out of the bar. The
   * pinned row never had the problem, because its pair is a flex item of the
   * row itself.
   *
   * The number falls out of the geometry rather than a taste call: the pitch is
   * bottom-anchored to the header and the row IS the header's bottom 54px, so
   * the row's centre line sits 27px above the header's bottom edge and the
   * pair's sits `padding + half its 15px line`. 19.5px is what makes those
   * equal — and 17.5px at the cut below, where the board draws a 50px row.
   *
   * Padding, not a transform, because this is where the pair RESTS — the state
   * Ryan is looking at — and because with the line already on the row's centre
   * the morph has nowhere left to move it: `--sf-pitch-drop` goes to zero, and
   * the outgoing icons and the incoming ones cross on one line, glyph over
   * glyph.
   */
  .sf-experts__pitch {
    padding-bottom: 19.5px;
  }

  .sf-header {
    --sf-pitch-drop: 0px;
  }
}

/* The tablet recipe below draws a 50px row, not a 54px one, so the same
 * alignment sits 2px lower. */
@media (max-width: 919.98px) {
  .sf-experts__pitch {
    padding-bottom: 17.5px;
  }
}

/* ------------------------------------------------------- header (mobile) -- */

.sf-header__mobile {
  display: none;
}

/*
 * 768, not 640. The boards put MobileA at ~768 and the build had it at 640,
 * which left 641-767 rendering a desktop header at a width no board draws — and
 * it does not fit: measured at 670, the search button overlapped the last nav
 * item by 37px and the experts eyebrow overlapped the cart by 37px. There is no
 * recipe to compress into that band, because the band below it already has one:
 * the mobile bar.
 *
 * `767.98` and not `768` per the convention at the top of this file: 768 itself
 * is a tablet. The page-level blocks that used to break at 640 — the container's
 * last gutter rung, the title band's stack, the footer's two-up columns and its
 * fine print — now break here too, so "mobile" is one width across the whole
 * site rather than one width for the chrome and another for what it wraps.
 */
@media (max-width: 767.98px) {
  .sf-header__full,
  .sf-header__scrim {
    display: none;
  }

  /*
   * MobileA's frozen header. There is no masthead to travel and no nav row to
   * reveal at this cut — the bar IS the header — so it pins at 0 rather than at
   * a negative offset, and there is no morph: `--sf-morph` stays at the 0 the
   * script never overwrites, because the masthead it would measure is display:
   * none and the travel comes out as zero.
   *
   * The shadow is the one thing that still marks "scrolled", and it rides the
   * same sentinel: with the bar pinned, the sentinel below it crosses the top of
   * the viewport exactly when the page has moved past the bar. A timed fade is
   * right here — nothing at this cut is scrubbed, so there is nothing for it to
   * fall out of step with.
   */
  .sf-header {
    transition: box-shadow 160ms ease;
  }

  /*
   * The pin, at 0 rather than at a negative offset — and gated on the driver
   * exactly as the desktop pin is. Nothing at this cut is scrubbed, so a
   * script-less mobile bar would in fact pin correctly; it is gated anyway so
   * that "sticky" and "the driver is running" stay the same fact at every width
   * rather than two rules to keep in step.
   */
  .sf-header[data-driven="on"] {
    top: 0;
  }

  /* The mobile bar is 54px, not the 50px the 920 recipe above sets — and that
   * recipe also matches here, so this restates the value rather than inheriting
   * the wrong one. */
  :root {
    --sf-pinned-offset: 54px;
  }

  .sf-header[data-stuck="true"] {
    box-shadow: 0 6px 18px color-mix(in srgb, var(--brand-color-ink, #17181a) 25%, transparent);
  }

  .sf-header__mobile {
    align-items: center;
    display: flex;
    gap: 10px;
    height: 54px;
    position: relative;
  }
}

.sf-burger {
  align-items: center;
  background: none;
  border: 0;
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  padding: 0;
}

/*
 * Centred and overlapping, with the eyebrow tucked to its left. Absolute is
 * correct here: 50% of the container's padding box IS the bar's true centre.
 */
.sf-header__logomark--mobile {
  left: 50%;
  position: absolute;
  top: 7px;
  transform: translateX(-50%);
}

.sf-header__logomark--mobile img {
  width: 64px;
}

.sf-header__mobile .sf-header__eyebrow {
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-micro);
  font-weight: 700;
  letter-spacing: 0.07em;
  position: absolute;
  right: calc(50% + 40px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
}

.sf-header__icons {
  align-items: center;
  display: flex;
  gap: 8px;
  margin-left: auto;
}

.sf-ghost {
  align-items: center;
  border: 1px solid color-mix(in srgb, var(--brand-color-surface, #ffffff) 30%, transparent);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  display: inline-flex;
  padding: 6px 7px;
  position: relative;
}

.sf-ghost__icon {
  display: inline-flex;
  position: relative;
}

.sf-ghost__dot {
  background: var(--brand-color-good-on-dark, #3dd68c);
  border: 1px solid var(--brand-color-nav, #192950);
  border-radius: var(--radius-pill);
  height: 6px;
  position: absolute;
  right: -4px;
  top: -3px;
  width: 6px;
}

/* ----------------------------------------------------------- title band -- */

/*
 * THE STUCK LINE'S FOUR KNOBS, and the height they add up to.
 *
 * Ryan's words (2026-08-26): as the page scrolls the BREADCRUMB hides away
 * behind the pinned nav bar and the title + controls line sticks in place, with
 * a SMALLER title and ASYMMETRIC padding — more space above the title than
 * below, because the chrome's logo badge hangs 23px below the bar's bottom edge
 * at the left (measured at 1440: bar bottom y=54, badge bottom y=77, x 64-132)
 * and the title starts at that same gutter. The title has to clear it.
 *
 * These are the numbers to change at review. `--sf-band-stuck-pad-top` is
 * literally "space between the bar and the title's line box"; the height below
 * is derived from it, so moving one number moves the band and nothing else.
 *
 * `--sf-band-stuck-line` is the RENDERED `normal` line box of Schibsted Grotesk
 * 700 at the stuck size (measured 2026-08-26: 26px -> 32px). It is written out
 * because the stuck rule has to shrink the GLYPHS without moving the BOX — see
 * `.sf-title-band__title` below.
 *
 * The `+ 1px` is the band's own bottom border, which is inside the stuck height
 * because the height is measured to the band's outer edge.
 *
 * THE MOBILE CUT HAS ITS OWN FOUR, AND ITS OWN LINE (Ryan's ruling,
 * 2026-08-27 — the band is sticky on a phone too, reversing the static-below-768
 * scoping this block shipped with). The stuck line there is the TITLE AND THE
 * QUIET DROPDOWN SIDE BY SIDE, which is a different line from the desktop's in
 * two ways that need their own numbers:
 *
 *  - THE DROPDOWN IS THE TALL THING ON IT, not the title. Measured at 390:
 *    the control is 44px (12px padding either side of a 14.5px line, plus its
 *    1px border), against ~26px for the title at its stuck cut. So
 *    `--sf-band-stuck-line-m` is the DROPDOWN's rendered box, and the title is
 *    centred in it.
 *  - THE OVERHANG IT HAS TO CLEAR IS THE MOBILE LOGOMARK, and that mark is
 *    CENTRED rather than left-hung: measured at 390, x 163-227, y 7-71, so it
 *    hangs 17px below the bar's bottom edge and it hangs there in the middle of
 *    the line, exactly where a truncating title runs. 20px of top padding puts
 *    the line's top at y=74 — clear of the mark by 3px — where the desktop's 34
 *    clears a badge that only ever overlaps the title's first few glyphs.
 *
 * `--sf-band-stuck-h` is 0 on any page whose band is NOT DRIVEN, and on any
 * brand that does not wear this chrome; category.css reads it as
 * `var(--sf-band-stuck-h, 0px)` so both are left at exactly the offsets they
 * had before the band could stick.
 */
:root {
  --sf-band-stuck-title: var(--text-section);
  --sf-band-stuck-line: 32px;
  --sf-band-stuck-pad-top: 34px;
  --sf-band-stuck-pad-bottom: 20px;
  /* The mobile cut's own four. `--text-product` is the one step of the scale
   * that reads as a title beside a 14.5px control without crowding it; 26px is
   * its RENDERED `normal` line box in Schibsted Grotesk 700 (measured
   * 2026-08-27), written out for the same reason the desktop's 32 is. */
  --sf-band-stuck-title-m: var(--text-product);
  --sf-band-stuck-title-line-m: 26px;
  --sf-band-stuck-line-m: 44px;
  --sf-band-stuck-pad-top-m: 20px;
  --sf-band-stuck-pad-bottom-m: 12px;
  --sf-band-stuck-h: 0px;
}

.sf-title-band {
  /* Named because the STUCK arithmetic is written against them: the row's
   * stuck `translateY` is exactly the difference between the band's own bottom
   * padding and the stuck line's. */
  --sf-band-pad-top: 26px;
  --sf-band-pad-bottom: 26px;
  /*
   * ONE LINE'S BOX, in px, per cut — NOT the title's box.
   *
   * These are the values `line-height: normal` already renders — measured
   * 2026-08-26 for Schibsted Grotesk 700: 30px -> 37px, 40px -> 49px — so the
   * resting band is unchanged to the pixel. They are written out because the
   * stuck state changes the title's FONT SIZE, and a line box that moved with
   * it would change the row's height, the band's height, and therefore the
   * position of everything below the band at the instant it sticks. The LINE
   * box is a constant; only the glyphs inside it shrink.
   *
   * IT IS NOT THE HEIGHT THE STUCK STATE HAS TO RESERVE, and reading it as if
   * it were is the bug this comment was written on top of (measured
   * 2026-08-26): from 768 to ~1024 the category title WRAPS to two lines at
   * rest and to one when stuck, so a stuck padding derived from this number
   * reserved 49px where the resting title occupied 98 and the band's flow
   * height dropped 188 -> 141 at the pin — 47px of page snapping up under the
   * reader. What the stuck state reserves is the title's WHOLE resting box,
   * which is a wrap point and therefore measured: `--sf-band-title-box`,
   * published by `title-band-sticky.tsx`. This value is only its fallback,
   * correct exactly where the title does not wrap.
   */
  --sf-band-title-line: 37px;
  background: var(--brand-color-paper, #fbfbfa);
  border-bottom: var(--border-hairline);
  /* As in the header and the footer: the boards declare no leading anywhere in
   * this band, so every line box is `normal`. Without this, globals.css's
   * `body { line-height: 1.65 }` deepens the crumb row and the title, and the
   * whole page below the band shifts down by the difference. */
  line-height: normal;
  /* 26px top clears the header badge protruding into this band. */
  padding: var(--sf-band-pad-top) 0 var(--sf-band-pad-bottom);
}

/* Category pages wear the 40px title, so their line box is the 40px one. */
.sf-title-band--category {
  --sf-band-title-line: 49px;
}

.sf-crumbs__list {
  align-items: center;
  color: var(--brand-color-faint, #9a988f);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-body);
  gap: 12px;
}

/* The home crumb's glyph is sized here rather than left on the SVG's own
 * attributes, because it is the one crumb that changes size at the mobile cut
 * and a width attribute cannot answer a media query. */
.sf-crumbs svg {
  height: 18px;
  width: 18px;
}

.sf-crumbs__list li {
  align-items: center;
  display: inline-flex;
  gap: 12px;
}

.sf-crumbs a {
  align-items: center;
  color: inherit;
  display: inline-flex;
  text-decoration: none;
}

.sf-crumbs a:hover {
  color: var(--brand-color-ink, #17181a);
  text-decoration: underline;
  text-underline-offset: 4px;
}


.sf-crumbs__current {
  font-weight: 600;
}

.sf-title-band__row {
  align-items: flex-start;
  display: flex;
  gap: 24px;
  justify-content: space-between;
  margin-top: 18px;
}

.sf-title-band__title {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-page-title);
  font-weight: 700;
  letter-spacing: -0.015em;
  /* The desktop boards give this line no leading; the mobile board is the one
   * that names 1.12, and it does so because that title wraps. Both are below.
   * `--sf-band-title-line` IS that leading, written in px rather than left as
   * the `normal` keyword — see the declaration for why the box has to be a
   * constant the stuck state cannot move. */
  line-height: var(--sf-band-title-line);
  margin: 0;
  /* globals.css balances every h1/h2/h3; no board asks for it, and on the
   * mobile board this title's wrap point is drawn, not balanced. `wrap` is the
   * initial value and the one that turns balancing OFF — `normal` is not a
   * `text-wrap` keyword at all (it is `white-space`'s), so it was dropped at
   * parse time and `balance` stayed in force from globals.css. */
  text-wrap: wrap;
}

/* Category pages get the heavier 40px cut — category is special. */
.sf-title-band__title--category {
  font-size: var(--text-category-title);
}

.sf-title-band__controls {
  flex: none;
  padding-top: 8px;
}

/* ------------------------------------------------- the band's stuck state -- */

/*
 * THE BAND PINS UNDER THE PINNED NAV ROW, and the crumb row goes behind it.
 *
 * THE MECHANISM, because it is the same one the comparison table's header row
 * uses and for the same reason: the band's FLOW HEIGHT NEVER CHANGES. A sticky
 * element that changed height would move everything below it at the exact
 * moment the reader is scrolling past — so nothing here is added to or taken
 * out of the band's box. What changes is which part of that box is on screen.
 *
 * The band sticks not at the bar but at `--sf-pinned-offset - bandHeight +
 * stuckHeight`, which puts its BOTTOM edge `stuckHeight` px below the bar. The
 * crumb row, which lives in the top of the box, therefore travels off the top
 * of the viewport under its own steam and comes to rest BEHIND the bar (the
 * header's z-index of 40 beats this band's 20, and the bar is opaque). No
 * script moves it, and there is nothing to animate.
 *
 * `--sf-band-h` is the band's rendered height, measured and published by
 * `title-band-sticky.tsx`, for the reason every other length in this chrome is
 * measured: it depends on which typeface has loaded, on where the title wraps,
 * and on which breakpoint's recipe is in force.
 *
 * `--sf-band-title-box` and `--sf-band-stuck-box` come from the same driver and
 * are what keep the promise above TRUE AT EVERY WIDTH rather than only where
 * the title happens not to wrap: the resting title's whole rendered box, and
 * the stuck title's own box with this padding taken out of the way. Both are
 * measured in a state the driver forces for the read, so neither can be a
 * guess about a wrap point. See the stuck title rule at the foot of this block.
 *
 * `data-driven` gates the pin exactly as it does on the header: before the
 * height is known the offset above would resolve out of a zero and pin the band
 * with its crumbs across the top of the viewport. Without scripting the band is
 * the plain, static, complete band — which is the correct plainer thing.
 *
 * THE STUCK LINE ITSELF is three declarations:
 *
 *  - the row bottom-aligns, so the title's line box sits at the row's bottom
 *    edge however tall the controls beside it are. That is what makes the
 *    space BELOW the title a constant this stylesheet controls rather than a
 *    consequence of the page's own control;
 *  - the row is TRANSLATED down by the difference between the band's resting
 *    bottom padding and the stuck line's, which is how the space below the
 *    title shrinks without the band's box changing size (a transform does not
 *    lay out);
 *  - the title's glyphs shrink to `--sf-band-stuck-title` and everything the
 *    shrink frees — the leading, AND any line the smaller cut no longer needs
 *    to wrap onto — is handed back as `padding-top`, so the title's BOX is the
 *    same box it occupies at rest and the row's height cannot move. The freed
 *    space is above the glyphs, which is where it is wanted, and the part of it
 *    that is above the band's visible top is simply hidden behind the bar.
 *
 *    THE PADDING IS THE DIFFERENCE BETWEEN TWO MEASURED BOXES, not between two
 *    declared line boxes, and that is the whole of the 2026-08-26 fix. The
 *    declared version read `--sf-band-title-line - --sf-band-stuck-line`, which
 *    is the right answer only where BOTH cuts render on one line. From 768 to
 *    ~1024 the category title wraps to two at rest and to one when stuck, so it
 *    reserved 49px for a title occupying 98 and the band's flow height fell
 *    188 -> 141 the instant it pinned — 47px of page snapping up under the
 *    reader, and the invariant at the top of this block broken by the very
 *    declaration meant to keep it. `--sf-band-title-box` is the RESTING box,
 *    wrap and all; `--sf-band-stuck-box` is what the stuck cut actually needs;
 *    the difference is exactly the space that has to be reserved, at any width
 *    and for any title. `max(0px, …)` is the floor for a stuck cut that somehow
 *    needs MORE room than the resting one — impossible with a smaller font, and
 *    a negative padding would be a parse error rather than a clamp.
 *
 *    The `var()` fallbacks are the pre-driver declared pair, so an unscripted
 *    page — which never pins at all — is left at exactly the arithmetic it had.
 *
 * IT IS NO LONGER SCOPED TO THE 768 CUT (Ryan's ruling, 2026-08-27). It shipped
 * desktop-only on the argument that below the cut the band is a STACK with a
 * full-width control, and a stuck version of that would eat a phone's viewport.
 * The ruling keeps the objection and rejects the conclusion: on a phone the
 * stuck band is not the stack, it is the TITLE AND THE DROPDOWN ON ONE LINE,
 * with the title truncated to an ellipsis. That line is 77px — 20 + the
 * dropdown's own 44 + 12 + the border — against the desktop's 87, so it costs a
 * phone LESS chrome than a laptop pays, and the reader keeps the one thing the
 * band is for: which catalog page they are on, and the control that changes it.
 *
 * THE TRUNCATION IS RULED, not a fallback: "In-Ground Adjustable Bas…" is
 * acceptable because THE CATEGORY WORDS OUTRANK "Basketball Hoops" — the tail
 * of this page's title is the part every page in the catalog shares, so it is
 * the part worth losing. The dropdown keeps its INTRINSIC width beside it and
 * the title takes what is left; the reverse (a dropdown squeezed to fit a whole
 * title) would truncate the one thing on the line that is a control.
 *
 * SO THE PIN AND THE CRUMB ROW ARE NOW UNSCOPED — one band, one mechanism, both
 * cuts — and only the STUCK LINE'S OWN RECIPE is per-cut, because only that is
 * genuinely different between them. Nothing about the band's RESTING mobile
 * layout changed; it is the same 14/18 stack it was.
 *
 * NO TRANSITION anywhere in here, in either direction: the state is discrete
 * and the travel is the scroll's. There is nothing for `prefers-reduced-motion`
 * to turn off.
 */

.sf-title-band[data-driven] {
  position: sticky;
  top: calc(
    var(--sf-pinned-offset, 0px) - var(--sf-band-h, 0px) + var(--sf-band-stuck-h)
  );
  /* Over the page — the comparison table's own pinned header cells sit at 5
   * and below — and UNDER the header's 40, which parks the nav bar and the
   * badge that hangs off it over the top of this band. */
  z-index: 20;
}

/* `visibility`, not `opacity`: a crumb parked behind an opaque bar is still a
 * tab stop, and tabbing to it would scroll the page to something the reader
 * cannot see. It keeps its box, so the band's height is untouched. */
.sf-title-band[data-stuck="true"] .sf-crumbs {
  visibility: hidden;
}

@media (min-width: 768px) {
  /*
   * SCOPED TO PAGES THAT HAVE A BAND. This variable is read by everything that
   * pins below the chrome — `scroll-padding-top` on `html` above, the table's
   * sentinel, the photo panel — and on a page with no band it was resolving to
   * 87px of clearance for furniture that is not on the screen. `:has()` makes
   * the declaration say what the number means: the stuck band's height, where a
   * stuck band exists. Elsewhere the `:root` default of `0px` stands (it is the
   * lower-specificity rule, so this wins wherever it matches), and every reader
   * falls back to the bare nav row — which is all the chrome those pages have.
   *
   * `:has()` is Baseline-available across every engine this storefront targets
   * (Chrome/Edge 105, Safari 15.4, Firefox 121). An engine without it simply
   * never matches this rule and keeps the `0px` default — which is the CORRECT
   * plainer thing on a band-less page, and on a category page costs only the
   * stuck band's own height of clearance on a fragment jump.
   *
   * IT MATCHES A BAND, NOT A DRIVEN ONE, and the mobile rung below matches a
   * DRIVEN one. The asymmetry is deliberate and is not tidied here: flow.css
   * already owns a `:has()` opt-out for the one page in the site whose band
   * mounts no driver, written against THIS selector, and re-scoping this line
   * would silently make that override dead code from another sheet. The mobile
   * rung is new, has no such history, and says the truer thing from the start.
   */
  :root:has(.sf-title-band) {
    --sf-band-stuck-h: calc(
      var(--sf-band-stuck-pad-top) + var(--sf-band-stuck-line) +
        var(--sf-band-stuck-pad-bottom) + 1px
    );
  }

  .sf-title-band[data-stuck="true"] .sf-title-band__row {
    align-items: flex-end;
    transform: translateY(
      calc(var(--sf-band-pad-bottom) - var(--sf-band-stuck-pad-bottom))
    );
  }

  .sf-title-band[data-stuck="true"] .sf-title-band__title {
    font-size: var(--sf-band-stuck-title);
    line-height: var(--sf-band-stuck-line);
    padding-top: max(
      0px,
      calc(
        var(--sf-band-title-box, var(--sf-band-title-line)) -
          var(--sf-band-stuck-box, var(--sf-band-stuck-line))
      )
    );
  }
}

/*
 * Zero-height, in NORMAL FLOW immediately below the band: the one point in the
 * layout that still says where the page has scrolled to once the band is pinned
 * and every point inside it has stopped moving.
 *
 * `scroll-margin-top` here is a READ-BACK CARRIER, not a clearance. Nothing ever
 * scrolls to this element — it is zero-height, `aria-hidden`, and carries no id
 * — so the property is chosen for one reason: an unregistered custom property
 * computes to its token stream, so `getComputedStyle` hands back the literal
 * `calc(...)` for `--sf-band-stuck-h`, while the same expression on a REAL
 * LENGTH property comes back resolved in pixels. This is where
 * `title-band-sticky.tsx` reads that resolved sum.
 *
 * Its value deliberately MIRRORS the cascade sum that `html`'s
 * `scroll-padding-top` already applies to every scroll port. That is not a
 * duplicate clearance: CSSOM-View ADDS a target's `scroll-margin` to the port's
 * `scroll-padding`, so a real scroll target declaring this sum would land twice
 * as far down (measured on `.sf-cat-embed`, 2026-08-26: 294px instead of 153px).
 * Real scroll targets keep only their own breathing room; scroll-padding owns
 * the chrome. This carrier mirrors the sum because the sum is what the driver
 * needs to read, and it is safe to mirror precisely because nothing lands here.
 */
.sf-title-band__sentinel {
  height: 0;
  scroll-margin-top: calc(var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px));
}

@media (max-width: 767.98px) {
  /* CategoryMobile.dc.html: the band tightens to 14px over / 18px under, the
   * crumb row drops a size, and the title's controls become one full-width
   * control stacked beneath it.
   *
   * THROUGH THE NAMED PAIR rather than as two literals, which renders the same
   * resting band to the pixel and is load-bearing for the stuck state: the
   * stuck row's `translateY` is the difference between the band's own bottom
   * padding and the stuck line's, and with the literals in place it was reading
   * the DESKTOP's 26 on a phone — 14px of travel where 6 was wanted, which put
   * the line 28px under the bar and left 4px beneath it instead of 20 and 12.
   * The band's total was right and its distribution was wrong, which is exactly
   * the failure a second copy of a number produces. */
  .sf-title-band {
    --sf-band-pad-top: 14px;
    --sf-band-pad-bottom: 18px;
    padding: var(--sf-band-pad-top) 0 var(--sf-band-pad-bottom);
  }

  .sf-crumbs__list,
  .sf-crumbs__list li {
    gap: 8px;
  }

  .sf-crumbs__list {
    font-size: var(--text-small);
  }

  .sf-crumbs svg {
    height: 15px;
    width: 15px;
  }

  .sf-title-band__row {
    /* `stretch`, not the desktop `flex-start`: stacked, the controls are the
     * band's full measure. */
    align-items: stretch;
    flex-direction: column;
    gap: 14px;
    margin-top: 10px;
  }

  .sf-title-band__title {
    font-size: var(--text-product);
    line-height: 1.12;
  }

  .sf-title-band__title--category {
    font-size: var(--text-section);
  }

  .sf-title-band__controls {
    padding-top: 0;
  }

  /* ------------------------------------ the band's stuck state, on a phone -- */

  /*
   * THE SAME MECHANISM, A DIFFERENT LINE (Ryan's ruling, 2026-08-27).
   *
   * The pin itself and the crumb row's disappearance are unscoped now and live
   * with the desktop recipe above; what is here is the phone's own stuck LINE
   * and the rung it adds up to. The invariant is the one the whole block is
   * built on and it is unchanged: THE BAND'S FLOW HEIGHT NEVER MOVES. The
   * band's resting mobile layout is untouched — 14/18, crumbs, a title that
   * wraps, a full-width control under it. Only what is on screen changes.
   *
   * GATED ON `[data-driven]`, unlike the desktop rung. A band with no driver
   * never pins (that is what `data-driven` means), so reserving its stuck height
   * would be clearance for furniture that never arrives — which is exactly the
   * bug flow.css's own `:has()` override exists to undo at the desktop cut. Said
   * correctly here, there is nothing for another sheet to undo.
   *
   * THE ROW BECOMES A LINE, and the collapse it costs is handed straight back
   * as padding. Stacked, the row is title + 14px + control (116.22px at 390 for
   * this page's two-line title); as a line it is the taller of the two, which is
   * the control (44px). The difference is reserved as the row's `padding-top`,
   * so the row's BORDER BOX is the same box it occupies at rest and the band's
   * height cannot move at the instant it pins. Above the glyphs, which is where
   * it is wanted, and the part of it above the band's visible top is simply
   * hidden behind the bar.
   *
   * BOTH BOXES ARE MEASURED, for the reason the desktop's two are: the resting
   * one is a wrap point (a title that takes one line at 430 takes two at 320),
   * and the stuck one moves with whichever control the page hung in the band.
   * `title-band-sticky.tsx` publishes them as `--sf-band-row-box` and
   * `--sf-band-stuck-row-box`, each read in a state the driver forces for the
   * read. The `var()` fallbacks are the declared pair, so a page whose driver
   * cannot measure a row is left with a plain — never a broken — band.
   *
   * `max(0px, …)`: the floor for a stuck line somehow TALLER than the resting
   * stack. It cannot happen while the stack contains the line's tallest member,
   * and a negative padding would be a parse error rather than a clamp.
   *
   * THE ROW IS THEN TRANSLATED down by the difference between the band's resting
   * bottom padding and the stuck line's, exactly as the desktop row is — a
   * transform does not lay out, so the space below the line shrinks without the
   * band's box changing size.
   */
  :root:has(.sf-title-band[data-driven]) {
    --sf-band-stuck-h: calc(
      var(--sf-band-stuck-pad-top-m) + var(--sf-band-stuck-line-m) +
        var(--sf-band-stuck-pad-bottom-m) + 1px
    );
  }

  .sf-title-band[data-stuck="true"] .sf-title-band__row {
    /* Back to a line, from the resting stack. `center`, not the desktop's
     * `flex-end`: the two things on this line are a 26px title box and a 44px
     * control, and a title sitting on the control's baseline reads as having
     * fallen off it. */
    align-items: center;
    flex-direction: row;
    padding-top: max(
      0px,
      calc(
        var(--sf-band-row-box, var(--sf-band-title-line)) -
          var(--sf-band-stuck-row-box, var(--sf-band-stuck-line-m))
      )
    );
    transform: translateY(
      calc(var(--sf-band-pad-bottom) - var(--sf-band-stuck-pad-bottom-m))
    );
  }

  /*
   * THE TITLE TRUNCATES, AND IT IS THE ONLY THING ON THE LINE THAT MAY.
   *
   * `min-width: 0` is what makes the other three declarations work at all: a
   * flex item's automatic minimum size is its CONTENT, so without it the title
   * refuses to shrink below its full measure, pushes the dropdown off the
   * gutter, and the ellipsis never appears no matter how narrow the phone.
   *
   * The controls beside it keep their intrinsic width — `.sf-title-band__controls`
   * is already `flex: none`, and shrink-wrapping it is what returns the quiet
   * dropdown to its natural size from the full-width one the resting stack
   * gives it.
   */
  .sf-title-band[data-stuck="true"] .sf-title-band__title {
    font-size: var(--sf-band-stuck-title-m);
    line-height: var(--sf-band-stuck-title-line-m);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ------------------------------------------------------ the z-index ladder -- */

/*
 * ─── WHAT PAINTS OVER WHAT, ONCE (Ryan's DD-E, 2026-08-28) ─────────────────
 *
 * THE RULING: "popups sit above their LOCAL content but BELOW pinned/sticky
 * chrome — a page-flow popup never covers navigation." He gave it after seeing
 * the reviews sort-by's open panel painted on top of the below-the-fold nav,
 * and he ruled it a POLICY question rather than a surface one: "fix it at the
 * policy level, not with a per-surface z-index bump — this is the same disease"
 * as the stacking-context workaround the photo module was carrying.
 *
 * So the rungs are written here, in the sheet every page loads, and nothing
 * else on the site is allowed to invent one:
 *
 *   0–3   PAGE CONTENT and its own local layers. The category hero's copy
 *         column (2), the comparison table's pinned head (2) and its label cell
 *         (3), an orb panel (3), a table note chip (1).
 *   5     POPUPS. Every panel this control opens, in either skin —
 *         the two skins the control draws. Above all of
 *         the above; below all of the below.
 *   10    IN-PAGE PINNED BANDS. The category page's AI-embed stack and the
 *         product page's below-the-fold nav.
 *   20    THE TITLE BAND, which parks above the in-page bands.
 *   30    THE BUY BAR, a fixed bar that covers the bands as it passes them.
 *   40    THE SITE HEADER, the top pinned row, above everything.
 *
 * TWO THINGS THIS LADDER DOES NOT DECIDE, and both are correct:
 *
 *   A POPUP INSIDE PINNED CHROME RIDES WITH IT. The title band is a stacking
 *   context at 20, so the dropdown it carries paints at 20 with the band rather
 *   than at 5 against the page. That is the band's popup covering the page,
 *   which is what a control in a pinned bar is for — not a page-flow popup
 *   covering navigation, which is what the ruling forbids.
 *
 *   MODALS AND THE LIGHTBOX are not on this ladder. They are dialog elements
 *   in the top layer, which is above every z-index there is by construction.
 */

/* -------------------------------------------------- quiet dropdown -- */

/*
 * The QUIET DROPDOWN (docs/storefront-tokens-and-patterns.md §4): a white
 * bordered control with a chevron, for a choice we do not push. The
 * higher-margin default is preselected and shown; the alternatives and the
 * guide that compares them live INSIDE the menu, so the page never asks the
 * reader to make the choice before it has made its case.
 *
 * It lives beside the title band because that is where it hangs — it is what a
 * page passes as the band's right-aligned control — and because the two share
 * the band's `line-height: normal`. The menu is not drawn on the desktop board
 * (only on CategoryMobile, where it is drawn open and full width); the desktop
 * placement below is derived from it.
 */

.sf-quiet-select {
  display: inline-block;
  position: relative;
}

/*
 * ─── THE RAISE IS SCOPED TO THE OPEN STATE (DD-UNIFY, 2026-08-28) ───────────
 *
 * WHAT THE NUMBER IS: THE POPUP RUNG — see the ladder at the top of this
 * sheet. Above the local content a panel has to cover (the category hero's copy
 * column at 2, the comparison table's own pinned furniture at 2 and 3) and
 * BELOW every pinned band on the site, which is Ryan's DD-E ruling: "popups sit
 * above their LOCAL content but BELOW pinned/sticky chrome — a page-flow popup
 * never covers navigation."
 *
 * IT WAS 10 UNTIL DD-E, which is the rung the IN-PAGE PINNED BANDS take, and
 * that is exactly the bug Ryan saw: the reviews sort-by's open panel painted on
 * top of the below-the-fold navigation. A number a popup shares with the chrome
 * it must not cover is a coin toss decided by document order.
 *
 * WHY IT MOVED: every one of those reasons is about the open PANEL. A CLOSED
 * control has nothing to be above — and being above things anyway is what made
 * this control paint over the photo module's open suggestion list, two sections
 * down a page it knows nothing about. That module answered by boxing this
 * control in a stacking context of its own (`.sf-ph__filter { position:
 * relative; z-index: 1 }`) so the 10 was scoped INSIDE it — a consumer fighting
 * a number the shared control should never have been asserting. Ryan's
 * unification ruling names that shape directly: "no z-index that consumers must
 * fight."
 *
 * `quiet-select.tsx` writes `data-open` ONLY while the menu is up, so the
 * closed markup is the closed markup every recorded gate holds, to the
 * attribute.
 */
.sf-quiet-select[data-open="true"] {
  z-index: 5;
}

.sf-quiet-select__control {
  /*
   * THE CONTROL'S PADDING-LEFT IS A NAMED NUMBER (Ryan's MC1, 2026-08-29),
   * because the FX1 reserve's correction is computed FROM it and the two cuts
   * do not agree about it. See `.sf-quiet-select__reserve` at the foot of this
   * block for the arithmetic; the short version is that the reserve owes
   * `padding-left + 54` and shipped as a flat 72, which is right at 18px and
   * two pixels short at the phone cut's 16px — enough to wrap the widest row
   * of the one panel narrow enough to care.
   */
  --sf-qs-pad-left: 18px;
  align-items: center;
  appearance: none;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-body);
  font-weight: 600;
  gap: 26px;
  /* Asymmetric by design on the board: the chevron sits closer to the edge than
   * the label does. */
  padding: 11px 16px 11px var(--sf-qs-pad-left);
}


/*
 * THE VALUE, RESERVING THE WIDEST LABEL'S WIDTH (Ryan's CAT-DD (b),
 * 2026-08-28: "the width should be the longest option's, like a real OS
 * dropdown; no resize when the value changes").
 *
 * ONE GRID CELL, EVERY LABEL IN IT. The auto-sized column takes the max-content
 * of its widest item, so the cell is the widest label and the hidden ones are
 * what make it so — `visibility: hidden` keeps their boxes in the layout, which
 * is the whole mechanism; `display: none` would measure nothing.
 *
 * MEASURED on the category page's series switch: THOR is 43.484px and APOLLO is
 * 61.031px in this type, so the control reserves 61.031 and grows 17.547px. The
 * label stays against the left padding and the CHEVRON moves out, which is what
 * a native select does with a short value in a long list.
 *
 * ONLY WHERE THE ISLAND ASKS FOR IT. `quiet-select.tsx` renders this stack for
 * `steadyWidth` callers only; every other consumer still gets the bare label as
 * a text run and this rule matches nothing there, so their pixels are the ones
 * their gates recorded. DD-UNIFY is where it becomes the rule.
 */
.sf-quiet-select__value {
  display: grid;
  /*
   * ─── MC1 (a): THE VALUE SITS AGAINST THE LEFT PADDING (Ryan, 2026-08-29) ───
   *
   * "The closed value THOR renders shifted right — as if tabbed over."
   *
   * The paragraph above already CLAIMED this: "the label stays against the left
   * padding and the CHEVRON moves out, which is what a native select does with
   * a short value in a long list." It did not. A `<button>` carries the UA's
   * `text-align: center`, every child of this grid STRETCHES to the reserved
   * cell, and the visible label was therefore centred inside a cell it does not
   * fill — so the phantom indent is exactly half the difference between the
   * reserve and the word.
   *
   * IT GREW WITH FX1 AND THAT IS WHY IT WAS REPORTED NOW. While the cell was
   * the widest LABEL (61.03 for APOLLO) the indent on THOR was ~9px and read as
   * a rounding. Once the cell also had to cover the guide row it became 100.92,
   * and MEASURED at 390 the ink started 29.44px inside the padding edge — a tab
   * stop, which is what Ryan called it.
   *
   * ON THE STACK RATHER THAN THE BUTTON, deliberately: this is the only place a
   * child is stretched to a box wider than its words. Every non-`steadyWidth`
   * caller draws the label as a shrink-wrapped text run or a shrink-wrapped
   * flex item, where the UA centring has nothing to centre inside and changing
   * it would move pixels their gates recorded for no reason.
   */
  text-align: left;
}

.sf-quiet-select__value > * {
  grid-area: 1 / 1;
}

.sf-quiet-select__value-ghost {
  visibility: hidden;
}

/* Open, the control and the menu are one shape: the seam between them is a
 * single hairline, drawn by the menu's top border ON TOP of the control's
 * bottom border (see the menu's own rule). */
.sf-quiet-select__control[aria-expanded="true"] {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* `display: flex` so the span's box is the glyph's box: an inline wrapper is a
 * line box two or three pixels taller than the chevron, and centring THAT in the
 * control leaves the chevron off centre. */
.sf-quiet-select__chevron {
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  flex: none;
}

/*
 * ─── CAT-DD (c): THE PANEL IS A CLOSED BOX AGAIN (Ryan, 2026-08-28) ─────────
 *
 * `border-top: none` IS RETIRED BY NAME, and the reading that justified it —
 * "the control and the menu are one shape, so the seam between them is the
 * menu's MISSING top border" — was only ever true of the stretch of panel that
 * sits UNDER the control. This panel is `min-width: 100%` and anchored to the
 * control's right edge, so whenever an entry is wider than the closed value it
 * hangs out to the LEFT with nothing above it: measured on the category page's
 * series switch, the panel is 174.92px against a 117.48px control, so 57.44px
 * of it stood open at the top, its left border running up to a corner that was
 * not there. Ryan saw it as a missing top border because that is exactly what
 * it was. (CAT-DD (b) narrows the overhang to 39.89px by widening the control;
 * it cannot close it, because the guide row is wider than any label.)
 *
 * THE BORDER IS BACK AND THE SEAM IS STILL ONE HAIRLINE, which is what
 * `margin-top: -1px` buys: the panel's top border lands exactly ON the control's
 * bottom border rather than under it, so the two coincide instead of stacking
 * into a 2px rule. Everything else is where it was to the pixel — the first
 * entry still begins at the control's bottom edge, and the panel's bottom edge
 * has not moved — so the surfaces where the panel and its control are the same
 * width (the mobile cut, where both are full width) are byte-identical, and the
 * only new ink on any page is the cap over an overhang that had none.
 */
.sf-quiet-select__menu {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: 0 0 var(--radius-control) var(--radius-control);
  box-shadow: 0 10px 24px rgba(23, 24, 26, 0.1);
  margin-top: -1px;
  min-width: 100%;
  position: absolute;
  /* Anchored to the control's RIGHT edge because the band's controls are
   * right-aligned against the page gutter: a menu wider than its control has to
   * grow inwards or it grows off the page. */
  right: 0;
  top: 100%;
  white-space: nowrap;
}

.sf-quiet-select__item {
  align-items: center;
  appearance: none;
  background: none;
  border: none;
  border-bottom: var(--border-hairline);
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  display: flex;
  font-family: inherit;
  font-size: var(--text-support);
  font-weight: 600;
  gap: 8px;
  padding: 12px 16px;
  text-align: left;
  text-decoration: none;
  width: 100%;
}

.sf-quiet-select__item:last-child {
  border-bottom: none;
}

/* The guide link is the quiet one of the pair — it explains the choice rather
 * than making it. */
.sf-quiet-select__item--guide {
  color: var(--brand-color-muted, #6c6a62);
  font-weight: 400;
}

.sf-quiet-select__item:hover {
  background: var(--brand-color-paper, #fbfbfa);
}

/*
 * ─── THE MENU BOX'S THREE CONFIGURED VARIANTS (DD-UNIFY, 2026-08-28) ────────
 *
 * The unification ruling asks for one control whose differences are
 * CONFIGURATION rather than copies. These are the drawn half of that: three
 * modifiers the component adds on request, and no surface that does not ask for
 * one carries a single changed declaration.
 *
 * NONE OF THEM IS ON A SURFACE TODAY. Every menu-mode caller wants the default
 * — right-anchored, content-wide, no caption — which is why the base rules
 * above are untouched and `tools/ddgate.mjs` still holds the drawing to the
 * pixel. They exist so the next surface configures rather than forks.
 */

/* Pinned to the control's LEFT edge, for a control that is not right-aligned
 * against the page gutter. */
.sf-quiet-select__menu--left {
  left: 0;
  right: auto;
}

/*
 * ─── `width="field"`: THE CONTROL FILLS ITS COLUMN (Ryan’s DD-A, 2026-08-28) ─
 *
 * The shared configuration row is a two-column grid whose control column takes
 * whatever the container has left, so a shrink-wrapped control would sit in a
 * cell it does not fill and the rows’ controls would not line up under each
 * other. Asked for, the control spans its cell and the chevron goes to the far
 * edge — the same arrangement the phone cut has always given this control —
 * and the panel is exactly the control’s width with its rows free to wrap.
 *
 * OPT-IN, so every surface that draws this control OUTSIDE a grid keeps the box
 * its own gate recorded: the category page’s series switch above all, whose
 * drawing `tools/ddgate.mjs` holds to the pixel.
 */
.sf-quiet-select--field {
  display: block;
}

.sf-quiet-select--field .sf-quiet-select__control {
  display: flex;
  justify-content: space-between;
  width: 100%;
}

/* Exactly the control's width, rows wrapping — against the default, which is
 * at least the control's width and as much more as the widest row needs. */
.sf-quiet-select__menu--field {
  left: 0;
  white-space: normal;
}

/* A caption over the entries, in the sheet's mono micro-eyebrow, with the
 * hairline that stops it reading as an entry a reader could walk onto. */
.sf-quiet-select__eyebrow {
  border-bottom: var(--border-hairline);
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin: 0;
  padding: 10px 16px 8px;
}

/* A figure against the row's right edge. `margin-left: auto` rather than
 * `space-between` on the row, because the row is also the box a glyph and a
 * label sit against on the left and those two keep the 8px gap. */
.sf-quiet-select__item-count {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin-left: auto;
  padding-left: 10px;
}

/*
 * ─── FX1: THE RESERVE COVERS THE WHOLE LIST, NOT JUST THE LABELS (Ryan,
 *     2026-08-28) ──────────────────────────────────────────────────────────
 *
 * CAT-DD (b) reserved the widest OPTION LABEL and Ryan's screenshot showed why
 * that is only half the rule: the open panel also draws a GUIDE row and can
 * draw a CAPTION, and on the category page's series switch the guide ("THOR vs
 * APOLLO", with its glyph) is the widest entry in the box. MEASURED before this
 * rule: control 135.03 against a 174.92 panel — 39.89px of panel hanging out
 * past its own control, which is the broken seam in the screenshot. A real OS
 * dropdown's closed control fits the widest entry of the ENTIRE list.
 *
 * THE GHOSTS ARE THE PANEL'S OWN ROWS. `quiet-select.tsx` renders each one with
 * the row's real class — `__item`, `__item--guide`, `__eyebrow` — so the type,
 * the 8px gap and the 16px side paddings are MEASURED off the same declarations
 * the panel draws with rather than restated here, where a later change to one
 * of them would silently stop being copied.
 *
 * ─── THE ONE NUMBER, AND WHERE IT COMES FROM ────────────────────────────────
 *
 * A ghost sits in the VALUE CELL, and the value cell is not the panel: the two
 * boxes wrap their ink in different amounts of chrome. Two steps, both
 * arithmetic.
 *
 * FIRST, WHAT THE CELL OWES. Call a row's ink — its glyph, its words, its
 * count — `ink`:
 *
 *     the closed control, around the value cell   1 + PL + 26 + 12 + 16 + 1
 *       (border, padding-left, the gap, the 12px chevron, padding-right, border)
 *     the open panel, around a row's ink              1 + 16 + 16 + 1       = 34
 *       (border, the row's padding-left, its padding-right, border)
 *
 * `control = cell + (PL + 56)` and `panel = ink + 34`, so they are flush
 * exactly when the cell reserves `ink − PL − 22`.
 *
 * SECOND, WHAT THE GHOST ACTUALLY MEASURES. It wears the row's own class, so it
 * keeps the row's two 16px side paddings: its contribution is `ink + 32`, not
 * `ink`. The correction is therefore the whole distance between what it
 * measures and what the cell owes:
 *
 *     (ink + 32) − (ink − PL − 22) = PL + 54
 *
 * That is the negative margin. A grid item's margins count toward its track's
 * contribution, so that right margin makes the ghost reserve precisely the
 * width its row will need and not a pixel more. (MEASURED on the way here: at
 * −40 — the first number, which corrected only the first step — the category
 * switch came out 206.92 against a 174.92 panel, over by exactly the 32px of
 * row padding this second step accounts for.)
 *
 * ─── MC1 (b): `PL` IS NOT A CONSTANT, AND THAT WAS THE BUG (Ryan, 2026-08-29)
 *
 * FX1 wrote the correction as a flat 72, which is `PL + 54` evaluated at the
 * DESKTOP control's 18px padding-left. The phone cut's control is `12px 16px`,
 * so there `PL + 54` is 70 and the shipped 72 reserved TWO PIXELS TOO FEW.
 *
 * TWO PIXELS IS ENOUGH, and the state that proves it is the one Ryan
 * screenshotted — the category band STUCK on a phone. At rest the phone's
 * control is `width: 100%` and the reserve buys nothing; stuck, the band's
 * controls box is `flex: none`, the control shrink-wraps its reserve again, and
 * the panel is `min-width: 100%` of it. MEASURED at 390 before this fix:
 * control 172.92, the guide row's ink 140.92 in a row offering 172.92 − 34 =
 * 138.92 — so "THOR vs APOLLO" wrapped to a second line by exactly the 2px.
 *
 * SO THE NUMBER READS THE PADDING IT IS DERIVED FROM. `--sf-qs-pad-left` is
 * declared on the control (which is this ghost's ancestor) and consumed by the
 * control's own `padding` shorthand, so the two cannot disagree the way a
 * literal and a media query can. `tools/ddgate.mjs` asserts BOTH halves at both
 * widths and in both band states: panel width == control width, AND every row's
 * content fits its line without wrapping — the second is new, because the first
 * was true throughout the defect (172.92 == 172.92) and could not see it.
 *
 * IT IS A NUMBER IN TWO PLACES AND A GATE IS WHAT KEEPS THEM EQUAL — the same
 * device `--sf-hero-review-lift` and `EXPAND_MS` already use. `tools/ddgate.mjs`
 * asserts `panel width == control width` on EVERY menu-mode caller it can find
 * by walking the page, so a change to the control's padding or the chevron's
 * size that this constant no longer matches fails loudly rather than drifting.
 *
 * OVER-RESERVING WOULD BE SAFE AND IS STILL WRONG: the panel is `min-width:
 * 100%`, so a control that reserved too much would still meet it flush — and
 * would be a fat control nobody asked for. The number is exact so the box is.
 *
 * WHY IT ADDS NO HEIGHT: zero block size, no block padding and no border, so
 * the cell's height stays the visible label's line box. The row rules the ghost
 * inherits set all three, which is why all three are named here — and
 * `overflow: hidden` is load-bearing rather than tidy, because a grid item's
 * `min-block-size: auto` would otherwise floor the box at its content and the
 * zero would not stick.
 *
 * THIS BLOCK SITS AFTER THE ROW RULES ON PURPOSE. Every declaration here has a
 * counterpart in `__item` at the SAME specificity — `border`, `padding`,
 * `width` — so the cascade is decided by sheet order, and the ghost has to be
 * the one that wins. It was written above them first and measured correctly
 * anyway, by three separate accidents (the percentage width being indefinite
 * during track sizing, `:last-child` happening to cancel the border, the block
 * padding not mattering while the row was 24px tall). Order is what makes it a
 * rule instead of a coincidence.
 */
.sf-quiet-select__reserve {
  block-size: 0;
  border: none;
  margin-right: calc(-54px - var(--sf-qs-pad-left, 18px));
  overflow: hidden;
  padding-block: 0;
  visibility: hidden;
  /* `.sf-quiet-select__item` is `width: 100%` — it fills the panel. In a grid
   * cell that is a percentage of the track being sized, which is not a width at
   * all: what this ghost is FOR is its content's width. */
  width: max-content;
}


/* -------------------------------------------------- the field's clear -- */

/*
 * ─── THE X INSIDE A SEARCH FIELD (Ryan's DD-C, 2026-08-28) ─────────────────
 *
 * "Right-aligned INSIDE the field; reserved right padding so text can never run
 * under it; renders only when the field has text; clears query AND results;
 * a 44px touch target at the phone cut; Escape in the field also clears."
 *
 * IT REPLACES THE BROWSER'S OWN. Every field this lands on was
 * `type="search"`, which draws a native × — a different glyph per browser,
 * absent in Firefox unless the field has focus, about 14px of hit target, and
 * unstylable, which is four reasons and the ruling only needed one.
 *
 * IT ALSO REPLACES THE "CLEAR" LINKS those sections carried, RETIRED BY NAME:
 * `.sf-ph__clear` (the photo module's chip-row clear) and
 * `.sf-pdp-rev__clear` (the reviews search's). The affordance did not
 * disappear — it moved into the field it always acted on, which is where a
 * reader looks for it.
 *
 * ─── THE THREE THINGS THAT MAKE IT WORK, AND WHERE EACH LIVES ─────────────
 *
 *   THE RESERVE. This button is absolutely positioned against the field's right
 *   edge, so it takes no width in the row — which means the field's own input
 *   has to leave room for it. Each field's sheet adds that padding, because the
 *   fields differ (13/16 on the photo module, 14/16 on the two PDP sections)
 *   and a shared number would be wrong on all three.
 *
 *   THE TOUCH TARGET. 44px at the phone cut is the ruled minimum, and it is
 *   bought with padding rather than with a box: a 44px SQUARE would be wider
 *   than the field's own right padding and would overhang the text. The glyph
 *   stays where it is drawn and the pressable area grows around it.
 *
 *   THE CENTRING. `top: 50%` with a half-height translate rather than
 *   `inset-block: 0`, so the button is vertically centred on a field of ANY
 *   height without knowing what that height is.
 */

.sf-field-clear {
  align-items: center;
  appearance: none;
  background: none;
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  /* The board's × is set larger than the type it sits beside — it is a target
   * as much as a glyph. */
  font-size: 20px;
  justify-content: center;
  line-height: 1;
  padding: 6px;
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
}

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

/* THE 44px TARGET (ruled). Bought with padding on the same glyph, so nothing
 * moves and nothing overhangs: the button's box grows around a mark that stays
 * where the desktop drew it. */
@media (max-width: 767.98px) {
  .sf-field-clear {
    min-height: 44px;
    min-width: 44px;
    padding: 0;
    right: 4px;
  }
}


/* ------------------------------------------- the suggestions panel (skin b) -- */

/*
 * ─── THE FIELD-ANCHORED FLUSH-SEAM PANEL ────────────────────────────────────
 *
 * The site's OTHER dropdown drawing, and the second half of the DD-UNIFY
 * ruling: one control, two skins, and this is the one a TEXT FIELD wears.
 *
 * WHAT IT IS. The list hangs at the field's BORDER box (`left`/`right: -1px`,
 * because percentages on an absolutely positioned child resolve against the
 * PADDING box) with no top border of its own and the extra pixel of `top` that
 * carries it past the field's bottom border — so the field and the list share
 * ONE hairline and read as one control. The field's own rule turns off its two
 * bottom radii while the list is up; that half stays with the field, because it
 * is the field's box.
 *
 * WHERE IT CAME FROM, and why it is verbatim. This is the drawing
 * `.sf-cat-embed__menu` and `.sf-pdp-ans__menu` already carried — two hand
 * copies of one thing, identical declaration for declaration. Both are RETIRED
 * BY NAME into these rules, unchanged to the pixel, so `tools/aigate.mjs`'s
 * recorded ask crops come back where they were.
 *
 * WHAT CONVERGED ONTO IT. The photo module's facet dropdown was the site's
 * THIRD copy and the one that had drifted into a different drawing — a floating
 * card with all four borders, its own radius, 4px row gaps and 10px of padding.
 * Ryan's ruling takes it onto this skin while keeping its RICH ROWS (counts,
 * glyphs, the matched run in bold, the two-line free-text row, the split
 * eyebrow) as configuration. That is a real, deliberate pixel change on the
 * product page and nowhere else; `tools/phgate.mjs` re-records it and
 * `tools/phab.mjs` proves the category and home levels — which draw no facet
 * dropdown at all — did not move.
 *
 * THE ANCHOR IS THE SURFACE'S OWN FIELD BOX, never a wrapper this control
 * introduces: `.sf-suggest` is `display: contents` and generates nothing.
 */

.sf-suggest {
  display: contents;
}

.sf-suggest__menu {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-bottom-left-radius: var(--radius-control);
  border-bottom-right-radius: var(--radius-control);
  border-top: none;
  box-shadow: 0 12px 28px rgba(23, 24, 26, 0.1);
  /* -1px on both sides so the panel's border sits ON the field's rather than
   * beside it — the two read as one box. */
  left: -1px;
  position: absolute;
  right: -1px;
  top: calc(100% + 1px);
  /*
   * THE POPUP RUNG — the same one the menu box takes, and for the same reason
   * (Ryan's DD-E, 2026-08-28: a popup is above its local content and below every
   * pinned band). It was 30, which is the BUY BAR's rung: a suggestion list
   * opened under a product page's fixed buy bar painted straight through it.
   *
   * IT IS STILL ABOVE THE CLOSED CONTROL UNDER IT. The photo module's
   * configuration row holds a quiet dropdown BELOW this list and later in the
   * DOM, and that control is `z-index: auto` while it is shut — the raise is
   * scoped to its open state — so the list a reader just opened wins. The two
   * cannot be open at once: opening either presses outside the other.
   */
  z-index: 5;
}

/*
 * THE LIST IS WHAT SCROLLS, not the panel: the eyebrow is a heading for the
 * whole list and has to stay put while the reader walks it.
 *
 * `position: relative` because this is an `overflow != visible` container and
 * the playbook's sr-span rule applies to every one of them — an absolutely
 * positioned descendant whose containing block fell through to the page would
 * extend the DOCUMENT's scrollable overflow rather than this box's.
 */
.sf-suggest__list {
  list-style: none;
  margin: 0;
  max-height: 320px;
  overflow-y: auto;
  padding: 0;
  position: relative;
}

/*
 * THE CAPTION. "POPULAR QUESTIONS — OR ASK YOUR OWN": the dropdown read as a
 * mandatory select without it (Ryan, 2026-08-26 — he opened it and felt he had
 * to pick one of the questions). The mono micro-eyebrow the design system uses
 * everywhere it names a group of things, and a hairline under it, which is what
 * stops the caption reading as an entry a reader could arrow onto.
 */
.sf-suggest__eyebrow {
  border-bottom: var(--border-hairline);
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin: 0;
  padding: 10px 16px 8px;
}

/* THE SPLIT CAPTION — what was typed on the left, a column header for the
 * counts on the right. The board writes the typed text back in CAPITALS;
 * doing that in paint rather than in code keeps the data the reader's own
 * keystrokes. */
.sf-suggest__eyebrow--split {
  display: flex;
  justify-content: space-between;
  text-transform: uppercase;
}

.sf-suggest__option {
  border-bottom: var(--border-hairline);
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  font-size: var(--text-small);
  padding: 9px 16px;
}

.sf-suggest__option:last-child {
  border-bottom: none;
}

/*
 * The ACTIVE entry, which is not the same thing as the focused one: focus stays
 * in the input the whole time and this is what `aria-activedescendant` names,
 * so it has to be drawn or a keyboard reader is walking an invisible list.
 * Hover lands on the same treatment because it means the same thing.
 */
.sf-suggest__option--on,
.sf-suggest__option:hover {
  background: var(--brand-color-well, #f1f0ec);
  color: var(--brand-color-ink, #17181a);
}

/*
 * A ROW WITH FURNITURE. Only rows that carry a glyph, a count or a second line
 * become flex rows — a row that is only words stays a block with a text node in
 * it, which is what the two ask fields' recorded crops hold and what keeps a
 * plain suggestion list one text-shaping run per entry (playbook §5).
 */
.sf-suggest__option--rich {
  align-items: center;
  display: flex;
  justify-content: space-between;
}

.sf-suggest__option-label {
  align-items: center;
  display: flex;
  min-width: 0;
}

.sf-suggest__option-mark {
  color: var(--brand-color-muted, #6c6a62);
  display: inline-flex;
  flex: none;
  margin-right: 8px;
}

.sf-suggest__option-count {
  color: var(--brand-color-muted, #6c6a62);
  flex: none;
  font-size: var(--text-small);
  padding-left: 10px;
}

.sf-suggest__option-stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

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

/*
 * THE FREE-TEXT ROW — "search all photos for X". Two lines with the glyph
 * aligned to the first of them, and a magnifier rather than the tag glyph,
 * which is the board saying "this one is not a filter".
 *
 * IT NEEDS NO RULE OF ITS OWN ANY MORE. On the card drawing it had to draw the
 * separating hairline itself (a `border-top`, with the radius and the tint
 * undone around it, because the rows there were separate tinted boxes with a
 * 4px gap). In this skin EVERY row is already divided from the last by the same
 * hairline, so what is left is the two-line alignment.
 */
.sf-suggest__option--free {
  align-items: flex-start;
  display: flex;
}

.sf-suggest__option--free .sf-suggest__option-mark {
  color: var(--brand-color-ink, #17181a);
  margin-top: 2px;
}

/*
 * GROUPS — a heading over a run of rows. DRAWN NOWHERE YET: no surface groups
 * its suggestions today, and this is here because the unification ruling names
 * group headers as one of the configured extras. The heading is `aria-hidden`
 * because the group's own `aria-label` already says it.
 */
.sf-suggest__group {
  list-style: none;
}

.sf-suggest__group-label {
  border-bottom: var(--border-hairline);
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  margin: 0;
  padding: 10px 16px 8px;
}

.sf-suggest__group-list {
  list-style: none;
  margin: 0;
  padding: 0;
}


@media (max-width: 767.98px) {
  .sf-quiet-select {
    display: block;
  }

  .sf-quiet-select__control {
    /* SYMMETRIC AT THIS CUT, where the desktop's is asymmetric — and the
     * reserve's correction is derived from this number, so it is written
     * through the same custom property rather than as a literal inside the
     * shorthand (Ryan's MC1, 2026-08-29; the derivation is on
     * `.sf-quiet-select__reserve`). */
    --sf-qs-pad-left: 16px;
    display: flex;
    font-size: var(--text-support);
    justify-content: space-between;
    padding: 12px 16px 12px var(--sf-qs-pad-left);
    width: 100%;
  }

  .sf-quiet-select__menu {
    left: 0;
    white-space: normal;
  }
}

/* ---------------------------------------------------------------- footer -- */

/*
 * ─── THE FOOTER OWNS ITS OWN SPACE ABOVE (Ryan's ruling A3, 2026-08-27) ─────
 *
 * "The footer should own its own space above, so no page ever sits against it
 * and no per-page edit is ever needed."
 *
 * WHAT IT FIXES, MEASURED before the rule landed: the product page's last
 * control — the answers pager's buttons — sat 0px from the blue, and the
 * homepage's live-orders canvas sat 1px from it. Two other pages had 48px and
 * two had none, because each had been given, or not given, its own bottom
 * padding on the day someone noticed. That is the shape of a rule that lives in
 * the wrong place: the footer knows it wants air above it, and no page can know
 * whether the footer is next.
 *
 * MARGIN, NOT PADDING. Padding would grow the BLUE, which is the footer's own
 * drawn box (48/40 inside `__top`, from Footer.dc.html); the space Ryan is
 * asking for is page ground between the content and the band. `.shell` is a
 * flex column, so this margin neither collapses with the page's own bottom
 * margin nor is collapsed through by it — the distance is exactly this number
 * plus whatever the page's last section already carries, which is what makes
 * the floor a floor.
 *
 * THE PLATFORM'S SECTION RHYTHM, because that is what the footer is from the
 * page's point of view: the next section. 80px is the number every below-fold
 * section on the product page is separated by, and `--space-section-mobile` is
 * the phone step the other mobile boards draw.
 *
 * TWO PER-PAGE PADDINGS WENT WITH IT, and only those two: `.sf-flow` and
 * `.sf-cf` each carried `padding-bottom: 48px` whose comment named this exact
 * job ("the board's wrapper carries 48px under the grid before the page ends").
 * Audited, not guessed: those were the only two `padding-bottom` declarations
 * on any page WRAPPER in the storefront sheets. What remains under other pages
 * — the photo module's 48, the legal pages' section padding — is each block's
 * own symmetric rhythm rather than footer clearance, and belongs to sheets this
 * pass does not own.
 */
.sf-footer {
  background: var(--brand-color-nav, #192950);
  color: var(--brand-color-surface, #ffffff);
  /* As in the header: the board declares no leading, so every line box is
   * `normal` and only the rules that name a line-height below carry one. */
  line-height: normal;
  margin-top: 80px;
}

@media (max-width: 767.98px) {
  .sf-footer {
    margin-top: var(--space-section-mobile);
  }
}

.sf-footer a {
  color: var(--brand-color-nav-text, #d9e8ff);
  text-decoration: none;
}

.sf-footer a:hover {
  color: var(--brand-color-surface, #ffffff);
  text-decoration: underline;
}


/* The link grid and the contact rail, side by side. */
.sf-footer__top {
  display: flex;
  gap: 36px;
  padding: 48px 0 40px;
}

.sf-footer__columns {
  display: grid;
  flex: 1;
  gap: 44px 28px;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  min-width: 0;
}

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

/*
 * `content-box` is the board's own geometry, not a stylistic choice: the board
 * gives the rail a 268px width OUTSIDE its 32px indent and 1px rule, which the
 * platform's global `border-box` would otherwise eat into — moving the whole
 * link grid 33px right of where it is drawn.
 */
.sf-footer__rail {
  border-left: 1px solid var(--brand-color-nav-raised, #263864);
  box-sizing: content-box;
  display: flex;
  flex: none;
  flex-direction: column;
  gap: 12px;
  padding-left: 32px;
  width: 268px;
}

.sf-footer__rail-logo {
  display: block;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.35));
}

.sf-footer__rail-logo img {
  display: block;
  height: auto;
  width: 64px;
}

.sf-footer__rail-blurb {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-size: var(--text-small);
  line-height: 1.55;
}

/*
 * The white has to be re-stated here: `.sf-footer a` is a class-plus-type
 * selector, so it outranks `.sf-contact--phone` and would repaint the number in
 * the quiet nav tint the rest of the footer's links wear.
 */
.sf-footer__rail .sf-contact--phone {
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-emphasis);
}

.sf-footer__rail .sf-contact--chat {
  font-size: var(--text-small);
}

/* Pulled up under the number and indented past its dot and glyph, so the hours
 * read as a continuation of that line rather than a new one. */
.sf-footer__rail-hours {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-size: var(--text-caption);
  margin: -6px 0 0 23px;
}

.sf-footer__rail-emails,
.sf-footer__rail-address {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-size: var(--text-caption);
  font-style: normal;
  line-height: 1.7;
}

/* Both blocks are line-per-item stacks, not link rows. */
.sf-footer__rail-emails li,
.sf-footer__rail-address > span {
  display: block;
}

.sf-footer__rail-address-links a {
  text-decoration: underline;
}

@media (max-width: 833.98px) {
  /* The rail rolls UNDER the grid as a full-width strip rather than being cut
   * — the board's own ruling for its narrow-width right rails. */
  .sf-footer__top {
    flex-direction: column;
  }

  .sf-footer__rail {
    border-left: 0;
    border-top: 1px solid var(--brand-color-nav-raised, #263864);
    padding-left: 0;
    padding-top: 28px;
    width: auto;
  }

  .sf-footer__columns {
    gap: 32px 24px;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 767.98px) {
  .sf-footer__top {
    padding: 32px 0 28px;
  }

  .sf-footer__columns {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/*
 * These two are real headings, so globals.css has already given them the
 * heading face, a 500 weight, a tight leading, negative tracking and
 * `text-wrap: balance`. The board draws both as plain divs in the body face at
 * body weight, so each of those five has to be named back.
 */
.sf-footer__heading {
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  font-weight: 400;
  letter-spacing: 0.12em;
  line-height: normal;
  margin: 0 0 12px;
  text-wrap: wrap;
}

.sf-footer__group-label {
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-caption);
  font-weight: 600;
  letter-spacing: 0;
  line-height: normal;
  margin: 0;
  padding: 12px 0 3px;
  text-wrap: wrap;
}

.sf-footer__group:first-child .sf-footer__group-label {
  padding-top: 0;
}

.sf-footer__link {
  display: block;
  font-size: var(--text-caption);
  line-height: 1.5;
  padding: 4px 0;
}

.sf-footer__link-label {
  color: var(--brand-color-surface, #ffffff);
  font-weight: 600;
}

/* Sub-label under a brand name, e.g. what family of system it is. */
.sf-footer__link-note {
  color: var(--brand-color-nav-text, #d9e8ff);
  display: block;
  font-size: var(--text-micro);
  padding-bottom: 2px;
}

.sf-footer__fine {
  align-items: center;
  border-top: 1px solid var(--brand-color-nav-raised, #263864);
  color: var(--brand-color-nav-text, #d9e8ff);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-caption);
  gap: 18px;
  padding: 16px 0;
}

.sf-footer__fine-links {
  flex: 1;
  text-align: center;
}

.sf-footer__fine-links a {
  text-decoration: underline;
}

.sf-footer__fine-note {
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
}

@media (max-width: 767.98px) {
  .sf-footer__fine-links {
    flex: 1 1 100%;
    text-align: left;
  }
}

/* ------------------------------------------------- browse nav dropdown -- */

/*
 * THE BROWSE DROPDOWN (NavMenu.dc.html) and THE MOBILE TAKEOVER
 * (MobileMenu.dc.html).
 *
 * EVERYTHING BELOW IS ADDITIVE. Not one rule above this line is touched: the
 * header's scroll-morph system owns its own boxes and its own z-order, and both
 * of these surfaces are built to hang off that system rather than to reach into
 * it. Both render NOTHING while closed — they are client islands that mount on
 * open — so the header at rest is byte-for-byte the header that was there
 * before (`tools/navgate.mjs closed`).
 *
 * The one exception, and it is a reset rather than a change: the nav's one
 * disclosure entry is a `<button>` where the others are `<a>`, so the block
 * immediately below hands the button back every default a UA gives it. The
 * board draws one row of five identical labels and that is what has to render.
 */

/*
 * THE TRIGGER, RESET TO THE LINK IT REPLACES.
 *
 * `font: inherit` is the one that matters most — a button does not inherit the
 * document's face, and without it this entry alone renders in 13.333px Arial.
 * `.sf-nav__link` above supplies the colour, the flex box and the nowrap; this
 * only removes what the UA added.
 */
.sf-nav__trigger {
  appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  letter-spacing: inherit;
  margin: 0;
  padding: 0;
  text-align: inherit;
}

/*
 * ACTIVE WAYFINDING ON A BUTTON.
 *
 * The same three declarations `.sf-nav__link[aria-current="page"]` carries, off
 * `aria-current="true"` instead — "the current item in this set", which is what
 * a control that opens a panel can honestly claim. It is written out rather
 * than added to that rule's selector list so the morph system's own block is
 * left exactly as it was.
 */
.sf-nav__trigger[aria-current="true"] {
  border-bottom: 3px solid var(--brand-color-logo-orange, #ff5a00);
  box-sizing: border-box;
  color: var(--brand-color-surface, #ffffff);
  font-weight: 600;
}

/*
 * THE PANEL'S FRAME.
 *
 * `top: 100%` of `.sf-header__full` is the whole of the positioning, and it is
 * correct in BOTH header states without a number of its own: that element IS
 * the header's box, so its bottom edge is the bottom of the two-level masthead
 * at rest and the bottom of the pinned row once the header has stuck. See
 * `nav-menu-panel.tsx` for why nothing here is measured from the viewport.
 *
 * z-index 2 places it inside the header's own stacking context — above the nav
 * row (1) and below the masthead (3), which is what leaves the badge hanging
 * over the panel in the pinned state exactly as it hangs over the page. Being
 * inside that context is also what puts it over the title band (20) and the
 * hero (10) with no number of its own: the header's 40 already did that.
 *
 * Below the mobile cut the nav row is gone and so is this; the takeover's own
 * browse section is that width's affordance.
 */
.sf-nav-menu {
  left: 0;
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 2;
}

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

/*
 * The white sheet itself. The board's `margin: 0 64px` is the container's own
 * gutter, so `.sf-container` draws it and this box is simply full width inside
 * it — which is also what makes the panel follow the gutter down the breakpoint
 * ladder without a query of its own.
 *
 * The 8px radius is the board's and is NOT one of the platform radii (4/6/10/
 * 12/16). It is the bottom corners of a sheet whose top edge is the nav bar,
 * and normalising it to 6 would round a corner the board drew at 8.
 */
.sf-nav-menu__panel {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: 0 0 8px 8px;
  border-top: none;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--brand-color-ink, #17181a) 8%, transparent);
  display: flex;
  gap: 30px;
  /* As everywhere else in this chrome: the boards declare no leading, so every
   * line box is `normal` and globals.css's 1.65 body cannot deepen the cards. */
  line-height: normal;
  padding: 26px 32px;
}

.sf-nav-menu__cards {
  display: grid;
  flex: 1;
  gap: 16px;
  grid-template-columns: repeat(5, minmax(0, 1fr));
}

/* The whole card is the link — the board draws a card, and a card that
 * navigates is a card-sized target rather than a title-sized one. */
.sf-nav-menu__card {
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  color: inherit;
  display: flex;
  flex-direction: column;
  gap: 9px;
  height: 100%;
  padding: 14px;
  text-decoration: none;
}

.sf-nav-menu__card:hover {
  border-color: var(--brand-color-line-mid, #d8d6d0);
}

.sf-nav-menu__well {
  align-items: center;
  background: var(--brand-color-well, #f1f0ec);
  border-radius: var(--radius-inner);
  display: flex;
  height: 96px;
  justify-content: center;
}

/* Deliberately under the 9.5px floor: it is the board's placeholder stamp, not
 * copy — the same exemption the parts-diagram micro-text has
 * (docs/storefront-tokens-and-patterns.md §2). */
.sf-nav-menu__well-label {
  color: var(--brand-color-muted, #6c6a62);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: 9px;
}

.sf-nav-menu__card-title {
  color: var(--brand-color-ink, #17181a);
  display: block;
  font-size: var(--text-support);
  font-weight: 700;
  line-height: 1.3;
}

.sf-nav-menu__card-note {
  color: var(--brand-color-ink-soft, #44464a);
  display: block;
  font-size: var(--text-caption);
}

/* Money is a mono duty. */
.sf-nav-menu__card-price {
  font-family: var(--brand-font-mono, ui-monospace, monospace);
}

/*
 * The rail: help choosing a TYPE. Its rules are DASHED on the board — the one
 * dashed stroke in the chrome, and the thing that says this column is an aside
 * rather than a sixth card.
 */
.sf-nav-menu__rail {
  border-left: 1px dashed var(--brand-color-line, #e8e7e3);
  /* THE BOARD'S 210 IS A CONTENT WIDTH. The design boards set no box-sizing
   * reset, so `width: 210px` there is 210px of text with the 26px indent and
   * the rule OUTSIDE it — 237 in all. globals.css puts the whole site on
   * `border-box`, which would take the indent out of the 210 and leave 183: the
   * rail's three situation lines then wrap where the board fits them on one. */
  box-sizing: content-box;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-left: 26px;
  width: 210px;
}

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

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

.sf-nav-menu__hint-answer {
  color: var(--brand-color-ink, #17181a);
  font-weight: 600;
}

.sf-nav-menu__closing {
  border-top: 1px dashed var(--brand-color-line, #e8e7e3);
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin: 0;
  padding-top: 12px;
}

.sf-nav-menu__closing a {
  color: inherit;
  text-decoration: underline;
}

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

/*
 * NARROW WIDTHS: the rail rolls UNDER the cards as a full-width strip rather
 * than being cut — the board's own caption, and the reason this is a wrap
 * rather than a `display: none`. The cut is the 1024 container rung, where the
 * gutter tightens and 5 cards plus a 210px rail stop having room to be a row.
 */
@media (max-width: 1023.98px) {
  .sf-nav-menu__panel {
    flex-direction: column;
    gap: 20px;
  }

  .sf-nav-menu__rail {
    border-left: none;
    border-top: 1px dashed var(--brand-color-line, #e8e7e3);
    padding-left: 0;
    padding-top: 16px;
    width: auto;
  }
}

/* ---------------------------------------------------- mobile takeover -- */

/*
 * THE BLUE SHEET (MobileMenu.dc.html), and the two z-indexes that keep the
 * FROZEN HEADER frozen.
 *
 * The board is drawn as a side-by-side of closed and open so the freeze is
 * checkable: nothing in the bar moves, and the logo — which already runs
 * oversized, centred, and PROTRUDING BELOW the 54px bar — keeps protruding, over
 * the sheet, exactly as it protrudes over the page. That is the whole of why the
 * bar is lifted to 2 and the sheet sits at 1: the sheet is drawn under a bar
 * whose logo hangs into it.
 *
 * Both are inside the header's own stacking context, so neither number has to
 * beat anything on the page — the header's 40 already does.
 */
@media (max-width: 767.98px) {
  .sf-header__mobile {
    z-index: 2;
  }
}

/*
 * `position: fixed` and not `absolute`: the sheet covers the viewport below the
 * bar, and the page behind it is scroll-locked, so it has no business being as
 * tall as a document. `--sf-pinned-offset` is the bar's own published height —
 * the same variable the page's sticky rules read — so the sheet's top edge
 * tracks the bar at every cut without a second copy of the number.
 *
 * The header is `position: sticky` and sets no transform, filter or
 * `will-change`, so it does NOT become the containing block for a fixed
 * descendant: this is positioned against the viewport, which is what makes the
 * `100dvh` below the right height.
 */
.sf-menu {
  background: var(--brand-color-nav, #192950);
  border-top: 1px solid var(--brand-color-nav-raised, #263864);
  bottom: 0;
  color: var(--brand-color-surface, #ffffff);
  display: flex;
  flex-direction: column;
  /* `dvh`, so the sheet is the height of the viewport a phone's collapsing
   * address bar has actually left. */
  height: calc(100dvh - var(--sf-pinned-offset, 54px));
  left: 0;
  /* As everywhere else in this chrome: the boards declare no leading. */
  line-height: normal;
  /* Taller content than the sheet scrolls INSIDE it — the page behind is
   * locked, so this is the only thing left that can. */
  overflow-y: auto;
  position: fixed;
  right: 0;
  top: var(--sf-pinned-offset, 54px);
  z-index: 1;
}

/*
 * The drop. The board draws two static states and says the menu "drops below
 * the header", so the motion is the shortest one that means that word: 8px and
 * 150ms, opacity along with it, transform-only so it costs no layout.
 */
@keyframes sf-menu-drop {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
}

.sf-menu {
  animation: sf-menu-drop 150ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .sf-menu {
    animation: none;
  }
}

.sf-menu__search-row {
  flex: none;
  padding: 24px 18px 10px;
}

/*
 * Drawn as a field, wired as nothing — there is no search overlay on any board
 * to open (see `mobile-menu.tsx`). It is a real button so the control that
 * eventually lands has the right box and the right ring from the start, and it
 * is reset to the board's flat row rather than styled as one.
 */
.sf-menu__search {
  align-items: center;
  appearance: none;
  background: var(--brand-color-nav-raised, #263864);
  border: 0;
  border-radius: var(--radius-control);
  color: var(--brand-color-nav-text, #d9e8ff);
  cursor: pointer;
  display: flex;
  font: inherit;
  font-size: var(--text-support);
  gap: 10px;
  padding: 11px 14px;
  text-align: inherit;
  width: 100%;
}

.sf-menu__nav {
  flex: none;
  padding: 0 18px;
}

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

/* Every row after the browse section carries the board's hairline above it. */
.sf-menu__row:not(.sf-menu__row--browse) {
  border-top: 1px solid var(--brand-color-nav-raised, #263864);
}

.sf-menu__link {
  align-items: center;
  appearance: none;
  background: none;
  border: 0;
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: flex;
  font: inherit;
  font-size: var(--text-emphasis);
  font-weight: 600;
  justify-content: space-between;
  padding: 13px 0;
  text-align: inherit;
  text-decoration: none;
  width: 100%;
}

/* The collapsed rows' chevrons point at a destination and are drawn in the
 * reserved informational tint; the browse row's points at a STATE and is drawn
 * in white, because orange got lost in the blue (Ryan's ruling). */
.sf-menu__link:not(.sf-menu__link--browse) svg {
  color: var(--brand-color-steel, #7387a6);
}

.sf-menu__link--browse {
  font-weight: 700;
  padding: 12px 0 8px;
}

/* The board's 2px inset rule, aligned under the row it belongs to. */
.sf-menu__sub {
  border-left: var(--border-inset);
  border-left-color: var(--brand-color-nav-raised, #263864);
  display: flex;
  flex-direction: column;
  margin: 0 0 8px 2px;
  padding-left: 14px;
}

.sf-menu__sublink {
  color: var(--brand-color-nav-text, #d9e8ff);
  display: block;
  font-size: var(--text-emphasis);
  padding: 9px 0;
  text-decoration: none;
}

.sf-menu__sublink:hover {
  color: var(--brand-color-surface, #ffffff);
}

/*
 * The pinned experts block. `margin-top: auto` is what pins it to the sheet's
 * foot on a tall phone and lets it simply follow the rows on a short one — the
 * board draws the first and the second is what the same rule does when the
 * content has grown past the sheet.
 */
.sf-menu__experts {
  align-items: flex-end;
  display: flex;
  gap: 12px;
  justify-content: space-between;
  margin-top: auto;
  padding: 14px 18px 0;
}

.sf-menu__pitch {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-bottom: 16px;
}

.sf-menu__eyebrow {
  color: var(--brand-color-nav-text, #d9e8ff);
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
}

.sf-menu__phone {
  align-items: center;
  color: var(--brand-color-surface, #ffffff);
  display: flex;
  font-size: var(--text-emphasis);
  font-weight: 700;
  gap: 8px;
  text-decoration: none;
}

.sf-menu__chat {
  align-items: center;
  color: var(--brand-color-nav-text, #d9e8ff);
  display: flex;
  font-size: var(--text-support);
  gap: 8px;
  text-decoration: none;
}

.sf-menu__chat-label {
  text-decoration: underline;
}

/* The always-on presence dot, the same one the header's ghosts wear — here as
 * its own mark rather than a corner badge, because this block has the room the
 * bar did not. */
.sf-menu__dot {
  background: var(--brand-color-good-on-dark, #3dd68c);
  border-radius: var(--radius-pill);
  flex: none;
  height: 7px;
  width: 7px;
}

.sf-menu__photo {
  display: block;
  flex: none;
  height: 77px;
  object-fit: contain;
  object-position: bottom;
  width: 168px;
}

/*
 * THE PARENTLESS CRUMB, as a control.
 *
 * It renders IDENTICALLY to the plain text it replaces — every declaration here
 * removes something the UA added to a `<button>`, and nothing adds anything the
 * span did not have. `font: inherit` is the load-bearing one: a button does not
 * inherit the document's face, and without it this crumb alone renders in
 * 13.333px Arial. What it gains is the hover the crumb's LINK siblings already
 * have, because it is now the same kind of thing they are.
 */
.sf-crumbs__pop {
  appearance: none;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
  letter-spacing: inherit;
  margin: 0;
  padding: 0;
  text-align: inherit;
}

.sf-crumbs__pop:hover {
  color: var(--brand-color-ink, #17181a);
  text-decoration: underline;
  text-underline-offset: 4px;
}
