/*
 * SECTION PATTERNS — the shared stylesheet for the two page-level patterns that
 * belong to the PLATFORM rather than to any one page (tokens §4): the SECTION
 * HEADER and the REVIEW BAR.
 *
 * Served as a static asset and pulled in by `<SectionStyles>` rather than
 * `import`ed, for exactly the reasons given at the top of chrome.css: a bundled
 * import attaches the chunk to the shared `[brand]` layout segment and Next then
 * emits a <link> to it on every brand's every page. A hoisted <link> to a public
 * asset is rendered only by the surface that needs it — and this one is rendered
 * by the COMPONENTS, so a page adopting either pattern gets its rules by
 * rendering it.
 *
 * Class prefix `sf-sec-` (written `sf-sec-head…` / `sf-sec-reviews…`, the
 * sibling sheets' convention).
 *
 * ─── WHY THIS SHEET EXISTS ──────────────────────────────────────────────────
 *
 * The section header shipped in category.css on 2026-08-25 with a note saying it
 * "moves to a shared sheet when the second one lands". It landed FIVE times:
 *
 *   1. category.css  `.sf-cat-section-head`     the comparison table's head
 *   2. category.css  `.sf-cat-section-head`     the review bar's head (same rules)
 *   3. home.css      `.sf-home-band__head`      CHOOSE A CATEGORY
 *   4. home.css      `.sf-home-reviews__head`   the homepage review bar
 *   5. home.css      `.sf-home-live__head`      Live Orders
 *   6. photos.css    `.sf-ph__head`             the customer-photo module
 *
 * and the review bar twice (category.css + home.css). Every time the move was
 * declined for the same reason: it read as a chrome.css edit, and every one of
 * those passes was gated on chrome.css being untouched. THE PHOTO MODULE'S
 * PROMOTION (photos.css, `sf-ph-`, 2026-08-26) settled that — a NEW shared sheet
 * avoids chrome.css entirely, which is the precedent this file follows.
 *
 * ─── THIS IS A RELOCATION ───────────────────────────────────────────────────
 *
 * Every declaration below arrived from category.css, home.css or photos.css
 * UNCHANGED, and the move is proved rather than asserted: `tools/phrelo.mjs`
 * captures each consumer's affected region before and after the move and diffs
 * the two at threshold 0, at 1440 and at 390 — six regions across two pages,
 * all pixel-identical (the noise floor of that capture was measured at zero in
 * the same session, so a zero means something). The absolute board gates
 * (`tools/p4gate.mjs`'s review regions, `tools/h2gate.mjs`'s) hold their
 * recorded numbers across it.
 *
 * THE PAIRWISE PARITY GUARDS ARE RETIRED with this move — they existed to hold
 * copies against each other, and there is one copy now. See the note in
 * tests/home-page.test.ts.
 *
 * ONE DECLARATION IS NOT SHARED and it is the only asymmetry left: the review
 * bar's `line-height: normal` sits on the `--band` placement alone, because the
 * `--stage` placement inherits it from `.sf-cat-hero`, which declares it for the
 * whole white stage. Hoisting it to the base would be a no-op on both — and a
 * relocation with no changed declaration is the only kind whose A/B means
 * anything, so it stays where it was and this comment says why.
 */

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

/*
 * EVERY INTERACTIVE CONTROL IN THIS SHEET, in one block (Ryan's ruling,
 * 2026-08-26 — the platform pattern, its three grounds and the contrast
 * arithmetic are documented once, at the top of chrome.css).
 *
 * The `var()` fallbacks repeat the platform values exactly once each, so this
 * sheet still draws a compliant ring if it is ever loaded without chrome.css.
 *
 * The section header holds no controls: it is a circle, a mark and a heading.
 * The review bar holds two — the "Read all reviews" link, on the page's paper,
 * and the mobile quote strip. THE PAGER holds one kind: its page buttons, which
 * take the outward ring like every other quiet control on a paper ground.
 */
/* THE CONFIGURATION BUTTON is the third kind, added with the shared row
 * (Ryan’s DD-A/DD-B, 2026-08-28): a bordered square on the section’s own
 * ground, which is where every other quiet control on these surfaces takes the
 * outward ring. */
/* THE EMBED'S TWO ACTIONS are the fourth kind (the shared-embed-core
 * extraction, 2026-08-28): an ink-filled "Continue this conversation" and the
 * quiet link beside it, both on the answer box's `surface` ground. Accent on
 * surface is 4.58:1, over the 3:1 the indicator needs — and the button's own
 * ink fill is what the offset holds the ring clear of. */
/* THE SEND ARROW is the fifth kind (Ryan's WP3, 2026-08-29): a bare button
 * INSIDE the drawn field, on the field's own `surface` ground. It takes the
 * outward ring rather than the X's inset one, and that is measured rather than
 * copied: the X sits hard against the field's right padding at `right: 8px`,
 * where an outward ring would be drawn half outside the box; this one sits
 * 36px in, with room on every side. */
/* THE TOGGLE'S SEGMENTS are the sixth kind (Ryan's WP9, 2026-08-29). The ring
 * is OUTWARD and on the segment rather than on the track: the track is a
 * container, and a ring around it could not say which of the two has focus —
 * the same reasoning pdp.css records for the quantity stepper's two buttons. */
.sf-seg__option:focus-visible,
.sf-field-send:focus-visible,
.sf-embed__continue:focus-visible,
.sf-embed__quiet:focus-visible,
.sf-sec-config__button:focus-visible,
.sf-sec-pager__page:focus-visible,
.sf-sec-reviews__all a:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * THE QUOTE STRIP'S RING IS DRAWN INSIDE. Chrome makes a scroll container
 * keyboard-focusable so it can be scrolled without a pointer, so the strip IS a
 * tab stop; it runs edge to edge at the mobile widths where it exists, and an
 * outward ring on a full-bleed box is drawn off the viewport.
 */
.sf-sec-reviews__quotes:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-inset, -2px);
}

/* ========================================================= section head == */

/*
 * The section header pattern (tokens §4): a 40px tinted circle carrying the
 * section's mark, then the title beside it.
 */
.sf-sec-head {
  align-items: center;
  display: flex;
  gap: 14px;
}

.sf-sec-head__icon {
  align-items: center;
  background: var(--brand-color-nav-text, #d9e8ff);
  border-radius: var(--radius-pill);
  color: var(--brand-color-nav, #192950);
  display: inline-flex;
  flex: none;
  height: 40px;
  justify-content: center;
  width: 40px;
}

.sf-sec-head__title {
  color: var(--brand-color-ink, #17181a);
  /* globals.css's heading rules would otherwise supply the typeface and a 1.12
   * leading, and the leading alone moved this title 1px inside its own 40px flex
   * row — 2.546% on the homepage's `bandhead` crop when it was missing there. */
  font-family: var(--brand-font-body, system-ui, sans-serif);
  /* FLAT, at the platform's section-header size. The homepage demo carried a
   * `clamp(22px, 2.1vw, 26px)` ramp under it and the category page did not; both
   * resolve to 26 at 1440, so every pixel gate on both pages was green while the
   * same header was a different size on the two pages everywhere between 768 and
   * 1238. That divergence is what the parity guard caught, and having ONE
   * declaration is what stops it recurring. */
  font-size: var(--text-section);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: normal;
  margin: 0;
  /* globals.css balances every h1/h2/h3; no board asks for it, and a balanced
   * two-word title wraps where the algorithm likes rather than where it fits.
   *
   * `wrap` — the initial value, which is what turns balancing off. This read
   * `normal` until 2026-08-26; `normal` is `white-space`'s keyword, not
   * `text-wrap`'s, so the declaration was dropped at parse time and `balance`
   * never actually left. Today's two-word heads fit on one line at every width,
   * where the two algorithms cannot differ — so the correction is invisible now
   * and does its job the first time a longer head lands. */
  text-wrap: wrap;
}

/*
 * THE PLATFORM'S MOBILE TREATMENT — smaller circle, smaller mark, the title
 * dropped one step down the type scale. Both mobile boards draw every section
 * head this way, so it belongs to the pattern and not to any one section.
 *
 * THE PATTERN HAS NO ONE-SIDED EXCEPTION. `.sf-cat-section-head__hint` — the
 * comparison table's mobile "Swipe for specs · model stays pinned" line — was
 * the one rule that ever lived on a single copy, and Ryan retired the line
 * itself on 2026-08-26 (self-evident: the transposed strip cuts its third column
 * mid-cell). Nothing here is scoped to a page.
 */
@media (max-width: 767.98px) {
  .sf-sec-head {
    gap: 11px;
  }

  .sf-sec-head__icon {
    height: 34px;
    width: 34px;
  }

  .sf-sec-head__icon svg {
    height: 16px;
    width: 16px;
  }

  .sf-sec-head__title {
    font-size: var(--text-product);
  }
}

/* ========================================================== review bar == */

/*
 * THE REVIEW BAR (tokens §4). Transplanted from CategoryPage.dc.html and
 * HomeB.dc.html (drawn at 1440) and CategoryMobile.dc.html (drawn at 390).
 *
 * An icon header, then two columns: the AGGREGATE STACKED THREE-HIGH — the
 * mono score over its stars over the review count — and three quote cards
 * beside it.
 *
 * TWO PLACEMENTS, and they are the only thing the two boards disagree about:
 *
 *   `--stage`  the category board draws the bar INSIDE the hero's white stage,
 *              pulled 95px up so its top lands at the bottom of the system's
 *              cast shadow — the review bar is the hero's closing line, not a
 *              section after it. Below the mobile cut the pull is gone and it
 *              becomes its own paper band, exactly as the mobile board draws it.
 *
 *   `--band`   the homepage board draws it as its own section on the page's
 *              paper, 84px below the CHOOSE A CATEGORY band.
 *
 * Everything else — every declaration between here and the clipped text at the
 * foot of the sheet — is the pattern, and there is one copy of it.
 */

/* The board's own number: the bar's top lands on the bottom of the hoop's cast
 * shadow. `z-index` over the stage for the same reason the hero's copy column
 * has one — an <img> paints over its siblings (tokens gotcha #5). */
.sf-sec-reviews--stage {
  margin-top: -95px;
  position: relative;
  z-index: 2;
}

.sf-sec-reviews--band {
  /* THE SECTION'S LEADING, and it is the same declaration `.sf-cat-hero` makes
   * for the same reason — which is why the `--stage` placement does not repeat
   * it: the boards declare no leading on the section, so every line box that
   * does not name one is `normal`. globals.css's body 1.65 would otherwise
   * deepen the count line, the "Read all reviews" line and every quote card's
   * attribution — measured at +14px on the aggregate column and +13px on the
   * cards before this landed. */
  line-height: normal;
  /* The platform's section rhythm (tokens §3: 84–96px desktop), at the board's
   * own 84. `padding-block` rather than `margin` on the section itself would
   * paint no differently here — the ground is the page's paper either way — and
   * `margin-top` is what the board writes. */
  margin-top: 84px;
}

/* One heading, two wordings — the mobile board drops "Customer" to hold one
 * line. `display: none` rather than clipping: the wording that is not in play
 * must leave the accessibility tree too, or the title is announced twice. */
.sf-sec-reviews__heading-compact {
  display: none;
}

.sf-sec-reviews__grid {
  align-items: start;
  column-gap: 44px;
  display: grid;
  /* The board's fixed 300px aggregate column. `minmax(0, 1fr)` beside it
   * because a bare `fr` floors at its min-content, and the quote cards' longest
   * word would then set the column. */
  grid-template-columns: 300px minmax(0, 1fr);
  margin-top: 34px;
}

/*
 * THE AGGREGATE, STACKED THREE-HIGH — score over stars over count — at every
 * size. Ryan's ruling, 2026-08-24: "the inline 4.91 + tiny stars + count row
 * read weird — restacked THREE HIGH like desktop". The score is what a reader
 * came to this bar for, and it does not become small print because the screen
 * got narrower.
 */
.sf-sec-reviews__score {
  font-family: var(--brand-font-mono, ui-monospace, monospace);
  font-size: var(--text-display-lg);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1;
  margin: 0;
}

.sf-sec-reviews__score-stars {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
  margin-top: 10px;
}

.sf-sec-reviews__score-stars svg {
  height: 20px;
  width: 20px;
}

.sf-sec-reviews__count {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-emphasis);
  font-weight: 600;
  margin: 10px 0 0;
}

.sf-sec-reviews__all {
  font-size: var(--text-support);
  margin: 12px 0 0;
}

.sf-sec-reviews__all a {
  color: var(--brand-color-muted, #6c6a62);
  text-decoration: underline;
}

/* Three cards on one row. They are a list — three reviews, not one paragraph —
 * so the grid is a <ul> and the cards are its items. */
.sf-sec-reviews__quotes {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.sf-sec-reviews__quote {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-media);
  /* EVERY LINE INSIDE THIS CARD SETS ITS OWN SIZE, so this one declaration
   * exists for the box's STRUT: the star row is an inline-flex box, and the
   * line box it sits in is sized by the card's own font. The boards inherit a
   * 16px base from an unstyled `body`; globals.css's base is 17px, which put
   * the stars a pixel low and made the card a pixel tall. Board value, on the
   * platform's own token. */
  font-size: var(--text-emphasis);
  padding: 22px 24px;
}

.sf-sec-reviews__quote-stars {
  color: var(--brand-color-accent, #c4530a);
  display: inline-flex;
}

.sf-sec-reviews__quote-stars svg {
  height: 15px;
  width: 15px;
}

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

/* The mobile board shortens two of the three quotations to fit its cards. The
 * wording that is not in play leaves the tree, as with the heading. Only the
 * category copy carries the shorter wordings today; a surface that supplies
 * none renders the one quotation at both widths. */
.sf-sec-reviews__quote-compact {
  display: none;
}

.sf-sec-reviews__quote-by {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  margin: 0;
}

@media (max-width: 767.98px) {
  /*
   * The mobile board's own band: paper, 40px of air over the heading, 34px
   * under the last thing in it. The 52px that closed the white stage is this
   * block's margin now (see the hero's `padding-bottom: 0` at this cut), so the
   * white still ends exactly where the board ends it.
   */
  .sf-sec-reviews--stage {
    background: var(--brand-color-paper, #fbfbfa);
    margin-top: 52px;
    padding: 40px 0 34px;
    z-index: auto;
  }

  .sf-sec-reviews--band {
    margin-top: var(--space-section-mobile);
  }

  .sf-sec-reviews__heading-wide {
    display: none;
  }

  .sf-sec-reviews__heading-compact {
    display: inline;
  }

  /*
   * One column, and "Read all reviews" MOVES: the board puts it under the quote
   * strip on mobile and beside the score on desktop. `display: contents` on the
   * aggregate dissolves its box so its four children become flex items of this
   * column, and `order` puts the link last without a second copy of it in the
   * DOM — the same device the category hero's stage uses at this cut.
   */
  /*
   * ─── MC2: THE DOTS AND "READ ALL REVIEWS" ARE ONE ROW (Ryan, 2026-08-29) ──
   *
   * "The dots row and Read all reviews are cramped under the cards: the
   * progression dots come DOWN a little, and Read all reviews goes RIGHT-
   * ALIGNED on the SAME ROW as the dots."
   *
   * A GRID, NOT A FLEX COLUMN, and that is the whole of the change: every item
   * that was a full-width row still is (`grid-column: 1 / -1`), and the two the
   * ruling pairs are the only two given columns of their own. The alternative
   * was a wrapper element around the pair — which the desktop grid has no use
   * for, and which would have to be conditionally rendered, i.e. a second
   * arrangement of the same four boxes in the markup rather than in the sheet.
   *
   * PLACEMENT STILL FOLLOWS `order`. Auto-placement walks the order-modified
   * document order, so the strip is still 4, the dots 4 (DOM order breaks the
   * tie) and the link 5 — the same sequence the flex column drew, with the last
   * two landing side by side instead of stacked.
   *
   * `align-items: center` is for that row and is harmless on the others: every
   * other row holds ONE item and is sized by it, so centring it in a track it
   * defines is where it already was. MEASURED: score, stars and count unmoved.
   */
  .sf-sec-reviews__grid {
    align-items: center;
    display: grid;
    /* The dots keep the LEFT edge they have today (measured x=15 at 390 — the
     * page gutter), so their column is sized to them and the link's takes the
     * rest. `minmax(0, 1fr)` rather than `1fr` so a long link cannot push the
     * track past the gutter. */
    grid-template-columns: auto minmax(0, 1fr);
    margin-top: 18px;
  }

  /* Everything that is not the paired row spans both columns — the aggregate's
   * own children included, which `display: contents` has already made items of
   * this grid. */
  .sf-sec-reviews__score,
  .sf-sec-reviews__score-stars,
  .sf-sec-reviews__count,
  .sf-sec-reviews__quotes {
    grid-column: 1 / -1;
  }

  .sf-sec-reviews__aggregate {
    display: contents;
  }

  .sf-sec-reviews__score {
    /* Between --text-display-sm and --text-display, and drawn rather than
     * derived: the mobile board's own 52 (Ryan, 2026-08-24). */
    font-size: 52px;
  }

  .sf-sec-reviews__score-stars {
    /* THE LINE BOX'S DESCENDER, restored by hand. On the board the star row is
     * an inline-level box, so the line box it sits in carries the strut's
     * descent below the glyphs — measured at 4px against the board's 16px base.
     * `display: contents` on the aggregate above blockifies this row into a flex
     * item, which has no line box and therefore no descent, and the count line
     * came up 4px. */
    margin-bottom: 4px;
    margin-top: 8px;
  }

  .sf-sec-reviews__score-stars svg {
    height: 13px;
    width: 13px;
  }

  .sf-sec-reviews__count {
    font-size: var(--text-support);
    margin-top: 7px;
  }

  .sf-sec-reviews__all {
    /* THE SIZE MOVES TO THE LINK at this cut. The board draws the line as a
     * 13.5px anchor inside a div that inherits the 16px base, so the LINE BOX
     * is the 16px strut's 20px and the small text is positioned inside it.
     * Sizing the paragraph itself gave a 17px line box and pulled the text 2px
     * up. Desktop is untouched: its own crop reads 0.000% as authored. */
    font-size: var(--text-emphasis);
    /* MC2: the second column of the dots' row, against the right gutter. Its
     * own `margin-top` is RETIRED — the air above this row is the strip's
     * `margin-bottom` now, declared once for both members so the two cannot
     * sit at different heights inside the row they share. */
    grid-column: 2;
    justify-self: end;
    margin-top: 0;
    order: 5;
  }

  .sf-sec-reviews__all a {
    font-size: var(--text-small);
  }

  /*
   * THE SWIPE STRIP — DISCRETE CARDS (Ryan's ruling, 2026-08-26: "just like our
   * photo viewer — swipe and you go to the next"). It breaks the page gutter and
   * takes it back as padding, so the cards start on the text's left edge and the
   * last one can still reach the right edge.
   *
   * `mandatory`, WHERE THE TABLE'S STRIP IS `proximity`, and the difference is
   * the cards rather than a preference: the table's columns are not a uniform
   * width, so it has no page to snap to and a reader who stops between two of
   * them must be allowed to. Every card here is exactly one viewport, so
   * mandatory is what makes the ruling true rather than approximately true — and
   * it is safe in the sense mandatory can be unsafe, because a snap area the
   * same size as the snapport can never scroll content out of reach.
   *
   * THE GAP IS THE GUTTER, and that is what kills the peek completely rather
   * than nearly: card + gap is one viewport plus one gutter, so the next card's
   * left edge sits exactly ON the right edge of the screen. A 10px gap would
   * have left the last 5px of the gutter showing a sliver of the neighbour —
   * the peek surviving as an accident after being removed on purpose.
   */
  .sf-sec-reviews__quotes {
    display: flex;
    gap: var(--sf-gutter);
    /* MC2: THE AIR UNDER THE CARDS, declared once. It was the dots'
     * `margin-top`, which cannot serve a row the link also sits in — two
     * members with two top margins land at two heights. On the strip it is one
     * number for one gap, and `align-items: center` puts both members on it. */
    margin-bottom: 24px;
    margin-inline: calc(-1 * var(--sf-gutter));
    margin-top: 14px;
    order: 4;
    overflow-x: auto;
    padding-inline: var(--sf-gutter);
    /*
     * THE SCROLLBAR IS HIDDEN, THE SCROLLING IS NOT (Ryan, 2026-08-27) — the
     * photo chips' fix (photos.css, `.sf-ph__filters`), applied to the one
     * other swipe strip on the site that was still drawing one.
     *
     * The bar is redundant HERE in a way it is not everywhere: this strip
     * already says where the reader is, twice over — the DOTS underneath it are
     * the site's one way of drawing "you are here", and the live region names
     * the card by number. A platform scrollbar under them is a third answer to
     * a question already answered, drawn in the one place the eye is travelling.
     *
     * IT REMOVES NO CAPABILITY, which is the constraint: `scrollbar-width` and
     * `::-webkit-scrollbar` are PAINT. The strip still scrolls with a wheel, a
     * drag, a swipe and the arrow keys, still snaps, and still takes the
     * platform focus ring — it is the tab stop that `tools/revswipe.mjs`
     * asserts it is (Chrome makes a scroller focusable when it holds no
     * focusable child, and these cards hold none). Both spellings, because they
     * are different engines: the standard property for Firefox, the
     * pseudo-element for WebKit/Blink.
     */
    scrollbar-width: none;
    scroll-padding-left: var(--sf-gutter);
    scroll-snap-type: x mandatory;
  }

  .sf-sec-reviews__quotes::-webkit-scrollbar {
    display: none;
  }

  .sf-sec-reviews__quote {
    /*
     * ONE CARD, ONE VIEWPORT. `100%` of the flex container's CONTENT box, which
     * is the screen less its two gutters — the same measure every other block in
     * the section is set to, so a card's type lines up with the heading above it
     * instead of hanging 38px past it.
     *
     * IT SUPERSEDES THE BOARD'S 82% (`calc(82% + 38px)` — that percentage
     * border-box-converted), which drew the peek. The board cannot draw a swipe;
     * the ruling is the spec here, and the board's own drawing is what it
     * replaces.
     */
    flex: 0 0 100%;
    /*
     * MC2: MORE AIR TOP AND BOTTOM (Ryan, 2026-08-29). The block padding only —
     * the 18px sides are the card's measure and moving them would move where
     * every quote wraps, which is a horizontal change out of a vertical ruling.
     *
     * 24 IS NOT A NAMED TOKEN, and that is a flag rather than a decision made
     * quietly: the platform's space scale has nothing between
     * `--space-after-header` (14) and `--space-section-mobile` (56), and this
     * is a card's inner padding rather than a rhythm between sections. 24 is
     * the house step this block's neighbours already sit on.
     */
    padding: 24px 18px;
    scroll-snap-align: start;
  }

  /*
   * THE PROGRESS DOTS. Drawn as the comparison table's are
   * (`.sf-cat-table__dot`, category.css) and as the lightbox's phone rail is —
   * the same 4px bar, 6px gap and ink-on-line-mid pair — because the site should
   * have ONE way of drawing "you are here" rather than three.
   *
   * FLAGGED: that makes this the SECOND copy of the dot drawing on a light
   * ground (the lightbox's is white-on-translucent, a different ground and a
   * genuinely different rule). Three declarations is under the bar the section
   * header crossed, but this sheet is where it would go, and the table's copy is
   * the one to move when a third light-ground rail lands.
   *
   * `order: 4` puts them with the strip: the strip is 4 and the "Read all
   * reviews" line is 5, and equal orders keep DOM order, so the dots land
   * between them without a third number.
   */
  .sf-sec-reviews__dots {
    display: flex;
    gap: 6px;
    /* MC2: the first column of the paired row. `margin-top` RETIRED BY NAME —
     * see the strip's `margin-bottom`. */
    grid-column: 1;
    justify-content: center;
    order: 4;
  }

  .sf-sec-reviews__dot {
    background: var(--brand-color-line-mid, #d8d6d0);
    border-radius: 2px;
    height: 4px;
    width: 18px;
  }

  .sf-sec-reviews__dot--on {
    background: var(--brand-color-ink, #17181a);
  }

  .sf-sec-reviews__quote-stars svg {
    height: 13px;
    width: 13px;
  }

  .sf-sec-reviews__quote-text {
    font-size: var(--text-support);
    line-height: 1.55;
    margin: 9px 0 10px;
  }

  .sf-sec-reviews__quote-wide {
    display: none;
  }

  .sf-sec-reviews__quote-compact {
    display: inline;
  }

  .sf-sec-reviews__quote-by {
    font-size: var(--text-caption);
  }
}

/* =============================================================== pager == */

/*
 * THE PAGER (Ryan's ruling PP5, 2026-08-28): "consistency is King" — Reviews,
 * Customer photos and Answers draw ONE pager, not three that resemble each
 * other.
 *
 * ─── IT IS A PROMOTION, NOT A NEW DRAWING ───────────────────────────────────
 *
 * Every declaration below arrived UNCHANGED from `.sf-pdp-rev__pager` /
 * `.sf-pdp-ans__pager` (pdp.css) and `.sf-ph__pager` (photos.css), which were
 * already the same pager written twice: the same 16px gap, the same
 * space-between row, the same muted `--text-support` label, and a page button
 * that was byte-for-byte identical in both sheets (transparent ground,
 * `line-mid` border, `--radius-control`, 12.5px/500, 8px 14px). The sheets
 * agreed on the drawing and disagreed on two details, and each is settled here
 * with the reason:
 *
 *   THE HAIRLINE ABOVE IT ships. Reviews and Answers drew one; the photo module
 *   did not. A pager is the close of a list, and the rule that separates it from
 *   the last row is what makes it read as one — so the photo section GAINS it
 *   (its `phctl` pager crop re-records with that attribution).
 *
 *   THE SPENT ARROW IS MUTED AND NOT FADED. The reviews copy carried
 *   `opacity: 0.5` on top of the muted colour; the photo module's did not, and
 *   its own comment gives the reason — the boards draw `‹` on page one in
 *   `muted` and `›` in `ink`, which is a pager saying which way it can still go,
 *   and a fade is a treatment this design language does not use. The better-
 *   argued half wins and the fade retires.
 *
 * THE SPACE ABOVE THE PAGER IS NOT THE PAGER'S. Reviews and Answers sit theirs
 * directly under a bordered list (the `padding-top` below is the whole gap); the
 * photo grid needs air between the tiles and the rule. That distance belongs to
 * the SECTION, so each surface keeps its own margin on its own wrapper class —
 * which is also what keeps `photos.css` inside its own share of the namespace.
 */
.sf-sec-pager {
  align-items: center;
  border-top: var(--border-hairline);
  display: flex;
  gap: 16px;
  justify-content: space-between;
  padding-top: 16px;
}

.sf-sec-pager__label {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  /* A `<p>`, so its UA margin has to go: the row is centred on the buttons. */
  margin: 0;
}

.sf-sec-pager__pages {
  align-items: center;
  display: flex;
  gap: 6px;
}

/*
 * ONE PAGE BUTTON — the boards' `ghost-sm`: a 1px border in the quiet line
 * colour, 8px/14px of padding and 12.5px semibold type, with the current page
 * inverted.
 *
 * TRANSPARENT, NOT `surface`. The boards' `ghost-sm` declares no ground, so the
 * section's paper shows through — and paper against surface is a four-per-
 * channel delta, which the acceptance threshold of 24 is blind to (playbook §4,
 * the mobile row-striping lesson). Drawn as the boards draw it, and read at
 * threshold 2.
 */
.sf-sec-pager__page {
  appearance: none;
  background: transparent;
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-caption);
  font-weight: 500;
  padding: 8px 14px;
}

.sf-sec-pager__page--on {
  background: var(--brand-color-ink, #17181a);
  border-color: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
  font-weight: 600;
}

/* A step at the end of its run is drawn muted — see the promotion note above. */
.sf-sec-pager__page:disabled {
  color: var(--brand-color-muted, #6c6a62);
  cursor: default;
}

/*
 * THE GAP IS NOT A CONTROL — but it IS a box: the boards draw the `…` as another
 * `ghost-sm`, so it keeps the border and loses only the pointer and the press.
 * Drawing it as bare type would leave a hole in a row of pills.
 */
.sf-sec-pager__page--gap {
  color: var(--brand-color-ink, #17181a);
}

/*
 * ─── ONE LINE AT EVERY WIDTH (Ryan's ruling A2, 2026-08-27, now shared) ─────
 *
 * The answers pager stacked on a phone — label on one line, pages on the next.
 * MEASURED at 390, where it draws seven buttons: label 70.92 + gap 16 + pages
 * 297.75 = 384.67 against 360 of measure, so the row could not hold.
 *
 * THREE TRIMS MAKE IT FIT, none of which touches the desktop: the label drops
 * one step of type (14.5 -> 13.5) and refuses to wrap; the buttons give up 4px
 * of side padding each; and the group's gap goes 6 -> 5. That is ~315px at 390,
 * with room.
 *
 * AND THE PAGES GROUP IS THE ESTABLISHED SCROLLER for the widths where seven
 * buttons plus a label genuinely cannot fit — 320, where the measure is 290.
 * "One line" is then still literally true, and the alternative (shrinking the
 * controls until they fit a 320 screen) would take the tap targets below
 * anything usable. Hidden bar, `flex: none` children, `position: relative` — the
 * same three every chip row on this site takes.
 *
 * THE PHOTO PAGER TAKES IT TOO, where it used to stack its note over its steps.
 * That divergence retires with the promotion: one pager, one phone treatment.
 *
 * NOT A TAB STOP under Chrome's rule: the group holds focusable buttons.
 */
@media (max-width: 767.98px) {
  .sf-sec-pager {
    gap: 10px;
  }

  .sf-sec-pager__label {
    flex: none;
    font-size: var(--text-small);
    white-space: nowrap;
  }

  .sf-sec-pager__pages {
    gap: 5px;
    /* A flex item's automatic minimum size is its content, so a nowrap row
     * inside one refuses to be narrower than its buttons and the CLIP never
     * happens. */
    min-width: 0;
    overflow-x: auto;
    /* A scroller with no positioned ancestor lets an abs-positioned descendant
     * escape into the DOCUMENT's overflow (playbook §5). Standing precaution. */
    position: relative;
    scrollbar-width: none;
  }

  .sf-sec-pager__pages::-webkit-scrollbar {
    display: none;
  }

  .sf-sec-pager__page {
    flex: none;
    padding: 8px 10px;
  }
}

/* Text a reader gets from the layout and a listener cannot — the aggregate
 * score with its scale and its count. Same treatment, and the same reasoning,
 * as `.sf-cat-table__sr`. */
.sf-sec-reviews__spoken {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* ================================================== the configuration row == */

/*
 * ─── ONE CONFIGURATION ROW, THREE SECTIONS (Ryan's DD-A + DD-B, 2026-08-28) ─
 *
 * PP1 gave the photo module a configuration button inline with its search
 * field, revealing the filter and the sort. PN1 gave the answers section the
 * same button. DD-B gives it to REVIEWS as well — "Consistency!" — and DD-A
 * rules the layout the three of them share.
 *
 * THE LAYOUT IS A TABLE, and that supersedes a measured impossibility. The
 * previous pass reported "Filter and sort in line with each other is IMPOSSIBLE
 * at the panel width — 434px needed against 307 available" and offered Ryan two
 * levers (drop the leading labels, or widen the panel). He took neither and
 * ruled a third: stop sizing the row to its CONTENT. A two-column grid whose
 * label column is `max-content` over BOTH labels and whose control column is
 * `1fr` fits any width there is, because the control takes whatever is left —
 * so the labels line up as a column, the controls line up as a column, and the
 * 434 was only ever the width of a row that refused to give.
 *
 * `display: contents` ON THE PAIRS is what makes one grid out of three
 * components. Each control is drawn by the section that owns it, wrapped in its
 * own labelled group so the label and the control stay associated; the wrapper
 * generates no box, so its two children are the grid's children and every
 * section's labels share one column measure.
 *
 * THE CONTROL FILLS ITS COLUMN. `QuietSelect`'s `width="field"` is the half
 * that lives in the shared control (chrome.css) — this sheet only says how wide
 * the column is.
 */

/*
 * ─── THE ROW OWNS ITS AIR ABOVE THE FIELD (Ryan's MP5, 2026-08-29) ─────────
 *
 * "The disclosure row butts against the search field's bottom — give it the
 * SAME ~15px the two config fields have between them; universal (photos shows
 * it; answers shows it)."
 *
 * MEASURED BEFORE CHANGING ANYTHING, at 390 AND 1440: the gap between the field
 * row's bottom and this grid's top was **0px on the photo module and 0px on the
 * answers section**, and **14px on reviews** — where it is not this row's at
 * all but the reviews search card's own `gap: 14px` column. So two surfaces had
 * nothing and one had 14, which is the "~15" Ryan read off the screen and the
 * one of the three he did not report as broken.
 *
 * SO 14 IS THE NUMBER, and it is the reviews card's own — the value already on
 * the page that Ryan accepted, and a step the platform's scale actually has
 * (MC2 recorded that there is nothing between 14 and 56 on it). Declaring 15
 * would have moved the one surface that was right.
 *
 * NOT `var(--sf-gutter)`, which the brief suggested: that token is 15px only at
 * the phone rung — it is 64px at 1440 — so a vertical margin reading it would
 * open a 64px hole above the row on desktop. Named here because "the gutter
 * token" is the obvious wrong answer and the next reader deserves to know it
 * was measured and rejected.
 *
 * ONE DECLARATION, ONE EXCEPTION, both here: the exception is that reviews'
 * card ALREADY spends this air as a flex `gap`, and a margin inside a gapped
 * column ADDS to it (14 + 14 = 28). Its own sheet zeroes this margin, next to
 * the card that owns the gap.
 */
.sf-sec-config {
  align-items: center;
  column-gap: 10px;
  display: grid;
  /* THE LABEL COLUMN IS THE LONGEST LABEL, measured by the browser rather than
   * declared: `max-content` on an auto-placed column takes the widest item in
   * it, which is exactly "sized to the longest label text". */
  grid-template-columns: max-content minmax(0, 1fr);
  margin-top: 14px;
  row-gap: 12px;
}

/* ONE GRID, NOT THREE ROWS OF FLEX — see the note above. */
.sf-sec-config__pair {
  display: contents;
}

/*
 * THE LABEL — a glyph and one word about the CONTROL, in the supporting type on
 * the muted rung, which is the treatment the reviews list-head has always given
 * "Sort by". `flex` rather than an inline box so the glyph's own box centres
 * against the word rather than against its line box.
 */
.sf-sec-config__label {
  align-items: center;
  color: var(--brand-color-muted, #6c6a62);
  display: inline-flex;
  flex: none;
  font-size: var(--text-support);
  gap: 6px;
}

.sf-sec-config__mark {
  display: flex;
  flex: none;
}

/* WHERE THE LABEL IS A REAL `<label for>` — the reviews "With photos" row since
 * FX4 — the whole word is a tap target for the checkbox beside it, so it says
 * so. The `<span>` form of the same class is not pressable and does not. */
label.sf-sec-config__label {
  cursor: pointer;
}

/*
 * ─── THE FIELD ROW, AND THE BUG IN IT (Ryan, 2026-08-28) ───────────────────
 *
 * "On mobile the configure button is squished — taller than the search field.
 * Its height must match the field exactly."
 *
 * THE FIX IS TO STOP GIVING IT A HEIGHT AT ALL. The button carried its own
 * `height`/`width` pair, so it agreed with the field only at the width where
 * those numbers happened to match the field's line box; below the mobile cut
 * the field grows (bigger type, more padding) and the button did not follow.
 * `align-items: stretch` on the row hands the button the row's height, which IS
 * the field's height, at every width and in every type size — and the button
 * keeps a `width` because a square is a square only if one of its sides is
 * declared.
 *
 * `aspect-ratio: 1` rather than a second number: whatever height the field
 * turns out to have, the button is that wide too.
 */
.sf-sec-config__row {
  align-items: stretch;
  display: flex;
  gap: 8px;
  /*
   * NO LEAD OF ITS OWN, and that is a decision rather than an omission.
   *
   * The distance above this row belongs to the SECTION: the photo module’s
   * panel is a block where the row needs 12px of its own, and the reviews
   * search card is a `gap: 14px` column where a margin would be spacing on top
   * of spacing — MEASURED as 12px of it, which is what pushed
   * `tools/pdpbelow-gate.mjs`’s two reviews-search crops out of size.
   *
   * WHAT THE ROW OWNS is that its children do not carry vertical margins (see
   * the `:first-child` rule below): a margin on a stretch row’s child is height
   * that child does not get, which is the button/field mismatch DD-A names.
   */
}

/*
 * THE ROW OWNS ITS CHILDREN’S VERTICAL SPACING, and this is the second half of
 * the height bug rather than tidiness.
 *
 * `align-items: stretch` gives both children the ROW’s height — minus their
 * own vertical margins, which is the part that bites: every one of these search
 * fields carries a `margin-top` for the case where it stands alone, so under
 * stretch the FIELD came out shorter than the button beside it by exactly that
 * margin. MEASURED on the photo module: the button reported 49px against the
 * field’s 48. Each older copy of this row zeroed the margin by hand, in a rule
 * naming its own field; the shared row says it once, about whatever it is given.
 */
/* The field takes the rest of the row — and gives up the margin it carries
 * for the case where it stands alone. `:first-child` rather than `*` is what
 * makes that stick: a bare `> *` ties with the field’s own single-class rule
 * and loses on sheet order, which is how the margin survived the first attempt
 * (MEASURED: row 60, button 60, field 48). */
.sf-sec-config__row > :first-child {
  flex: 1;
  margin-block: 0;
  min-width: 0;
}

.sf-sec-config__button {
  align-items: center;
  appearance: none;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  justify-content: center;
  padding: 0;
  /*
   * A SQUARE BY DEFAULT, STRETCHED WHEN IT HAS A FIELD TO MATCH.
   *
   * ─── THE WIDTH ─────────────────────────────────────────────────────────
   * It has to be declared. `aspect-ratio: 1` was tried first and does NOT
   * resolve an inline size from a STRETCHED cross size on a flex item —
   * MEASURED, the button came out about 10px wide and read as a black pill. 46
   * is the number the photo module’s own button carried; the answers section’s
   * 34 was the smaller of the two copies, and one pattern cannot keep both.
   *
   * ─── THE HEIGHT, WHICH IS RYAN’S DD-A FIX ──────────────────────────────
   * A square is right where the button stands ALONE — the answers section draws
   * it in its results head, beside a count, with no field to agree with. Beside
   * a FIELD it must be the field’s height exactly ("its height must match the
   * search field"), and a declared 46 would be the old bug in a new number the
   * moment a field is not 46 tall. So the row hands it back to `stretch`, one
   * rule below, and the two cases are one control.
   */
  height: 46px;
  width: 46px;
}

/*
 * IN A FIELD ROW, THE FIELD SETS THE HEIGHT. `height: auto` is what lets the
 * row’s `align-items: stretch` reach it — a declared height is not stretched —
 * so the button is exactly as tall as the box beside it at every width and in
 * every type size, which is the whole of what Ryan reported.
 */
.sf-sec-config__row .sf-sec-config__button {
  height: auto;
}

/* OPEN, IT INVERTS — the treatment the photo module already drew for this
 * exact button (`.sf-ph__config--on`, ruling PP1), carried here verbatim so the
 * three sections that now draw it draw the same one. */
.sf-sec-config__button--on {
  background: var(--brand-color-ink, #17181a);
  border-color: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
}

/* ==================================================== the AI embed box == */

/*
 * THE ANSWER SURFACE EVERY AI EMBED DRAWS (the shared-embed-core extraction,
 * 2026-08-28; Ryan's AI1-AI6, 2026-08-27).
 *
 * --- WHY IT IS IN THIS SHEET ------------------------------------------------
 *
 * Three surfaces draw it: the category comparison table, the PDP's answers
 * section and - new with ruling R5 - the PDP's reviews search. It arrived here
 * as two hand copies of one drawing (`.sf-cat-embed__convo...` in category.css
 * and `.sf-pdp-ans__convo...` in pdp.css, fourteen declarations each), which is
 * the same shape the section header and the pager were in before they were
 * promoted. The rule this sheet's own header states is that a shared sheet
 * earns a pattern when the SECOND surface wants it; this is the third.
 *
 * chrome.css is deliberately NOT the destination - the photo module's
 * promotion (photos.css, 2026-08-26) settled that a NEW shared block in a
 * SIBLING sheet is how a pattern is shared without touching the sheet that
 * carries the z-index ladder.
 *
 * --- WHAT IS RETIRED BY NAME, BY THIS BLOCK ---------------------------------
 *
 * category.css:  `.sf-cat-embed__convo`, `__convo:focus`, `__from-user`,
 *                `__bubble`, `__from-ai`, `__ai-mark`, `__answer`,
 *                `__answer-strong`, `__thinking`, `__dots`, `__dot`,
 *                `__dot:nth-child(2|3)`, `__actions`, `__continue`, `__clear`
 *                and `@keyframes sf-cat-embed-think`.
 * pdp.css:       the same list under `.sf-pdp-ans__`, plus
 *                `__answer-provenance`, `__convo-actions` and
 *                `@keyframes sf-pdp-ans-think`.
 *
 * The USER BUBBLE is retired by RULING, not by tidying - AI1: "the query STAYS
 * in the search box, never cleared, never re-bubbled". A question still sitting
 * in the field a few pixels above does not need a grey copy of itself below.
 *
 * --- THE MEASURE (AI3) ------------------------------------------------------
 *
 * "FAR NARROWER - chat-convo measure, for readability - and RIGHT-ALIGNED,
 * where the eye already is, beside the search field."
 *
 * 554px, and it is not a taste value: it is the CATEGORY ASK FIELD'S OWN
 * rendered width (`.sf-cat-embed__ask`'s `max-width`, itself the board's
 * content-box 520 plus its paddings and borders). Taking the field's measure
 * for the answer is what makes the pair read as one object - their right edges
 * are flush - and it lands the answer at roughly 60 characters of the body
 * face, inside the ~72 the token doc names as the readable ceiling and well
 * under the 1,312px card the answer used to run.
 *
 * A CAP, NEVER A WIDTH. In the answers rail (~360px) and on a phone the column
 * is narrower than the cap, so the box simply fills it - one declaration serves
 * a full-bleed section, a rail and a phone.
 */
:root {
  --sf-embed-measure: 554px;
}

/*
 * --- AI2: THE REVEAL --------------------------------------------------------
 *
 * "Animate the content DOWN, then FADE IN the answer box."
 *
 * TWO ELEMENTS, because an auto height cannot be animated on one. The outer is
 * a single-row grid whose track runs `0fr -> 1fr` - the one interpolation that
 * opens a box to its own content height - and growing it is literally what
 * pushes the table (or the list) down the page. The box's opacity then comes up
 * AFTER it, delayed by the reveal's own duration, so the two read as one
 * gesture in sequence rather than a fade racing a push.
 *
 * BOTH ARE `animation`, NOT `transition`, because the element MOUNTS in its
 * start state - a transition needs two frames of an element that already
 * exists, and `@starting-style` for the discrete case is a second mechanism to
 * keep true for one drawing.
 *
 * IT RUNS ONCE PER CONVERSATION, not once per question: React keeps this node
 * across a re-ask, so asking again swaps the words inside a box that is already
 * open - which is right, since AI1 leaves the reader looking at the same field.
 */
.sf-embed {
  animation: sf-embed-open 220ms ease forwards;
  display: grid;
  grid-template-rows: 0fr;
}

/*
 * THE CLIP LETS THE TRACK HOLD A TALLER BOX WHILE IT OPENS.
 *
 * `clip`, NOT `hidden` - build gotcha #1: `overflow: hidden` on an ancestor
 * kills `position: sticky` inside it, and the category embed's whole block is
 * sticky while a conversation is open. `min-height: 0` is the other half: a
 * grid item's automatic minimum size is its content, which would hold the track
 * open at full height from the first frame and animate nothing.
 */
.sf-embed__clip {
  min-height: 0;
  overflow: clip;
}

@keyframes sf-embed-open {
  to {
    grid-template-rows: 1fr;
  }
}

@keyframes sf-embed-fade {
  to {
    opacity: 1;
  }
}

/*
 * THE BOX (AI3): the site's card, at the ask field's measure, pushed to the
 * RIGHT of whatever column it is in.
 *
 * `margin-inline-start: auto` is the right-alignment, and it is the whole of
 * it: in a column narrower than the cap the margin resolves to zero and the box
 * fills the column, which is exactly what the rail and the phone want.
 */
.sf-embed__box {
  animation: sf-embed-fade 200ms ease 200ms forwards;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  margin-inline-start: auto;
  margin-top: 16px;
  max-width: var(--sf-embed-measure, 554px);
  opacity: 0;
  padding: 18px 22px;
}

/* Focus lands here on submit and the box is the DESTINATION, so it must not
 * also draw a UA ring around a whole panel - the reader put focus here
 * themselves by pressing Enter, and the reveal and the new words are the
 * feedback. */
.sf-embed__box:focus {
  outline: none;
}

/*
 * A READER WHO ASKED FOR LESS MOTION GETS THE BOX, INSTANTLY. Reduced motion is
 * the ABSENCE of both animations rather than a shorter version of them: the
 * track is open and the box is opaque on the first frame.
 */
@media (prefers-reduced-motion: reduce) {
  .sf-embed {
    animation: none;
    grid-template-rows: 1fr;
  }

  .sf-embed__box {
    animation: none;
    opacity: 1;
  }
}

/* THE ANSWER ROW: the star, then the answer - or, until it arrives, the "..."
 * in the answer's own place. That placement is the point (Ryan's original
 * complaint was that he never saw the indicator at all): nothing moves
 * sideways when the words replace the dots. */
.sf-embed__row {
  align-items: flex-start;
  display: flex;
  gap: 12px;
}

.sf-embed__mark {
  color: var(--brand-color-accent-strong, #9c420a);
  display: inline-flex;
  flex: none;
  margin-top: 3px;
}

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

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

/* The provenance line, on its own row under the answer - a block inside the
 * inline answer span, which is what puts it on its own line without a second
 * flex container around the pair. */
.sf-embed__provenance {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-caption);
  margin-top: 8px;
}

/* --------------------------------------------------------- the thinking -- */

/*
 * The "..." the answer arrives behind: three dots on the answer's own baseline,
 * in the hairline grey, pulsing in sequence. It carries a spoken label rather
 * than being announced as "dot dot dot", and the dots themselves are hidden
 * from the reading.
 */
.sf-embed__thinking {
  align-items: center;
  display: inline-flex;
  min-height: 22px;
}

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

.sf-embed__dot {
  animation: sf-embed-think 1.05s ease-in-out infinite;
  background: var(--brand-color-faint, #9a988f);
  border-radius: var(--radius-pill);
  height: 5px;
  width: 5px;
}

.sf-embed__dot:nth-child(2) {
  animation-delay: 0.15s;
}

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

@keyframes sf-embed-think {
  0%,
  80%,
  100% {
    opacity: 0.28;
  }

  40% {
    opacity: 1;
  }
}

/* A reader who asked for less motion gets a still row of dots, which still says
 * "waiting" - the state is drawn, only the pulse stops. */
@media (prefers-reduced-motion: reduce) {
  .sf-embed__dot {
    animation: none;
    opacity: 0.6;
  }
}

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

/*
 * --- AI4 + AI5: THE ACTION LINE ---------------------------------------------
 *
 * The forward action holds the RIGHT edge (AI4, and tokens section 3's
 * "step/flow Continue actions right-align"); the undo-ish action is a quiet
 * LINK at the left (AI5), on the same line, vertically centred with the button
 * and at the button's own font size.
 *
 * `justify-content: flex-end` PLUS `margin-inline-end: auto` on the link,
 * rather than `space-between`: two of the three surfaces have no quiet action
 * in some states, and a `space-between` row with one child left-aligns the
 * button - the button's position would depend on whether some other control
 * happened to be drawn. This way the right edge is the row's own rule and holds
 * in every state.
 */
.sf-embed__actions {
  align-items: center;
  display: flex;
  /*
   * IT WRAPS AT EVERY WIDTH, and that is a MEASURED rule rather than a mobile
   * one. The first cut put `flex-wrap` behind the phone media query and the
   * answers RAIL broke it at 1440: the rail's box is 360px, so its content run
   * is 316 against a 240.61px button plus a 16px gap plus a 110.69px link =
   * 367.3. The link squeezed to three lines beside the button.
   *
   * A viewport query cannot see that - the box that matters is the COLUMN, and
   * one of the three lives in a rail. Wrapping unconditionally costs nothing
   * where the pair fits (measured at 1440 on the category table: one line, the
   * button's right edge unmoved) and is the honest answer where it does not:
   * the quiet link takes the first line at the left, the ink button the second
   * at the right, which is the same reading order in one dimension instead of
   * two.
   */
  flex-wrap: wrap;
  gap: 16px;
  justify-content: flex-end;
  margin-top: 14px;
  row-gap: 10px;
}

/*
 * AI4: BLACK, WITH AN ICON. It was an ink-OUTLINED ghost on both surfaces -
 * the board's `.ghost` - and Ryan ruled the fill: this control does not buy
 * anything, it moves the reader on, and ink fill is the site's
 * wayfinding-primary (tokens section 1: "ink ... ink buttons"; section 3:
 * "primary buttons (accent = commerce, ink = wayfinding/secondary CTA)").
 *
 * A button inherits none of the page's type, so the family and size are written
 * out. `--text-small` is the size BOTH controls on this line wear - the
 * category embed's own 2026-08-27 ruling that the two are peers, carried
 * forward into the shared drawing.
 */
.sf-embed__continue {
  align-items: center;
  background: var(--brand-color-ink, #17181a);
  border: 1px solid var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  cursor: pointer;
  display: inline-flex;
  flex: none;
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-small);
  font-weight: 600;
  gap: 8px;
  padding: 10px 20px;
  text-align: center;
}

.sf-embed__continue-mark {
  align-items: center;
  display: inline-flex;
  flex: none;
}

/*
 * AI5: THE QUIET LINK. "Demoted to a light-coloured LINK, left-aligned inside
 * the box, the same font size as the button and vertically centred with it."
 *
 * `muted` is the token for ALL supportive text and the one the site underlines
 * (tokens section 1), so this is the house's quiet link rather than a new
 * treatment. The size is the button's, which is the half of the 2026-08-27 peer
 * ruling that survives the demotion: they are still one line, and a line whose
 * two ends are set at different sizes reads as a mistake.
 */
.sf-embed__quiet {
  background: none;
  border: 0;
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-small);
  margin-inline-end: auto;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
}

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

/* ------------------------------------------------ the box's hidden names -- */

/*
 * ONE VISUALLY-HIDDEN RULE FOR THE SHARED BOX. Its two consumers each had one
 * of their own - `.sf-cat-table__sr` and `.sf-pdp-sr` - and a shared drawing
 * that reached back into a page sheet for its clipped text would be a shared
 * drawing only until one of those sheets was not loaded.
 *
 * `clip-path` rather than the legacy `clip` rect, which is the treatment the
 * sibling sheets settled on.
 */
.sf-embed__sr {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/*
 * THE 44px TOUCH FLOOR, on the devices that have fingers.
 *
 * `(hover: none)` rather than a width, because the question is the INPUT and not
 * the screen: the button is 38px tall at every width (10px padding on a 13.5px
 * line, which is the size ruling AI4 kept from the ghost it replaced), and 38
 * is under the 44 this site holds every touch target to. A `min-height` rather
 * than more padding leaves the drawn type and the drawn gap exactly where they
 * are on a pointer device, where the recorded gates read them.
 */
@media (hover: none) {
  .sf-embed__continue {
    min-height: 44px;
  }
}

/* --------------------------------------------- MC3: the field's landing -- */

/*
 * ─── THE ASK FIELD LANDS A GUTTER BELOW THE CHROME (Ryan's MC3, 2026-08-29) ─
 *
 * "On focus, scroll so the field sits near the top with a buffer EQUAL to the
 * side gutters — 15px sides, 15px top. A uniform frame."
 *
 * ONE DECLARATION, AND THE PLATFORM DOES THE REST. `html` already carries
 * `scroll-padding-top: calc(--sf-pinned-offset + --sf-band-stuck-h)`
 * (chrome.css), so a `scrollIntoView({ block: "start" })` on this form lands it
 * flush under the whole pinned cascade — MEASURED at 131.00 on the category
 * page at 390. `scroll-margin-top` is the element's half of that machinery: it
 * pushes the landing down by exactly this much, so the field settles at 146.00
 * and the ruling's frame is uniform on three sides by construction rather than
 * by an arithmetic that would have to re-derive the cascade.
 *
 * THE GUTTER, READ RATHER THAN RESTATED. `--sf-gutter` is 15px at this cut and
 * is THE source of the number (chrome.css: "15px everywhere. Settled."), so a
 * future gutter ruling moves the top buffer with the side ones. The literal in
 * the fallback is for a field drawn outside a `.sf-container`, which none is
 * today.
 *
 * SCOPED TO THE PHONE CUT, like the scroll that reads it: above it the field is
 * already near the top of a viewport no keyboard is shortening, and the lift
 * would be motion for nothing. `embed-core-field.tsx` gates the SCROLL on the
 * same cut; this is the drawing half of one behaviour and the two must agree.
 */
@media (max-width: 767.98px) {
  .sf-embed-field {
    scroll-margin-top: var(--sf-gutter, 15px);
  }
}

/* ------------------------------------------------ FH2: the field hold ---- */

/*
 * ─── SCROLL ANCHORING STANDS DOWN WHILE A FIELD IS HELD (Ryan's FH2) ───────
 *
 * Ryan, 2026-08-29: "once focus lifts the field, its viewport position is
 * PINNED until the USER manually scrolls." Point 4 of the ruling is this rule:
 * "`overflow-anchor: none` on the scroller (scoped to the held state) so
 * anchoring never fights the hold."
 *
 * WHAT ANCHORING DOES, AND WHY IT IS AN ENEMY HERE. When content above the
 * viewport changes height the browser picks an anchor node near the top of the
 * view and silently adjusts `scrollY` to keep it still. That is the RIGHT
 * behaviour for a page nobody is holding — WP2 measured it absorbing the
 * answer box's 241px exactly — and it is the wrong one for a page a reader is
 * holding by the field, because the node it chooses is not the field. The hold
 * would then be correcting the browser's correction, one frame behind, forever.
 *
 * SCOPED TO THE HELD STATE, AND NOWHERE ELSE. `use-field-hold.ts` sets
 * `data-sf-field-hold` on `<html>` for exactly as long as a hold is on — one
 * attribute, ref-counted across the four field surfaces — and removes it on
 * release. Every other page, and this page in every other state, keeps
 * anchoring.
 *
 * BOTH ELEMENTS, deliberately: `overflow-anchor` is not inherited, and the
 * document's anchor node is selected from the scrolling box's subtree, so the
 * opt-out is declared on the root AND on the body it actually contains rather
 * than assuming which one a browser treats as the scroller.
 */
:root[data-sf-field-hold],
:root[data-sf-field-hold] body {
  overflow-anchor: none;
}

/* ------------------------------------- WP3: the send arrow and the count -- */

/*
 * ─── THE SEND ARROW, INSIDE THE FIELD (Ryan's WP3, 2026-08-29) ─────────────
 *
 * "A visible submit button inside the field's right end, the chat-ask arrow
 * idiom, beside/left of the X — the fields submit on Enter and nothing says
 * so."
 *
 * IT IS THE X's TWIN, and drawn as one: `.sf-field-clear` in chrome.css is an
 * absolutely-positioned bare button at `right: 8px`, so this is the same shape
 * one slot further in. Absolute rather than a flex item for the X's own reason
 * — the field row is the surface's board geometry and a new item in it would
 * move every recorded pixel.
 *
 * THE OFFSET IS MEASURED, AND IT HAS TWO RUNGS — this pass's own gate found
 * the second one. The X is 6px padding either side of a 20px glyph at the wide
 * cuts (~27px, spanning 8..35 from the right edge), and chrome.css gives it
 * `min-width: 44px` BELOW 767.98 for the touch floor, where it spans 8..52. A
 * single offset overlapped it on the phone: measured at 390, the arrow sat at
 * 311..338 under an X at 326..370. So the arrow clears 35 at the wide cuts and
 * 52 at the phone cut, and its own touch floor comes from the same rule and
 * the same cut the X's does.
 *
 * THE RESERVE GROWS ONLY WHEN THE BUTTONS ARE THERE. The three fields each
 * declare a 44px right padding for the X (their own sheets say so), and two
 * buttons need 76. Widening that padding unconditionally would shorten every
 * field's placeholder measure at rest and move three recorded gates for a
 * control the reader cannot see yet — so the extra is `:has()`-gated on the
 * arrow, which is itself drawn only on a field with text. The resting drawing
 * is byte-identical; the typed one has room.
 *
 * `:has(.sf-field-send)` also OUT-SPECIFIES the surfaces' own padding without
 * an `!important` and without depending on sheet order: it is (0,2,0) against
 * their (0,1,0).
 */
.sf-field-send {
  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;
  justify-content: center;
  line-height: 1;
  padding: 6px;
  position: absolute;
  right: 40px;
  top: 50%;
  transform: translateY(-50%);
}

.sf-field-send:hover {
  color: var(--brand-color-accent, #c4530a);
}

.sf-embed-field:has(.sf-field-send) {
  padding-right: 76px;
}

/*
 * THE 44px TOUCH TARGET (Ryan's WP3: "44px touch at the phone cut").
 *
 * THE X'S OWN RULE, to the cut and to the property: chrome.css grows
 * `.sf-field-clear` to `min-width: 44px; min-height: 44px` at
 * `max-width: 767.98px`, and this is that rule for its sibling. The first
 * version reached for `(hover: none)` — the question is the input rather than
 * the screen, which is the reasoning `.sf-embed__continue` records — and it was
 * wrong HERE for a measured reason: the two buttons have to clear each other,
 * and one of them changes width on a WIDTH query. Two controls in one row
 * cannot answer to two different media questions.
 *
 * AND THE ARROW MOVES OUT with the X it is clearing (8 + 44), with 4px between
 * the two targets. The field's reserve grows to match: 56 + 44 + 4.
 */
@media (max-width: 767.98px) {
  .sf-field-send {
    min-height: 44px;
    min-width: 44px;
    right: 56px;
  }

  .sf-embed-field:has(.sf-field-send) {
    padding-right: 104px;
  }
}

/*
 * ─── THE RESULT-COUNT LINE (Ryan's WP3) ────────────────────────────────────
 *
 * "After any run, every surface shows the result-count line — silence is never
 * unambiguous." One drawing, placed by whichever surface renders
 * `EmbedFieldStatus`; the sentence is the surface's own.
 *
 * IT RESERVES NO HEIGHT WHILE EMPTY and takes its margin only when it has
 * something to say, which is the checkout toast slot's own arrangement: the
 * announcement point is always in the DOM (a live region that mounts with its
 * message is a live region some readers never announce) and the LAYOUT arrives
 * with the words.
 */
.sf-embed-field__status {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin: 0;
}

.sf-embed-field__status:not(:empty) {
  margin-top: 8px;
}

/* ================================================== WP9: the toggle ====== */

/*
 * ─── THE SITE'S EITHER/OR CONTROL, AUTHORED HERE (Ryan's WP9, 2026-08-29) ──
 *
 * "Use the site's TOGGLE/segmented control — one selected always, radio
 * semantics under segmented clothes."
 *
 * IT IS NEW VOCABULARY, and that is recorded rather than glossed. The ancestor
 * Ryan remembered is real and is GONE: photos.css still carries the note that
 * the board's "black three-way segmented toggle" was RETIRED by ruling P4 —
 * `.sf-ph__segment`, the clipped group, the inter-rung hairlines and the inset
 * ring went with it, replaced by the quiet dropdown because "that black is too
 * much". Nothing segmented survived. So this is the grammar being written, in
 * the shared section sheet, for every either/or after it.
 *
 * WHY A TRACK AT ALL, given that pdp.css explicitly refused one ("a track would
 * be a second selection language on a page that already has one"): because the
 * thing it refused a track FOR was two chips with a gap, and Ryan's report is
 * that exactly that reads as multi-select. A gap says "these are separate
 * things you may each pick"; a shared track says "these are the two halves of
 * one answer". That earlier note is superseded by name.
 *
 * AND IT IS NOT THE CHIP. The chip's selection language is the platform's 1.5px
 * accent border (tokens §1) and it stays the chip's: a chosen SEGMENT is marked
 * by lifting out of the track onto the card's own `surface`, which is the
 * segmented idiom and reads at a glance without a second accent on a page that
 * already spends one on the buy CTA.
 */
.sf-seg {
  display: inline-flex;
}

.sf-seg__track {
  background: var(--brand-color-paper, #fbfbfa);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  display: inline-flex;
  /* The inset the chosen segment lifts out of. */
  padding: 2px;
}

.sf-seg__option {
  align-items: center;
  background: none;
  border: 0;
  /* The track's radius less its own inset, so the two curves are concentric. */
  border-radius: calc(var(--radius-control) - 2px);
  color: var(--brand-color-ink-soft, #44464a);
  cursor: pointer;
  display: inline-flex;
  font-family: inherit;
  font-size: var(--text-support);
  gap: 7px;
  /* 5px against the retired chip's 5px, so the row's height does not move. */
  padding: 5px 11px;
  white-space: nowrap;
}

/*
 * THE CHOSEN SEGMENT LIFTS. A `surface` fill on the `paper` track plus one
 * hairline shadow — the same 1px lift the site's other raised-on-paper boxes
 * take. NO BORDER: a border here would add a pixel to the segment and shove its
 * sibling, which is the defect a track exists to avoid.
 */
.sf-seg__option--on {
  background: var(--brand-color-surface, #ffffff);
  box-shadow: 0 1px 2px rgb(23 24 26 / 10%);
  color: var(--brand-color-ink, #17181a);
  font-weight: 600;
}

/* The figure inside a segment — "With photos 916". Quiet, because it is the
 * reason to pick the segment and not the segment's name. */
.sf-seg__count {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  font-weight: 400;
}

/*
 * THE 44px TOUCH FLOOR at the phone cut, asked of the INPUT rather than of the
 * screen — the same `(hover: none)` rule the send arrow and the embed's
 * continue button take, for the same reason: the question is the pointer.
 */
@media (hover: none) {
  .sf-seg__option {
    min-height: 44px;
  }
}

/* ------------------------------------------- WP13: the pager's landing -- */

/*
 * THE BUFFER A PAGE CHANGE LANDS ON (Ryan's WP13, 2026-08-29).
 *
 * The researched standard scrolls the list's top under the pinned stack on a
 * page change (`section-pager.tsx` carries the sources), and MDN's own
 * `scroll-margin-top` note is the sticky-header half of it. The pager READS
 * this declaration back off the target element rather than carrying a number,
 * which is the same read-back the ask field's landing uses — so the two
 * surfaces' buffers are one ruling in one place, and a future gutter ruling
 * moves both.
 *
 * IT IS A SHARED CLASS, and it has to be: this sheet may only declare its own
 * `sf-sec-` prefix (the partition `category-page.test.ts` enforces, so that no
 * two sheets can claim one name), and the three targets are a reviews list, an
 * answers list and a photo grid in three other namespaces. The class is
 * therefore what the three have in COMMON — "this is a pager's landing" — and
 * it rides beside each list's own class exactly as `.sf-embed-field` rides
 * beside each field's.
 */
.sf-sec-pager-target {
  scroll-margin-top: var(--sf-gutter, 15px);
}
