/*
 * Category-page stylesheet — the annotated product hero.
 *
 * Served as a static asset and pulled in by `<CategoryStyles>` rather than
 * `import`ed, for exactly the reasons given at the top of chrome.css: a bundled
 * import attaches the chunk to the shared `[brand]` layout segment and Next then
 * emits a <link> to it on every brand's every page. A hoisted <link> to a public
 * asset is rendered only by the page that needs it.
 *
 * Transplanted from design/produnk/CategoryPage.dc.html (desktop, drawn at 1440)
 * and design/produnk/CategoryMobile.dc.html (drawn at 390), mechanically
 * tokenised: colours and typefaces read `--brand-*`, sizes and radii read the
 * platform constants in globals.css. Board values ship verbatim — the odd
 * numbers (78px, 5.5px, -95px) are measured, not sloppy.
 *
 * Class prefix `sf-cat-`.
 *
 * The sheet dresses the hero and the comparison table with its AI conversation
 * embed.
 *
 * THE SECTION HEADER AND THE REVIEW BAR LEFT THIS SHEET on 2026-08-26 — both
 * are PLATFORM patterns (tokens §4) that had accumulated copies in three sheets,
 * and both now live in `public/storefront/sections.css` under the `sf-sec-`
 * prefix, rendered by `<SectionStyles>` from the components that draw them. The
 * review bar's two placements ride a modifier (`--stage` here, `--band` on the
 * homepage); the AI embed's two SCOPED overrides of the header stay in this
 * sheet, because they are this page's arrangement rather than the pattern.
 *
 * THE CUSTOMER-PHOTO SECTION LEFT THIS SHEET on 2026-08-26. It was the PDP's
 * photo-search panel drawn at its category level, and it is now the shared
 * module: `public/storefront/photos.css` under the `sf-ph-` prefix, rendered by
 * `src/components/storefront/photo-panel.tsx` at whichever of §4's three levels
 * a surface asks for. The relocation is proved by `tools/phrelo.mjs` and by
 * p4gate's five photo regions holding their recorded numbers.
 */

/* ------------------------------------------------------- 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).
 *
 * It is written here rather than beside each component on purpose: the ruling is
 * that the ring is a SITE-WIDE rule, and a rule scattered across fourteen
 * component blocks is fourteen places for the next control to be missed. New
 * control in this sheet: add its class to the group whose ground it lands on.
 *
 * The `var()` fallbacks repeat the platform values exactly once each, so this
 * sheet still draws a compliant ring if it is ever loaded without chrome.css.
 */

/* --- on the light grounds: paper, surface, well --- */
.sf-cat-hero__orb-more:focus-visible,
.sf-cat-table__head-cta:focus-visible,
.sf-cat-table__head-link:focus-visible,
.sf-cat-table__compact-cta:focus-visible,
.sf-cat-table__cta:focus-visible,
.sf-cat-table__mcta:focus-visible,
.sf-cat-table__explain-link:focus-visible,
.sf-cat-table__review-count:focus-visible,
/* `.sf-cat-embed__continue` AND `.sf-cat-embed__clear` LEFT THIS ROSTER (the
 * shared-embed-core extraction, 2026-08-28) with the buttons they ringed. The
 * answer box's two actions are `.sf-embed__continue` and `.sf-embed__quiet`
 * now, and their ring is declared once, in `sections.css`'s own focus block,
 * beside the configuration button's. */
/*
 * THE AI FIELD DELEGATES ITS RING TO THE FIELD, not to the input inside it —
 * the same reasoning that already moved its accent border there (see the
 * `:focus-within` block below): the input is a bare borderless box inset from
 * the drawn control by its padding and its magnifier, so a ring on the input
 * wraps the text rather than the control the reader is looking at.
 *
 * `:has(… :focus-visible)` rather than the border's `:focus-within` so the RING
 * follows the platform rule exactly. For a text field the two match together in
 * practice — `:focus-visible` matches a focused text input however it was
 * focused — but the selector says which rule it is obeying, and the day this
 * wrapper holds a non-text control it will still be right.
 */
.sf-cat-embed__ask:has(.sf-cat-embed__ask-input:focus-visible),
/*
 * THE HERO'S FILM SLOT DELEGATES ITS RING TO THE DRAWN BOX, for the same kind
 * of reason and a different mechanism: `.sf-cat-hero__film-box` CLIPS its
 * children (`overflow: hidden`, for the rounded media corners), so an outward
 * outline on the tap control inside — which covers the whole rectangle, edge to
 * edge — is cut off on all four sides. The box is the clipper and its own
 * outline is not clipped. The ancestor walk in `tools/focusgate.mjs` is built
 * to follow exactly this.
 *
 * IT WAS `.sf-cat-hero__film` UNTIL 2026-08-27, when the slot became two
 * elements: an unclipped wrapper that holds the slot's place in the copy column
 * and the drawn box that grows out of it (Ryan's F1, the "bigger" state). The
 * ring belongs to the box in both states — it is the thing with the corners,
 * and it is the thing the reader is looking at when the film is expanded.
 *
 * The accent ring rather than the media pair, because this outline lands on the
 * hero's white stage — the footage is INSIDE the box it is drawn around.
 */
.sf-cat-hero__film-box:has(.sf-cat-hero__film-tap:focus-visible) {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * --- drawn inside: the full-bleed scroller ---
 *
 * Chrome makes a scroll container keyboard-focusable so it can be scrolled
 * without a pointer, so this IS a tab stop and needs the ring. 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, so this one is drawn inside.
 *
 * THE REVIEW BAR'S QUOTE STRIP was the second of these until 2026-08-26; it left
 * with the pattern, and its ring is declared in sections.css beside it.
 */
.sf-cat-table__mobile:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-inset, -2px);
}

/*
 * --- on the photograph: the hero's annotations and tap orbs ---
 *
 * These four sit on the product photo, which has no known adjacent colour, so
 * they take the ink-ring-plus-white-halo pair (chrome.css, MEDIA). Each already
 * carries a drop shadow at rest and `box-shadow` replaces rather than adds, so
 * the halo is composed with the shadow the control draws — listed FIRST, since
 * the first shadow in the list paints on top and the crisp halo has to sit over
 * the blur rather than under it.
 */
.sf-cat-hero__floaty-card:focus-visible,
.sf-cat-hero__la:focus-visible,
.sf-cat-hero__orb:focus-visible,
/*
 * THE FILM CLUSTER'S TWO BUTTONS SIT ON THE FOOTAGE — a moving ground, which is
 * the strongest possible case for the pair: there is no adjacent colour to
 * check contrast against, because it changes 30 times a second.
 *
 * They ring OUTWARD even though they live inside the clipping box, which the
 * tap control above cannot: they are inset from the box's edges by more than
 * the ring and its offset, so the whole indicator is drawn inside the clip.
 */
.sf-cat-hero__film-btn:focus-visible {
  outline: var(--sf-focus-ring-on-media, 2px solid var(--brand-color-ink, #17181a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

.sf-cat-hero__floaty-card:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff)),
    0 6px 18px rgba(23, 24, 26, 0.1);
}

.sf-cat-hero__la:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff)),
    0 10px 26px rgba(23, 24, 26, 0.13);
}

.sf-cat-hero__orb:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff)),
    0 4px 14px rgba(23, 24, 26, 0.22);
}

/* The open orb draws a deeper shadow; its halo composes with that one. */
.sf-cat-hero__orb[aria-expanded="true"]:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff)),
    0 4px 14px rgba(23, 24, 26, 0.3);
}

/* The cluster's pills carry a lift shadow so they read off the footage; the
 * halo composes with it, first in the list so the crisp ring paints over the
 * blur. */
.sf-cat-hero__film-btn:focus-visible {
  box-shadow: var(--sf-focus-halo, 0 0 0 4px var(--brand-color-surface, #ffffff)),
    0 2px 10px rgba(11, 12, 14, 0.36);
}

/* ------------------------------------------------------------------ hero -- */

.sf-cat-hero {
  /*
   * HOW FAR THE REVIEW BAND IS PULLED UP INTO THIS SECTION, stated here so the
   * copy column can reserve it (see `.sf-cat-hero__copy`).
   *
   * IT MIRRORS A NUMBER THIS SHEET DOES NOT OWN: `.sf-sec-reviews--stage` in
   * `public/storefront/sections.css` sets `margin-top: -95px`, which is the
   * board's own measurement — the band's top lands on the bottom of the hoop's
   * cast shadow. That declaration is the platform's; this is the page's reading
   * of it, and the two are held together by `tools/filmgate.mjs`, which reads
   * both computed values off the live page and fails if they disagree. Same
   * device as `EXPAND_MS` in `category-hero-film.tsx`: a number in two places
   * is allowed when a gate is the thing that keeps them equal.
   */
  --sf-hero-review-lift: 95px;
  /*
   * THE AIR BETWEEN THE LAST KEY POINT AND THE REVIEW BAND (Ryan's FB-v3a,
   * 2026-08-28), and it is a SECOND reservation on top of the lift above.
   *
   * WHAT IT RESOLVES, by name: the FH3 pass closed the collision — the band no
   * longer lands ON the bullets — but it closed it to EXACTLY zero. The
   * reservation is the lift, so in the worst case the band's top edge and the
   * copy column's bottom edge are the same line, and what saved it visually was
   * the last line box's own descender air (~14px). That was flagged as
   * "zero-clearance by construction" rather than fixed, with the cost of fixing
   * it priced on the spot; Ryan ruled for the air on 2026-08-28 and accepted
   * the cost.
   *
   * ─── FX2: 24px WAS STILL TOO TIGHT IN THE FLESH (Ryan, 2026-08-28) ───────
   *
   * "STILL not enough space between the expanded-video bullets and the review
   * band." The 24 came off the COLUMN'S OWN STACK — 28 above the film, 32 above
   * the points, 16 between two points — and that was the error in it, not the
   * arithmetic: this gap is not another step inside the column, it is the seam
   * between two SECTIONS that happen to share one white ground. A gap the same
   * size as the one between two bullets tells the reader the band is the next
   * bullet. Ryan's ruling names the scale to take it from.
   *
   * SO IT IS THE SECTION RHYTHM'S LOWER RUNG, and it is the token rather than
   * its value. `--space-section-mobile` is 56px; the name says where the scale
   * uses it by default and not what it means, and what it means is "a section
   * boundary, at the tighter of the two rungs" — which is exactly this seam,
   * inside a composition whose own top and bottom padding are 44 and 64. The
   * full `--space-section` (96px) is the rung between two independent bands and
   * would read as a dropped section here.
   *
   * 48 WAS THE OTHER CANDIDATE AND WAS MEASURED, not argued away: it costs 8px
   * less document at every bound width and it is not on the scale — the nearest
   * token to it is this one. Both were rendered expanded at 1440 and 1240 for
   * Ryan before the pick.
   *
   * THE COST, WHICH IS AT REST AND NOT IN THE EXPANDED STATE. The row is
   * `max(copy, stage)`, so this grows the SECTION only where the copy column is
   * already within `lift + buffer` of the stage. RE-MEASURED per rung on the
   * live page at 24 / 48 / 56 (the 48 column is the road not taken):
   *
   *     width   at 24     at 48        at 56        at-rest gap at 24
   *     1440    baseline  +0     +0    +0     +0    163.01px of slack
   *     1240    baseline  +24   +24    +32   +32    24.00 — already binding
   *     1120    baseline  +24   +24    +32   +32    24.00
   *     1024    baseline  +24   +24    +32   +32    24.00
   *      900    baseline  +24   +24    +32   +32    24.00
   *      800    baseline  +24   +24    +32   +32    24.00
   *                       (docH  section)  (docH  section)
   *
   * 1440 IS FREE AT REST because the stage is still the taller column there by
   * 163px, so nothing this reserves has reached it — which is why `p4gate`'s ten
   * recorded regions reproduce EXACTLY at both of its capture widths (1440 and
   * 390, the latter being where this column is `display: contents` and has no
   * box to pad). Everything from 1240 down grows by the full 32px difference,
   * at rest, and that is the price Ryan accepted a second time.
   *
   * EXPANDED — the state Ryan is looking at — the gap IS this number at every
   * width, because the spacer takes the copy column past the stage everywhere.
   * `tools/filmgate.mjs` asserts that, at rest and expanded, at five widths, and
   * reads the value off the live page rather than repeating it.
   */
  --sf-hero-review-buffer: var(--space-section-mobile, 56px);
  background: var(--brand-color-surface, #ffffff);
  /* As in the chrome: 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 every floaty card and the film's mono captions. */
  line-height: normal;
  padding: 44px 0 64px;
}

/*
 * The board's grid is `1fr 860px` inside a 1312px measure with a 24px gap, so
 * the copy column is 1312 − 860 − 24 = 428px. Written as `428fr 860fr` the ratio
 * is identical at 1440 and the composition SCALES instead of collapsing: with a
 * fixed 860px track the copy column is squeezed to nothing well before the
 * mobile cut, and there is no board between 390 and 1440 to say what should
 * happen instead.
 *
 * Everything the stage positions is a percentage of the stage for the same
 * reason — the anchors have to travel with it. Each one is its board pixel over
 * the board's 893 x 1080 stage, carried to full precision so that at 1440 it
 * resolves back to the drawn pixel and not to a rounded neighbour.
 */
.sf-cat-hero__grid {
  align-items: start;
  column-gap: 24px;
  display: grid;
  /* `minmax(0, …)`, not a bare `fr`: an `fr` track's automatic minimum is its
   * MIN-CONTENT, and the stage holds a 900px-wide photograph, so a bare `860fr`
   * is floored at 900 and steals 40px from the copy column at every width. */
  grid-template-columns: minmax(0, 428fr) minmax(0, 860fr);
}

/*
 * THE COLUMN RESERVES THE REVIEW BAND'S PULL AT ITS FOOT (Ryan's FH3).
 *
 * THE BUG, measured rather than reasoned about: the band under this grid is
 * pulled up by 95px, and what that 95px is SUPPOSED to eat is the white under
 * the hoop — the stage column's own slack, which is where the board draws the
 * cast shadow the band's top is meant to land on. But the row is `align-items:
 * start` and its height is `max(copy, stage)`, so the moment the COPY column
 * becomes the taller of the two the grid's bottom edge IS this column's bottom
 * edge, there is no slack left to eat, and the band is pulled 95px onto the key
 * points. The reviews heading and the 4.91 are then drawn straight through the
 * bullet list.
 *
 * IT WAS NEVER ONLY AN EXPAND BUG, which is what made it worth root-causing.
 * Ryan reported it after pressing Expand — and expanding does make it happen at
 * every width, because the spacer adds ~380–500px to this column and takes it
 * past the stage everywhere. But the same collision was ALREADY on the page at
 * rest at every width at and below 1120, where this column outgrows the stage
 * on its own. Measured at rest, before a line of this was written:
 *
 *     width   copy bottom   stage bottom   band top   points bottom
 *     1440      1133.22        1401.00      1306.00      1133.22   clear 172.78
 *     1240      1119.83        1233.30      1138.30      1119.83   clear  18.47
 *     1120      1144.73        1132.67      1049.73      1144.73   OVER   95.00
 *      768      1344.44         926.17      1249.44      1344.44   OVER   95.00
 *
 * So the fix is unconditional rather than scoped to `[data-big]`: a state-scoped
 * one would have left the resting page broken at three of the gate's five
 * widths (1024, 900 and 800) and at every width below 1120 besides.
 *
 * WHY A PADDING AND NOT A CEILING ON THE PULL. The pull is right; what was
 * missing is that this column never declared how much room it needs under its
 * last line. Reserving the lift means the row can never end higher than 95px
 * below the key points, so the band's top lands at this column's bottom edge in
 * the worst case and on the stage's slack in every other — which is the
 * relationship the board draws, now true of both columns instead of one. Since
 * FB-v3a the reservation is the lift PLUS the ruled buffer, so the worst case
 * is not a touching edge but a measured 24px of air.
 *
 * IT STILL COSTS NOTHING AT REST WHERE THE STAGE IS TALLER BY MORE THAN THE
 * RESERVATION, and that is arithmetic rather than hope: the row only grows when
 * `copy + 95 + 24` passes the stage, which at 1440 needs 267.78px of growth and
 * at 1240 needs 113.47px — so 1440 is untouched and 1240 grows by 5.53px, which
 * is the cost Ryan priced and accepted. The recorded desktop geometry is
 * untouched at the widths that ARE recorded: `tools/p4gate.mjs` captures at 1440
 * and 390 and reproduces all ten of its regions exactly, at both thresholds,
 * `hero` included; `tools/filmgate.mjs` is what measures the rungs in between.
 *
 * AND IT IS INERT ON MOBILE WITHOUT A MEDIA QUERY: below 767.98px this column
 * is `display: contents` (see the mobile block), so it has no box to pad — and
 * the band's margin is `+52px` there anyway, so there is nothing to reserve.
 */
.sf-cat-hero__copy {
  /* The lift the band takes back, PLUS the air Ryan ruled between the last key
   * point and it (FB-v3a) — see both custom properties above. The two are one
   * declaration because they are one question: how much room does this column
   * need under its last line. */
  padding-bottom: calc(var(--sf-hero-review-lift) + var(--sf-hero-review-buffer));
  /* Drops the copy to rim height on the photo beside it. */
  padding-top: 78px;
  position: relative;
  /* Gotcha #5: an <img> in a grid column paints over its siblings' shadows, and
   * the stage's photo overlaps this column's measure. */
  z-index: 2;
}

.sf-cat-hero__series {
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-display-lg);
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1;
  margin: 0;
  /* `wrap` — the initial value, which is what turns off globals.css's `balance`
   * on every h1/h2/h3. This read `normal` until 2026-08-26; `normal` is
   * `white-space`'s keyword, not `text-wrap`'s, so it was dropped at parse time
   * and `balance` stayed in force. The series name is one word today, where the
   * two algorithms cannot differ. */
  text-wrap: wrap;
}

.sf-cat-hero__lede {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-emphasis);
  line-height: 1.6;
  margin-top: 20px;
  max-width: 460px;
}

/* ------------------------------------------------------------ film well -- */

/*
 * THE WRAPPER HOLDS THE SLOT'S PLACE, AND ITS SHAPE IS THE FILM'S OWN.
 *
 * A 16:9 BOX REPLACED A FIXED 250px HEIGHT on 2026-08-27 (Ryan's F3), and the
 * bug it fixes was measured rather than eyeballed: the box drew 428 x 250 in
 * the copy column, a ratio of 1.712 against the 1.7778 the player was fitting
 * into it, so it drew 4.6px of black along the top and the bottom. The poster
 * state had none, which is what made it look like a playing bug rather than a
 * box bug. The ratio is declared once and the height follows the column, so no
 * width can reintroduce a bar. The still well is 240.75px tall at 1440 and
 * 202.5 at 390, and the `hero` region in `tools/p4gate.mjs` was re-recorded for
 * it.
 *
 * THE RATIO IS NOW DECLARED ON THE SPACER RATHER THAN ON THIS ELEMENT, and the
 * height it produces is identical — `--sf-film-w` is `100%` at rest, so the
 * spacer is the wrapper's own width at 16:9, which is what `aspect-ratio` on
 * the wrapper computed. It moved because of Ryan's FB2 (below): the wrapper has
 * to GROW with the film now, and a wrapper whose height comes from a child can
 * animate that growth while a wrapper whose height comes from its own ratio
 * cannot (the ratio itself would have to change, and the target ratio is not a
 * constant — see the arithmetic on `--sf-film-w`).
 */
.sf-cat-hero__film {
  /*
   * THE FILM'S WIDTH, AND THE ONE THING THE "EXPAND" STATE CHANGES.
   *
   * Two elements are sized from it — the drawn box, which is what the reader
   * sees, and the spacer, which is what the COLUMN sees — so they cannot drift
   * apart and there is one place to read the geometry from.
   */
  --sf-film-w: 100%;
  margin-top: 28px;
  max-width: 460px;
  position: relative;
}

/*
 * THE SPACER: the film's place IN THE COPY COLUMN'S FLOW.
 *
 * IT IS WHAT MAKES RYAN'S FB2 TRUE ("the bullet points push down"). Until
 * 2026-08-27 the wrapper was a fixed-height box and the drawn film floated out
 * of it over everything, which is still exactly right for the STAGE PHOTOGRAPH
 * beside it — that is in the other grid column and must not move — but was
 * wrong for the key points UNDER it, which are in this column and now make room.
 * So the film is in flow for its own column and out of flow for the page: one
 * element holds each half of that, which is the only way to have both.
 *
 * It draws nothing. It is not `visibility: hidden` and has no `content` beyond
 * the empty string, because there is nothing to hide — a pseudo-element with no
 * background and no text is already invisible, and giving it a hidden something
 * would be one more thing to be wrong about.
 *
 * IT OVERFLOWS THE WRAPPER HORIZONTALLY in the expanded state, by exactly as
 * much as the drawn box does, and to exactly the same edge — the page's content
 * right edge, never past it. `tools/overflow.mjs` is the proof of that, and it
 * is the same proof the box has needed since the state existed.
 */
.sf-cat-hero__film::before {
  content: "";
  display: block;
  width: var(--sf-film-w);
  aspect-ratio: 16 / 9;
}

/*
 * THE DRAWN SLOT: the rounded, clipping, shadowed rectangle the board draws.
 *
 * Absolutely positioned over the wrapper rather than filling it in flow, so
 * that it can cover the stage photograph in the expanded state without the
 * grid's other column knowing anything about it.
 *
 * `top: 0; left: 0` + a width + the ratio, rather than `inset: 0`: the box is
 * the same rectangle either way at rest, but only a WIDTH can be interpolated,
 * and FB1 asks for the change to be animated.
 */
.sf-cat-hero__film-box {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-media);
  box-shadow: 0 14px 36px rgba(23, 24, 26, 0.22);
  left: 0;
  overflow: hidden;
  position: absolute;
  top: 0;
  width: var(--sf-film-w);
}

.sf-cat-hero__film-well {
  align-items: center;
  background: linear-gradient(
    160deg,
    var(--brand-color-media-well-a, #23262e),
    var(--brand-color-media-well-b, #0b0c0e)
  );
  display: flex;
  height: 100%;
  justify-content: center;
  position: relative;
}

.sf-cat-hero__film-label {
  color: var(--brand-color-placeholder-on-dark, #5a5d66);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  letter-spacing: 0.12em;
  padding: 0 18px;
  text-align: center;
}

.sf-cat-hero__film-cue {
  align-items: center;
  bottom: 12px;
  display: flex;
  gap: 9px;
  left: 14px;
  position: absolute;
}

.sf-cat-hero__film-play {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border-radius: var(--radius-pill);
  color: var(--brand-color-ink, #17181a);
  display: flex;
  height: 32px;
  justify-content: center;
  width: 32px;
}

/* `margin-left` optically centres the triangle in its circle. */
.sf-cat-hero__film-play svg {
  height: 12px;
  margin-left: 1px;
  width: 12px;
}

.sf-cat-hero__film-cue-label {
  color: rgba(255, 255, 255, 0.85);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
}

/*
 * --- the film itself, over the well ---
 *
 * `sf-cat-hero__film-embed` — THE CROSS-ORIGIN <iframe> THIS RULE USED TO
 * DRESS — IS RETIRED BY NAME (written here without its leading dot on purpose:
 * the orphan sweep in `tests/category-page.test.ts` reads this sheet without
 * stripping comments, so a retired selector spelled as a selector reads as a
 * rule that matches no markup) (Ryan's FA, 2026-08-27). The slot serves its own
 * file now; see `category-hero-film.tsx` for the four seconds of phone chrome
 * that ended the embed and `tools/fetch-film.mjs` for what replaced it.
 *
 * THE WELL IS NEVER REPLACED, it is COVERED. The placeholder is what the server
 * sends, what a reader without script keeps, and what `prefers-reduced-motion:
 * reduce` holds; the film is laid over it only while the slot is on screen. So
 * the slot at rest is the same box of pixels the board draws, and nothing in
 * the rules above had to move.
 *
 * `object-fit: cover` NO LONGER CROPS ANYTHING, and the compromise it used to
 * be is RETIRED BY NAME (Ryan's FG, 2026-08-27).
 *
 * THE 3:2 CROP COMPROMISE — "the master is 720 x 480, display aspect 3:2, so
 * this 16:9 box centres the picture and loses 7.8% off the top and the bottom,
 * and `aspect-ratio: 3/2` plus `contain` is the reversible alternative" — was
 * REASONING FROM THE WRONG FILE. So was the finding under it, that YouTube's
 * copy is "the whole 3:2 frame SQUASHED into 16:9": that measurement correlated
 * ROW profiles, and a row profile survives a horizontal crop exactly as it
 * survives a vertical squash, so it could not tell the two apart. A 2D grid
 * correlation can, and does: the 720 x 480 files were a CENTRE CROP of a true
 * 16:9 master (crop 0.954 / 0.977 vs squash 0.918 / 0.955, per-frame best match
 * over both halves — `tools/fetch-film.mjs`), made narrow on purpose for the
 * legacy homepage's background loop. Nothing was ever squashed and nobody was
 * ever 18.5% too wide.
 *
 * Ryan then supplied the real exports: 1280 x 720, square pixels, natively 16:9.
 * The film's own aspect and this box's aspect are now the SAME NUMBER, so
 * `cover` and `contain` would draw identical pixels and the rendered frame is
 * the box exactly (`tools/filmgate.mjs` asserts it, at three sizes). `cover` is
 * kept rather than swapped because it is the safe one of the two if a future
 * source ever arrives off-ratio: it fills the well. `fill` remains the one
 * thing this must never be — it would reintroduce, by hand, the distortion the
 * squash premise wrongly accused YouTube of.
 *
 * NO `pointer-events: none` IS NEEDED and none is set: a `<video>` with no
 * `controls` attribute has no interactive surface of its own, so the tap
 * control above it gets the press by z-order alone. (The iframe needed it
 * because a cross-origin document consumes events whatever it draws.)
 */
.sf-cat-hero__film-video {
  display: block;
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
}

/*
 * THE SUBTITLES (Ryan's FD, 2026-08-27) — and what is and is not controllable.
 *
 * `::cue` accepts a SHORT list: colour, background, the font properties,
 * `line-height`, `opacity`, `outline`, `text-decoration`, `text-shadow`,
 * `visibility`, `white-space`. It accepts NO box properties at all — no margin,
 * no padding, no position, no width. So the type discipline is set here and the
 * GEOMETRY is set in the cue settings of the file itself
 * (`align:start position:2%,line-left size:46%`, written by
 * `tools/fetch-film.mjs`), which is the only place a caption's box can be
 * spoken about.
 *
 * `font-size` IS A PERCENTAGE OF A MOVING TARGET, and that is the whole problem
 * this pair of rules solves. MEASURED: the user agent's own cue size is exactly
 * 5% OF THE VIDEO'S HEIGHT, so it is 12.04px in the 240.75px slot and 36.9px in
 * the 738px expanded box — a pure linear scale, which is right for a player
 * that goes full screen and wrong for a box that triples in place. At one
 * percentage the captions are either too small to read at rest or cinema-sized
 * when expanded (they were 33.9px, three lines deep across the picture, which
 * is what sent this rule back for a second pass).
 *
 * SO THE SIZE IS DECLARED PER STATE. It was chosen to land the caption at
 * roughly a CONSTANT physical size — ~13px in the slot, ~22px expanded — on the
 * reasoning that a reader's eyes do not get further from the screen when a
 * video gets wider.
 *
 * ─ FH2: THE LINE COUNT OVERRULES THE PHYSICAL SIZE AT REST (Ryan, 2026-08-27) ─
 *
 * "~13px in the slot" WAS THE WRONG TARGET, and the phone found it first: at
 * 108% the resting caption is 13.0px in a 196.9px box, and MEASURED in the
 * shadow tree, TWELVE of the film's fifteen cues wrapped to FOUR LINES —
 * two-thirds of the picture, on the film's own bottom-left. Ryan ruled the
 * phone's treatment onto the desktop: smaller type until they stop.
 *
 * SWEPT A PERCENT AT A TIME against the rendered cue box, all fifteen cues, at
 * 1440 at rest — line count read as `(box height − one-line height) /
 * line-height + 1`, against cue 7 ("now at produnk.com") as the one-line
 * reference, because the display box is `h₁ + (n − 1) × line-height` and
 * dividing by the line height alone counts a line that is not there:
 *
 *     108%   13.00px   4 lines  (twelve of fifteen cues)
 *      96%   11.56px   4
 *      90%   10.83px   3
 *      86%   10.35px   3
 *      84%   10.11px   3   ← one cue still runs over
 *      83%    9.99px   2   ← the largest that holds
 *      82%    9.87px   2   SHIPPED
 *
 * 82% RATHER THAN THE 83% CEILING, for the same reason the phone took 82% over
 * its own: one point of headroom on a wrap that is decided by subpixel text
 * measurement. The cue that sets the ceiling is the same one at both cuts —
 * "from Pro Dunk. You can now adjust from 5 / to 10 feet in under 10 seconds
 * with the", the longest pair of authored lines in the track.
 *
 * AND THE TWO CUTS LANDING ON THE SAME NUMBER IS NOT A COINCIDENCE, which is
 * worth writing down because it retires the idea that each width needs its own
 * sweep. The cue box's WIDTH is `size:46%` of the picture and the UA's cue FONT
 * is 5% of the picture's height, and the box is 16:9 at every state — so the
 * ratio of the two is a constant (measured: 196.9/12.0375 = 16.36 at 1440 rest,
 * 603.5/36.9 = 16.36 expanded, 165.6/10.13 = 16.35 on the phone). CHARACTERS
 * PER LINE ARE SCALE-INVARIANT HERE. A percentage that holds two lines at one
 * size holds two at every size, which is why the ceiling measures 83% at rest,
 * 84% expanded and 82% on the phone — one number inside a point of noise.
 *
 * THE EXPANDED STATE WAS MEASURED AND LEFT ALONE. At its ruled 60% every one of
 * the fifteen holds two lines already (its own ceiling is 84%), so there is
 * nothing for Ryan's ruling to fix there and the ~22px physical size — which is
 * still the right target for a caption on a 1312px picture — stands.
 *
 * It is a step rather than a tween: `font-size` is one of the properties
 * `::cue` accepts, `transition` is NOT, so the caption resizes at the start of
 * the 280ms rather than across it. Under a caption that is on screen for four
 * seconds at a time, during a motion that is over in a third of a second.
 *
 * The background is the ink at 0.62 rather than the UA's opaque black: this
 * caption sits on OUR footage under OUR controls, and the same wash the film's
 * own furniture uses keeps it in the family.
 */
.sf-cat-hero__film-video::cue {
  background: rgba(11, 12, 14, 0.62);
  color: var(--brand-color-surface, #ffffff);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  /* 82% of 12.04px = 9.87px in the slot at 1440 — FH2's swept value; 108% was
   * the four-line size this replaced. */
  font-size: 82%;
  line-height: 1.35;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}

/* 60% of 36.9px = 22.1px in the expanded box at 1440 — see above. Swept under
 * FH2 and kept: every cue is already two lines here, and the ceiling is 84%. */
.sf-cat-hero__film[data-big="true"] .sf-cat-hero__film-video::cue {
  font-size: 60%;
}

/*
 * The mono placeholder is the film's stand-in, so it goes when the film is
 * there. `visibility` rather than `display`: the label is centred by the well's
 * flex box, and removing it from flow would move nothing but is one more thing
 * to be wrong about.
 *
 * `data-slot="film"` REPLACED `data-film="playing"` on 2026-08-27 (Ryan's FF).
 * The old name is retired because it became a LIE: the film now stays mounted
 * when the reader scrolls past it and PAUSES rather than leaving, so "playing"
 * was drawn on a slot that was not. What these two rules actually care about is
 * which of the two things is on the slot — the film, or the board's well — and
 * that is what the attribute says now.
 */
.sf-cat-hero__film[data-slot="film"] .sf-cat-hero__film-label {
  visibility: hidden;
}

/*
 * THE CUE IS THE STILL STATE'S, AND ONLY THE STILL STATE'S (Ryan's F2,
 * 2026-08-27). It used to be drawn over the footage with a `z-index: 2` and a
 * comment saying the caption became true the moment the film started — retired
 * for the film's own bottom-left, which is where the subtitles are.
 *
 * THE REASON THAT RULE GAVE IS ITSELF RETIRED BY NAME. It read "the Lightning
 * Adjust film carries burnt-in subtitles along its bottom left", and that was
 * WRONG: nine frames sampled across the full 86s carry no text in the pixels at
 * all (`tools/fetch-film.mjs`). What the F2 pass was looking at was YOUTUBE'S
 * OWN CAPTION LAYER — the player drawing the auto-caption track — read as part
 * of the picture. The GEOMETRY it produced is unchanged and now has a better
 * reason: since FD the subtitles are OURS, served as a `<track>` and anchored
 * to the left 46% by their own cue settings, so the bottom left is spoken for
 * by a box this page writes and the cluster is at the other end of the
 * rectangle by arithmetic rather than by observation.
 *
 * `display: none` rather than a fade: the cue is a caption about a tap, not a
 * thing that transforms into the cluster.
 */
.sf-cat-hero__film[data-slot="film"] .sf-cat-hero__film-cue {
  display: none;
}

/*
 * --- the cluster: sound and size, on the playing film ---
 *
 * BOTTOM RIGHT, which is the whole point of it (F2). It is dressed from the
 * slot's existing chrome — the cue badge's white pill, the mono micro caption,
 * the same `--radius-pill` — so the film's furniture reads as one family rather
 * than as a control panel that arrived from another page.
 */
.sf-cat-hero__film-controls {
  align-items: center;
  bottom: 12px;
  display: flex;
  gap: 8px;
  position: absolute;
  right: 14px;
  /* Over the tap layer, which covers this same rectangle at 3. */
  z-index: 4;
}

.sf-cat-hero__film-btn {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: 0;
  border-radius: var(--radius-pill);
  /* The pills sit on moving footage, so they carry their own lift rather than
   * trusting whatever frame is underneath them. */
  box-shadow: 0 2px 10px rgba(11, 12, 14, 0.36);
  color: var(--brand-color-ink, #17181a);
  cursor: pointer;
  display: flex;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  gap: 6px;
  height: 32px;
  letter-spacing: 0.12em;
  /* Tighter on the glyph side than the word side: the glyph has its own optical
   * margin and the letter-spacing adds a trailing gap to the last character. */
  padding: 0 11px 0 9px;
}

.sf-cat-hero__film-btn svg {
  height: 14px;
  width: 14px;
}

/*
 * THE ON STATE INVERTS, and that is what keeps the resting state unmistakable
 * (Ryan's F1: "the default state must read as OBVIOUSLY muted"). Silent is the
 * white pill with the struck-through speaker; audible is the filled one, and
 * the film at its own size is white while the expanded film is filled.
 *
 * `[data-on]` REPLACED `[aria-pressed]` on 2026-08-27 (Ryan's FC): the labels
 * are ACTIONS now — "Unmute", "Expand" — and `aria-pressed` on an
 * action-labelled control announces "Unmute, pressed", which is a
 * contradiction. The attribute that dresses the pill is no longer the attribute
 * that talks to a screen reader; the visible verb does that on its own. The
 * STATE is still drawn twice over, glyph and fill, which is what the original
 * three-way drawing loses and keeps.
 */
.sf-cat-hero__film-btn[data-on="true"] {
  background: var(--brand-color-ink, #17181a);
  color: var(--brand-color-surface, #ffffff);
}

/*
 * THE TAP CONTROL: the whole rectangle, drawn as nothing.
 *
 * It carries no text — its name is an `aria-label`, because the two mono
 * captions inside the well are placeholder typography rather than a sentence.
 * It sits above the frame so a press reaches it rather than the player.
 */
.sf-cat-hero__film-tap {
  background: transparent;
  border: 0;
  cursor: pointer;
  inset: 0;
  padding: 0;
  position: absolute;
  z-index: 3;
}

/*
 * --- EXPAND: the film grows to the content measure, in place ---
 *
 * NOT THE LIGHTBOX, and not the screen (Ryan's F1, 2026-08-27). The film opens
 * to the width of the PAGE'S CONTENT — the same measure the comparison table
 * and every other section is drawn in — and covers the hoop, with its top edge
 * still on the slot's top edge and its right edge tight to the content's right
 * edge.
 *
 * THE WIDTH IS THE GRID'S OWN ARITHMETIC RATHER THAN A MEASUREMENT. The hero
 * grid is `minmax(0, 428fr) minmax(0, 860fr)` with a fixed 24px gap (see it
 * declared above), so at EVERY width above the mobile cut:
 *
 *     content = copy + 24 + stage,   stage / copy = 860 / 428
 *     content = copy × (1288 / 428) + 24px
 *
 * and `100%` here is the copy column, because the wrapper is a block in it. At
 * 1440 that resolves 428 → 1312, which is the container's content box to the
 * pixel. The alternative was a JavaScript measurement written back as a custom
 * property on every resize; this needs no observer, no style write and no
 * second source of truth for a number the grid rule already states. If the
 * grid's ratio ever changes, this line changes with it — `tools/filmgate.mjs`
 * asserts the expanded box's right edge against the grid's at four widths, so
 * a drift is a failure rather than a slow lean.
 *
 * `left: 0` and the full content width mean the box is tight to BOTH content
 * edges at once; "anchored tight right" and "full content width" are the same
 * geometry here, not two constraints in tension.
 *
 * ONE DECLARATION DOES ALL OF IT, which is the point of the custom property:
 * the drawn box and the flow spacer both read `--sf-film-w`, so they cannot
 * disagree about how wide the film is and there is no state in which the column
 * has made room for a different rectangle than the one on screen.
 */
.sf-cat-hero__film[data-big="true"] {
  --sf-film-w: calc(100% * 1288 / 428 + 24px);
}

/*
 * --- the animation, and the two states that get none ---
 *
 * RYAN'S FB1: "a small animation to the new size (and back)."
 *
 * 280ms ON THE SITE'S OWN CURVE. `cubic-bezier(0.22, 0.61, 0.36, 1)` is what
 * the lightbox settles its stage with at 200ms and its tray with at 220ms, so
 * this is that family rather than a new one; the duration is longer because the
 * travel is — the box crosses ~880px of width at 1440 against the settle's few
 * dozen. `category-hero-film.tsx` reads the same two numbers for the scroll it
 * runs alongside, so the size and the page move as ONE motion; they are stated
 * in both places and `tools/filmgate.mjs` asserts they agree.
 *
 * BOTH ELEMENTS, ONE PROPERTY. The box is what the reader watches grow; the
 * spacer is what pushes the key points down. They are restyled in the same
 * recalculation by the same attribute, so no synchronisation is needed beyond
 * giving them the same transition.
 *
 * DECLARED UNDER `no-preference`, which is this site's convention for motion it
 * ADDS (the lightbox's settle and its swipe are declared the same way): a
 * reader who has asked for less motion gets the jump cut by default rather than
 * by a rule that has to remember to turn something off. FB3's scroll obeys the
 * same preference on the JavaScript side.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-cat-hero__film::before,
  .sf-cat-hero__film-box {
    transition: width 280ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
}

/*
 * SCROLL ANCHORING MUST NOT FIGHT THE ANIMATED SCROLL (FB3).
 *
 * Chrome keeps a reader's place by picking an anchor element and scrolling the
 * page to hold it still when content ABOVE it changes size. The expanding film
 * is exactly that kind of change, and everything below it in this column and in
 * the sections after it is a candidate anchor — so without this the browser
 * would scroll down by the growth to hold the key points still, and the film's
 * top would walk off the screen while our own animation was trying to bring its
 * bottom on.
 *
 * SUPPRESSED FOR THE HERO'S SUBTREE AND FOR EVERY SECTION AFTER IT, which is
 * every candidate that sits below the film. It is scoped to the expanded state
 * by `:has()`, so ordinary reading — where anchoring is doing useful work on
 * images and fonts loading in — is untouched.
 */
.sf-cat-hero:has(.sf-cat-hero__film[data-big="true"]) *,
.sf-cat-hero:has(.sf-cat-hero__film[data-big="true"]) ~ * {
  overflow-anchor: none;
}

/*
 * THE FLOAT NEEDS THE COPY COLUMN TO WIN THE PAINT, and that is a stacking
 * context problem rather than a z-index one: `.sf-cat-hero__copy` carries
 * `z-index: 2` (gotcha #5 above), which makes it a stacking context, so no
 * z-index INSIDE it can beat the stage's orbs (2) and open orb panels (3) —
 * they are painted in the section's context, after the copy column in document
 * order. Raising the column itself is what settles it, and it is scoped to the
 * one state that needs it: at rest the column stays at 2 and the orbs still
 * paint over the copy's measure exactly as the board draws them.
 */
.sf-cat-hero__copy:has(.sf-cat-hero__film[data-big="true"]) {
  z-index: 4;
}

/* ------------------------------------------------------------- key points -- */

.sf-cat-hero__points {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 32px;
  max-width: 470px;
}

/*
 * --- EXPANDED: THE FOUR POINTS GO 2 x 2 ACROSS THE CONTENT MEASURE (FB-v3b) --
 *
 * RYAN'S RULING, 2026-08-28, and its reasoning is the whole rule: the narrow
 * stacked list of four is "not artificially needed" — it is narrow because at
 * REST this column shares the row with the product stage, and 428px is all it
 * has. The expanded film has already taken the content measure and covered the
 * stage; the constraint that made the list narrow is gone, so the list stops
 * pretending it is there.
 *
 * THE WIDTH IS THE FILM'S OWN ARITHMETIC, written the second time rather than
 * inherited: `--sf-film-w` is declared on `.sf-cat-hero__film`, and a custom
 * property inherits DOWN, never sideways to a sibling. Both expressions resolve
 * `100%` against the same box — the copy column's content box, which is what
 * both the wrapper and this list are blocks in — so they cannot disagree, and
 * `tools/filmgate.mjs` asserts this list's right edge against the grid's at
 * every width exactly as it asserts the film's.
 *
 * ORDER IS DOM ORDER, which is the content's order in `page.tsx` and is Ryan's
 * ruled reading order: `grid-auto-flow: row` fills left to right, top to
 * bottom, so no `order` property is involved and the rest state's stacked
 * sequence and the expanded state's grid sequence are the SAME list read two
 * ways. A future reorder of either is a reorder of both, which is the point.
 *
 * 24px BETWEEN THE COLUMNS, which is the hero grid's own `column-gap` — the
 * page has one number for "the space between two columns of content" and this
 * is a pair of content columns. The 16px row gap is the list's own and is
 * unchanged, so the vertical rhythm between two points is what it always was.
 *
 * IT IS A STEP, NOT A TWEEN, AND THAT IS A DELIBERATE CALL (see the note beside
 * the film's transition): `display` and `grid-template-columns` cannot be
 * interpolated through a reflow, and the honest alternatives were both worse.
 * Animating the list's WIDTH over the same 280ms re-wraps every point's text on
 * every frame — measured at the half-way width each point is a different number
 * of lines than at either end, so the block jitters vertically while it travels.
 * Swapping at the END of the animation puts a second jump after the first,
 * which is exactly the two-step Ryan warned against. So the reflow happens on
 * the same frame the attribute flips, and the ONE motion the reader sees is the
 * film growing and the block travelling down with it.
 *
 * THE REST STATE IS NOT TOUCHED BY ANY OF THIS. Every declaration here is
 * inside the `:has()`, so the resting list is byte-for-byte the flex column
 * above it.
 */
.sf-cat-hero__copy:has(.sf-cat-hero__film[data-big="true"]) .sf-cat-hero__points {
  column-gap: 24px;
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: 1fr 1fr;
  max-width: none;
  width: calc(100% * 1288 / 428 + 24px);
}

.sf-cat-hero__point {
  align-items: flex-start;
  color: var(--brand-color-ink-soft, #44464a);
  display: flex;
  font-size: var(--text-emphasis);
  gap: 12px;
  line-height: 1.5;
}

.sf-cat-hero__point-check {
  color: var(--brand-color-good, #1f7a4d);
  flex: none;
  height: 17px;
  margin-top: 3px;
  width: 17px;
}

/* The Lightning Adjust mark is taller than the line it leads, so it is centred
 * against a box the height of that line rather than aligned to its top. */
.sf-cat-hero__point-mark {
  align-items: center;
  display: flex;
  flex: none;
  height: 24px;
}

.sf-cat-hero__point-mark img {
  width: 21px;
}

.sf-cat-hero__point-title {
  color: var(--brand-color-ink-soft, #44464a);
  display: block;
  font-size: var(--text-emphasis);
  line-height: 1.5;
}

.sf-cat-hero__point-note {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-support);
  line-height: 1.5;
  margin-top: 2px;
}

/* ----------------------------------------------------------- the stage -- */

.sf-cat-hero__stage-wrap {
  display: flex;
  justify-content: center;
}

/*
 * The board's stage is 893 x 1080 pulled 80px left of its 860px track — 893
 * because that is exactly what the 900 x 1088 photograph measures at 1080 tall.
 * As percentages of the track those are 893/860 and −80/860. They are written
 * to full precision on purpose: rounded to four places the stage lands 0.016px
 * short of 893, and a fraction of a pixel is enough to rasterise a pinned card's
 * arrow one row off the board.
 */
.sf-cat-hero__stage {
  aspect-ratio: 893 / 1080;
  margin-left: -9.30232558139535%;
  position: relative;
  width: 103.83720930232558%;
}

/* Mirrored: the photograph is shot with the pole on the other side. */
.sf-cat-hero__photo {
  display: block;
  height: 100%;
  object-fit: contain;
  transform: scaleX(-1);
  width: auto;
}

/* -------------------------------------------------------- spec floaties -- */

/* Pinned at the feature each one names, with its arrow POINTING AT the system.
 * Uniform card, so the arrow — leading or trailing — is what says which side of
 * the hoop the card is on. */
.sf-cat-hero__floaty {
  position: absolute;
}

.sf-cat-hero__floaty-card {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  box-shadow: 0 6px 18px rgba(23, 24, 26, 0.1);
  display: inline-flex;
  gap: 12px;
  padding: 11px 15px;
  text-decoration: none;
  white-space: nowrap;
}

.sf-cat-hero__floaty-arrow {
  color: var(--brand-color-faint, #9a988f);
  font-size: var(--text-emphasis);
}

.sf-cat-hero__floaty-title {
  color: var(--brand-color-ink, #17181a);
  display: block;
  font-size: var(--text-support);
  font-weight: 700;
}

.sf-cat-hero__floaty-note {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-caption);
  margin-top: 2px;
}


/* Glass: top right of the backboard, arrow pointing back down at it. */
.sf-cat-hero__floaty--glass {
  left: 67.86114221724524%;
  top: 17.22222222222222%;
}

/*
 * Rim: arrow pointing right at the rim.
 *
 * MOVED RIGHT off the board's 15.4535% (Ryan, 2026-08-26): out at the board's
 * pin the card sat 68px clear of the rim with nothing between the two, and the
 * arrow pointed across empty white. It now finishes ON the backboard — measured
 * at 1440, where the stage is 893px wide: the card's right edge lands at 806px
 * against the backboard's lower-left corner at 776px, so it overlaps by 30px
 * and stops 6px short of the rim's own left edge. 185/893 is that anchor as a
 * fraction of the stage, written to full precision for the reason the stage's
 * own percentages are.
 */
.sf-cat-hero__floaty--rim {
  left: 20.716685330347145%;
  top: 26.666666666666668%;
}

/* Steel: mid-pole, arrow pointing right at the post. */
.sf-cat-hero__floaty--steel {
  left: 44.79283314669653%;
  top: 50.74074074074074%;
}

/* ------------------------------------------------- lightning adjust card -- */

/*
 * The one EXPANDED floaty: the mark on top, the card vertical. It is the
 * patent-pending feature, and the board gives it the room the others do not
 * get — its own radius, its own shadow, four lines and a link.
 */
.sf-cat-hero__la {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-floaty);
  box-shadow: 0 10px 26px rgba(23, 24, 26, 0.13);
  display: flex;
  flex-direction: column;
  gap: 10px;
  left: 72.56438969764838%;
  /*
   * A pinned card may not run off the stage it is pinned to. The card's width is
   * fixed while the stage scales, so below roughly 834px the board's 186 reaches
   * past the stage's right edge and the whole PAGE gains a horizontal scrollbar —
   * measured 12px of it at 768. The cap is the distance from this card's anchor
   * to the stage's edge, (893 − 648) / 893, which at the board's own width is
   * 245px and therefore does nothing at all.
   *
   * FLAGGED: the 768–833 band has no board. The cap stops the overflow; what the
   * annotations should actually DO as the stage shrinks is a design question, and
   * the answer wants a measured sweep and a words-spec, not a guess here.
   */
  max-width: 27.43560134378499%;
  /* The extra left padding is the arrow's lane. */
  padding: 16px 16px 16px 30px;
  position: absolute;
  text-decoration: none;
  top: 45.37037037037037%;
  width: 186px;
}

.sf-cat-hero__la-arrow {
  color: var(--brand-color-faint, #9a988f);
  font-size: var(--text-emphasis);
  left: 9px;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

/* `height: auto` because the mark carries width AND height attributes: those are
 * there to reserve the box before the file lands, and without this the height
 * attribute would win as a presentational hint and round the mark half a pixel
 * off the board. The stylesheet sizes it from the width; the attributes only
 * tell the engine the shape to hold. */
.sf-cat-hero__la-mark {
  height: auto;
  width: 36px;
}

.sf-cat-hero__la-title {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-body);
  font-weight: 700;
}

.sf-cat-hero__la-desc {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-small);
  line-height: 1.5;
}

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

.sf-cat-hero__la-more {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-small);
  text-decoration: underline;
}

/* -------------------------------------------------------- the two states -- */

/*
 * The desktop annotations and the mobile orbs are DIFFERENT COMPOSITIONS, not
 * one restyled: a pinned card with an arrow is a link you read, an orb is a
 * control you tap. Both are rendered and the wrong one is `display: none`, which
 * takes it out of the accessibility tree as well as off the page — so nothing is
 * announced twice.
 */
.sf-cat-hero__orbs {
  display: none;
}

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

  .sf-cat-hero__orbs {
    display: block;
  }
}

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

@media (max-width: 767.98px) {
  .sf-cat-hero {
    /* The board's 52px of white under the stage is the REVIEW BAND's top margin
     * at this cut, not this section's bottom padding: the band renders inside
     * this section and paints paper, so padding here would put 52px of white
     * BELOW it. Same pixels, one owner. */
    padding: 26px 0 0;
  }

  /*
   * The stage moves from beside the copy to INSIDE it, between the lede and the
   * film. `display: contents` dissolves the copy column's box so its children
   * and the stage become siblings in one column, and `order` puts them in the
   * board's order without a second copy of the markup in the DOM.
   */
  .sf-cat-hero__grid {
    /* `stretch` undoes the desktop `align-items: start`, which in a COLUMN flex
     * is a horizontal instruction: it shrink-wraps every block to its own text,
     * and the film well then sits at the width of its caption instead of the
     * page's. */
    align-items: stretch;
    display: flex;
    flex-direction: column;
  }

  .sf-cat-hero__copy {
    display: contents;
  }

  .sf-cat-hero__series {
    /* Board-measured, off-scale: CategoryMobile.dc.html draws this line at 54px,
     * which sits between the display ladder's 44 and 64 and so has no token. It
     * ships verbatim rather than snapped to a neighbour — see the playbook's
     * no-normalization rule. */
    font-size: 54px;
    order: 1;
  }

  .sf-cat-hero__lede {
    margin-top: 12px;
    max-width: none;
    order: 2;
  }

  .sf-cat-hero__stage-wrap {
    /* Full bleed: the photograph runs edge to edge while the copy keeps the
     * page's gutter. */
    margin: 10px calc(-1 * var(--sf-gutter, 15px)) 0;
    overflow: hidden;
    position: relative;
    order: 3;
  }

  /*
   * 46px OF PADDING LEFT THIS BOX on 2026-08-26 with the shared detail panel it
   * was reserving room for. The floaties that replaced it open ON the
   * photograph, so the white strip under the frame had nothing left to hold.
   */

  .sf-cat-hero__stage {
    aspect-ratio: auto;
    margin-left: 0;
    /*
     * THE PHOTOGRAPH'S OWN BOX IS THE POSITIONING CONTEXT (2026-08-26). It used
     * to be the full-bleed wrapper, whose height was the photo PLUS the retired
     * panel's 46px reserve — so every orb's `top` percentage resolved against a
     * box that was 9.5% taller than the photo at 320 and 7% taller at 430, and
     * the four orbs slid down the hoop as the frame grew. This element measures
     * exactly the photograph, at every width, which is what a pin on a
     * photograph has to be measured from — and it is what the floaties beside
     * them are measured from too.
     */
    width: auto;
  }

  /* Oversized and pulled left, so the hoop fills the frame rather than sitting
   * in the middle of it. */
  .sf-cat-hero__photo {
    height: auto;
    margin-left: -19%;
    width: 132%;
  }

  .sf-cat-hero__film {
    margin-top: 22px;
    max-width: none;
    order: 4;
  }

  .sf-cat-hero__film-box {
    box-shadow: 0 10px 26px rgba(23, 24, 26, 0.2);
  }

  /*
   * THE WELL'S 197px LEFT WITH THE LETTERBOX (F3): the wrapper's 16 / 9 governs
   * at this cut too, and 360px of content measure makes it 202.5 tall. The
   * board drew 197 at 390, which is a ratio of 1.827 — the same 4px of black
   * along the top and bottom that the desktop box had, five pixels shallower.
   *
   * THE "EXPAND" CONTROL IS NOT DRAWN HERE, and the reason is that there is
   * nothing left for it to do: the slot is ALREADY the full content measure at
   * this cut (360 of 390, gutter to gutter), which is exactly what the control
   * grows it to on the desktop. The alternatives were both worse — a control
   * that visibly does nothing, or one that breaks the page's gutter to buy 30px
   * of width and calls that "bigger". A control that cannot act must not be
   * drawn is this page's own rule (the comparison table's pads line is the
   * precedent); the sound toggle, which CAN act, stays.
   *
   * RE-VERIFIED UNDER FB2's FLOW MODEL (2026-08-27), because the claim it rests
   * on is a measurement and the model under it changed: the wrapper is still
   * `max-width: none` here and still the column's full width, so
   * `--sf-film-w: 100%` is still the content measure and the control still has
   * nothing to grow. `tools/filmgate.mjs` asserts it at 390 rather than
   * trusting this paragraph.
   */
  .sf-cat-hero__film-btn--size {
    display: none;
  }

  /*
   * AND THE STATE ITSELF IS NEUTRALISED, not merely unreachable. The pill is
   * the only way to set `data-big`, so this can only be reached by a reader who
   * expanded the film on a wide window and then narrowed it — at which point
   * the desktop arithmetic would resolve against a column that IS the content
   * measure and blow the film out to 3× the viewport. The state is drawn as
   * "the slot's own size" at this cut, which is the only size there is.
   */
  .sf-cat-hero__film[data-big="true"] {
    --sf-film-w: 100%;
  }

  /*
   * AND SO IS THE 2 x 2 (FB-v3b), for the same reason and reachable only the
   * same way. The width the desktop rule computes is the content measure
   * derived from a 428px copy column; at this cut the column IS the content
   * measure, so that arithmetic would resolve to three times the viewport, and
   * two columns of a 360px measure is not a layout anybody ruled. The list is
   * the phone's own stacked one in both states.
   */
  .sf-cat-hero__copy:has(.sf-cat-hero__film[data-big="true"]) .sf-cat-hero__points {
    display: flex;
    width: auto;
  }

  /*
   * THE PHONE'S SUBTITLES, THIRD DECLARATION OF THE SAME PROPERTY (Ryan's FG,
   * 2026-08-27: "the caption wraps to four lines on the phone — take the font
   * size down"). The desktop pair above sizes for a box that is 240.75px tall
   * at rest and 738px expanded; this cut's box is 202.5px, so the user agent's
   * own 5%-of-video-height cue lands at 10.13px and the inherited 108% drew
   * 10.94px — at which MEASURED, in the shadow tree, thirteen of the film's
   * fifteen cues wrapped to FOUR lines.
   *
   * FOUR LINES BECAUSE OF THE WIDTH, NOT THE HEIGHT. Every cue is two authored
   * lines of up to forty characters, and the cue box is `size:46%` of a 360px
   * picture — 165.6px. Each authored line wraps once, and two become four.
   *
   * 82% IS THE LARGEST SIZE AT WHICH EVERY ONE OF THE FIFTEEN HOLDS TWO LINES,
   * swept a percent at a time against the rendered box: at 84% one cue ("from
   * Pro Dunk. You can now adjust from 5 / to 10 feet in under 10 seconds with
   * the") still runs to three, at 86% two do, at 88% six do. So 8.30px, and it
   * is small — that is the honest cost of the 46%.
   *
   * THE WIDTH IS THE LEVER THAT WAS NOT PULLED, and it is recorded here so the
   * next pass does not re-derive it: `size` is a cue setting inside the VTT, so
   * ONE VALUE SERVES EVERY WIDTH, and the binding width is 1440 AT REST, where
   * the two-pill cluster's left edge sits at 54.96% of a 428px box. 46% leaves
   * that 38px of daylight. Widening the caption for the phone — where the
   * cluster is one pill and starts at 72.4% — would spend the desktop's margin
   * on a cut that never needed it, and F2's clearance is a ruling. Font size is
   * the lever this cut owns; it is the one that moved.
   *
   * FH2 MADE THIS A RESTATEMENT RATHER THAN AN OVERRIDE (2026-08-27). Ryan
   * ruled the same treatment onto the desktop, its own sweep landed on 82% too,
   * and the base rule now declares it — for a structural reason recorded beside
   * that rule: characters-per-line is scale-invariant in this slot, so one
   * percentage was always going to serve every width. The pair is kept, not
   * deleted: the SECOND selector is still doing work at any cut where a big
   * state could exist, and the derivation above is the record of how the number
   * was found. If the base ever moves off 82%, this block is what holds the
   * phone at it.
   */
  .sf-cat-hero__film-video::cue,
  .sf-cat-hero__film[data-big="true"] .sf-cat-hero__film-video::cue {
    font-size: 82%;
  }

  .sf-cat-hero__film-label {
    padding: 0 16px;
  }

  .sf-cat-hero__film-cue {
    bottom: 10px;
    gap: 8px;
    left: 12px;
  }

  .sf-cat-hero__film-play {
    height: 28px;
    width: 28px;
  }

  .sf-cat-hero__film-play svg {
    height: 10px;
    width: 10px;
  }

  /* The sheddable end of each mono caption. The desktop board has room for the
   * full sentence; the mobile board stops short. */
  .sf-cat-hero__film-label span,
  .sf-cat-hero__film-cue-label span {
    display: none;
  }

  .sf-cat-hero__point-check {
    height: 15px;
    width: 15px;
  }

  .sf-cat-hero__points {
    gap: 13px;
    margin-top: 36px;
    max-width: none;
    order: 5;
  }

  .sf-cat-hero__point {
    font-size: var(--text-support);
    gap: 10px;
  }

  .sf-cat-hero__point-mark {
    height: 22px;
  }

  .sf-cat-hero__point-mark img {
    width: 19px;
  }

  .sf-cat-hero__point-title {
    font-size: var(--text-support);
  }

  .sf-cat-hero__point-note {
    font-size: var(--text-small);
    margin-top: 1px;
  }
}

/* ---------------------------------------------- tap orbs and their floaties -- */

/*
 * The mobile answer to the desktop's spec floaties: an accent +/− orb pinned at
 * each feature, and a FLOATY BESIDE THAT ORB when it is tapped.
 *
 * RULED 2026-08-26 (Ryan), retiring the shared panel below the photograph and
 * the one-at-a-time accordion that came with it. The panel's failure was
 * invisibility — tapping the backboard orb at the top of the frame changed a
 * card parked under the hoop, off screen. Floaties close INDIVIDUALLY, so any
 * number of them may be open at once.
 *
 * —— THE DERIVED PLACEMENTS ——
 *
 * NO BOARD DRAWS THIS COMPOSITION, so every number below is derived rather than
 * transplanted, and the derivation is written down here because that is the only
 * place it can be checked.
 *
 * The frame is the PHOTOGRAPH'S box (`.sf-cat-hero__stage`): width W = the
 * full-bleed wrapper's width, height 1.5957333 × W (the photo runs at 132% of
 * the frame and is 900 × 1088). Both the orbs and the floaties are measured from
 * it, so the whole composition scales as one thing.
 *
 * Each card is anchored to ITS OWN orb: the side with room (glass, steel and
 * lightning to the LEFT of their orb, rim to the RIGHT of its own), an 8px gap
 * to the orb it belongs to, and a 3–4% inset from the far edge of the frame —
 * so a card is FLUID and there is no fixed width to overrun a 320px phone. It is
 * centred on its orb, then nudged along the frame by the per-orb offsets below,
 * which are what open the lanes between the four cards at the narrowest width.
 *
 * THE CONSTRAINT THE NUDGES SATISFY: in ALL SIXTEEN open-states, no floaty
 * intersects another floaty or any orb other than its own, and no floaty leaves
 * the frame. Measured at 320, 360, 390 and 430 by `tools/orbfit.mjs`, which is
 * the gate rather than the eye.
 */
.sf-cat-hero__orb {
  align-items: center;
  appearance: none;
  background: var(--brand-color-surface, #ffffff);
  border: 2px solid var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-pill);
  box-shadow: 0 4px 14px rgba(23, 24, 26, 0.22);
  color: var(--brand-color-accent, #c4530a);
  cursor: pointer;
  display: flex;
  font-family: inherit;
  font-size: 18px;
  font-weight: 600;
  /* 34, not the board's authored 30: the boards run in the browser's default
   * content-box, so a 30px orb with a 2px border RENDERS 34px across, and the
   * board's rendered pixels are the specification. Everything on this site is
   * border-box, so the number that reproduces the drawing is the outer one. */
  height: 34px;
  justify-content: center;
  line-height: 1;
  padding: 0;
  position: absolute;
  width: 34px;
  z-index: 2;
}

/* Open, the orb inverts: it is the one thing on the photograph that is filled. */
.sf-cat-hero__orb[aria-expanded="true"] {
  background: var(--brand-color-accent, #c4530a);
  border-color: var(--brand-color-surface, #ffffff);
  box-shadow: 0 4px 14px rgba(23, 24, 26, 0.3);
  color: var(--brand-color-surface, #ffffff);
}


/*
 * THE FOUR PINS. `left` is the board's own percentage of the frame's width and
 * is unchanged. `top` is the board's percentage RE-BASED onto the photograph:
 * the orbs used to be measured from the wrapper, which carried 46px of reserve
 * for the retired panel, so the board's 17/30/55/74 resolved against a box
 * taller than the photo by a fraction that CHANGED with the width. Multiplying
 * by 644.4/598.4 — the two boxes at the board's own 390 — lands every pin
 * exactly where the board draws it at 390 and holds it there at every other
 * width instead of letting it slide down the hoop.
 */
.sf-cat-hero__orb--glass {
  left: 64%;
  top: 18.306819%;
}

.sf-cat-hero__orb--rim {
  left: 33%;
  top: 32.306150%;
}

.sf-cat-hero__orb--steel {
  left: 54%;
  top: 59.227942%;
}

.sf-cat-hero__orb--lightning {
  left: 68%;
  top: 79.688503%;
}

/*
 * THE FLOATY. The desktop's expanded card (`.sf-cat-hero__la`) at mobile scale:
 * the same surface, the same hairline, the same 10px floaty radius and the same
 * shadow. It is a CARD ON A PHOTOGRAPH in both compositions, so it is dressed
 * the same in both; only the size and the placement are this cut's.
 *
 * FLUID BY CONSTRUCTION — `left` and `right` both anchored, no `width`. A fixed
 * card is the thing that cannot be made to fit at 320, and the retired panel's
 * 257px is exactly what pinned it to the bottom of the frame.
 */
.sf-cat-hero__orb-panel {
  animation: sf-cat-orb-open 140ms ease-out both;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-floaty);
  box-shadow: 0 10px 26px rgba(23, 24, 26, 0.13);
  padding: 11px 13px;
  position: absolute;
  /* Every card is centred on its own orb before its nudge, so its own height
   * opens it symmetrically and a card that gains a line cannot walk into the
   * one below it. */
  transform: translateY(-50%);
  z-index: 3;
}

/* Opacity only, and no movement: a card that flies in from somewhere is telling
 * the reader it came from there, and this one did not — it is already where its
 * orb is. */
@keyframes sf-cat-orb-open {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

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

/* --- the four derived placements (see the block header for the derivation) --- */

/* Glass: LEFT of its orb, nudged 18px UP. The nudge is the top half of the
 * glass/rim lane, and 320 is what sizes it: both titles wrap to two lines at
 * that width, the pair grows 18px each, and centred on their own orbs they meet
 * exactly. MEASURED at 320 with the nudge: 16.0px of lane. */
.sf-cat-hero__orb-panel--glass {
  left: 4%;
  right: calc(36% + 8px);
  top: calc(18.306819% - 1px);
}

/* Rim: the one card with room on its RIGHT — its orb sits at 33% of the frame,
 * so the space left of it is 100px at 320 and nothing fits there. Nudged 14px
 * DOWN, the bottom half of the glass lane; it can afford it because the steel
 * card below is the short one. */
.sf-cat-hero__orb-panel--rim {
  left: calc(33% + 42px);
  right: 4%;
  top: calc(32.306150% + 31px);
}

/* Steel: LEFT of its orb, nudged 14px UP — the lightning card below it is the
 * tall one, and this is the room it is handing over. */
.sf-cat-hero__orb-panel--steel {
  left: 3%;
  right: calc(46% + 8px);
  top: calc(59.227942% + 3px);
}

/* Lightning: LEFT of its orb, nudged 4px down. The tallest card by far (a mark,
 * a wrapped description and a link — 140px at 320 against the others' 84) and
 * the lowest, so it is squeezed from both sides at once and its nudge is the
 * smallest of the four. AT 320 IT IS THE BINDING CARD, squeezed to 14.6px clear
 * of the steel card above and 7.9px clear of the bottom of the photograph below
 * (the glass/rim lane, by comparison, opens to 16.0px). */
.sf-cat-hero__orb-panel--lightning {
  left: 4%;
  right: calc(32% + 8px);
  top: calc(79.688503% + 21px);
}

/*
 * THE CLOSE IS THE ORB (Ryan's ruling, 2026-08-27). This block used to draw an
 * x in the card's top-right corner and reserve its 22px lane here with
 * `padding-right`; both are gone with the button. The orb's own glyph already
 * reads `-` while its card is open, which is the close a thumb finds without
 * being told, and one control for one fact.
 *
 * THE LANE GOING WITH IT IS THE POINT, not an oversight: 22px of dead space
 * beside a heading on a 150px card at 320 is the widest card in the set losing
 * a seventh of its measure to furniture that is not drawn. The cards can only
 * get SHORTER for it — a heading that no longer has to wrap — which is slack in
 * every clearance `tools/orbfit.mjs` measures, and it re-derives them all.
 */
.sf-cat-hero__orb-head {
  align-items: center;
  display: flex;
  gap: 10px;
}

/* `height: auto` for the reason the feature card's mark has it — see
 * `.sf-cat-hero__la-mark`. */
.sf-cat-hero__orb-mark {
  height: auto;
  width: 26px;
}

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

.sf-cat-hero__orb-desc {
  color: var(--brand-color-ink-soft, #44464a);
  font-size: var(--text-caption);
  line-height: 1.45;
  margin-top: 6px;
}

.sf-cat-hero__orb-note {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  margin-top: 5px;
}

.sf-cat-hero__orb-more {
  text-decoration: underline;
}

/* ================================================================= table == */

/*
 * THE MODEL COMPARISON TABLE. Transplanted from CategoryPage.dc.html (drawn at
 * 1440) and CategoryMobile.dc.html (drawn at 390).
 *
 * The two compositions are DIFFERENT MARKUP, not one restyled: the desktop
 * table has models across the top, the mobile one has them down the side. Both
 * are rendered and the wrong one is `display: none`, which takes it out of the
 * accessibility tree as well as off the page.
 */

.sf-cat-table-section {
  /* As in the hero: the board declares 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 every cell in the table. */
  line-height: normal;
  /* 56px is the board's own gap above this section, measured there from the
   * bottom of the hero's white stage — which is the review bar's block plus the
   * stage's own 64px of bottom padding. Both exist now, so this is the board's
   * seam against the board's neighbour. */
  padding: 56px 0 0;
}

/* --------------------------------------------------------- section head -- */

/*
 * THE PLATFORM'S SECTION HEADER LEFT THIS SHEET on 2026-08-26. It shipped here
 * because this was the FIRST section on the site to want one, with a note saying
 * it "moves to a shared sheet when the second one lands"; six copies later the
 * move is taken, to `public/storefront/sections.css` under the `sf-sec-` prefix
 * — a NEW shared sheet rather than chrome.css, following the photo module's
 * promotion earlier the same day.
 *
 * The markup here now renders `.sf-sec-head*`, and the pairwise parity tests
 * that held the copies against each other are retired with them: there is one
 * copy. The relocation is proved by `tools/phrelo.mjs` (threshold 0, six regions
 * across two pages) and by p4gate's `reviewhead` holding its recorded number.
 *
 * WHAT STAYS IN THIS SHEET is the AI embed's two SCOPED overrides of it
 * (`.sf-cat-embed__row .sf-sec-head`, base and mobile) — those are this page's
 * arrangement of a shared pattern, not the pattern.
 */
/* ------------------------------------------------------ desktop / mobile -- */

.sf-cat-table-section__mobile {
  display: none;
}

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

  .sf-cat-table-section__mobile {
    display: block;
  }
}

/* ------------------------------------------------------------ the table -- */

.sf-cat-table {
  margin-top: 16px;
  position: relative;
}

/* Zero-height, in NORMAL FLOW at the card's top edge: the one point in the
 * layout that still says where the page has scrolled to once the header row is
 * pinned and every point inside it has stopped moving.
 *
 * `scroll-margin-top` here is a READ-BACK CARRIER, not a clearance. Nothing
 * scrolls to this element — zero-height, `aria-hidden`, no id — and the property
 * is chosen because 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. ONE declaration of the sum, resolved by the cascade,
 * read by both `table-sticky.tsx` (the scroll position its header row pins at)
 * and `table-highlights.ts` (how much viewport its `view.scrollTo` must clear).
 *
 * Its value deliberately MIRRORS the cascade sum — the nav row, the stuck title
 * band, and the STICKY STACK (its ruled 12px top seam, the section head and the
 * open conversation, and its ruled 8px foot; `0px` unless a conversation is
 * open; see the sticky-stack block at the foot of this sheet).
 * That mirror is safe ONLY because nothing lands here: `html`'s
 * `scroll-padding-top` already applies the chrome to every scroll port, and
 * CSSOM-View ADDS a target's `scroll-margin` to it, 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). Scroll-padding owns the real clearance;
 * this carries the number for the two readers. The `view.scrollTo` in
 * `table-highlights.ts` is a manual scroll, which scroll-padding does not touch,
 * so it needs the whole sum in hand — which is what this hands it. */
.sf-cat-table__sentinel {
  height: 0;
  scroll-margin-top: calc(
    var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px) +
      var(--sf-cat-stack-h, 0px)
  );
}

/*
 * Gotcha #1: `overflow: hidden` on this card would make it a scroll container
 * and KILL the sticky header inside it. `clip` clips without scrolling, which
 * is what the rounded corners actually need.
 */
.sf-cat-table__card {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-hairline);
  border-radius: var(--radius-control);
  overflow: clip;
  /*
   * THE CONTAINING BLOCK FOR THE a11y SPANS INSIDE THIS CARD, and it is
   * UNCONDITIONAL on purpose — see the `__caption` / `__sr` block below for the
   * mechanism.
   *
   * An absolutely positioned element resolves against its nearest POSITIONED
   * ancestor, not against the nearest scroller. Without a position here the
   * `__sr` spans in the body cells resolved against the initial containing
   * block and their static offsets then extended the DOCUMENT's scrollable
   * overflow — measured at 835/843px of horizontal document scroll at 768/834,
   * where this card is the `overflow-x: auto` scroller.
   *
   * NOT scoped to the ≤1023.98 block that turns the card into that scroller.
   * At ≥1024 the card is `overflow: clip`, and clip still establishes a
   * scrollable overflow REGION — one that cannot be scrolled to, but a region
   * all the same, and the rule is unconditional so that no future state can
   * quietly turn it into a real one and re-open the escape.
   *
   * Geometry-neutral: the spans are at their static position with no offsets,
   * so containing them moves nothing that is drawn.
   *
   * (A stable scrollbar gutter used to be reserved here. It existed for the
   * BOUNDED SCROLL MODE, which made this card `overflow: auto` while a
   * conversation was open; the mode is retired — see the sticky-stack block at
   * the foot of this sheet — so the card is `overflow: clip` in every state and
   * there is no scrollbar for a gutter to reserve. The 15px switch flash it
   * half-covered cannot happen either. A test sweeps this sheet for the
   * property by name, so the name is deliberately not written out.)
   */
  position: relative;
}

.sf-cat-table__grid {
  border-collapse: collapse;
  width: 100%;
}

/*
 * The table's own name, for anyone who reaches it without the section heading
 * above it. Visually the heading IS the caption, so this one is not drawn.
 *
 * `sf-cat-table__sr` is the same treatment as a reusable name: the text the
 * TRANSPOSED table needs in each cell to say which model it belongs to, and the
 * star row's spoken score. Both are structure a sighted reader gets from the
 * layout — the pinned model column beside the cell, the drawn stars — and
 * neither has anywhere to be drawn without changing the board.
 *
 * `position: absolute` at the element's STATIC position, with no offsets: it
 * leaves flow (so it can neither deepen a 148px cell nor become a flex item)
 * and stays where it was drawn.
 *
 * IT DOES NOT FOLLOW that it cannot lengthen an ancestor's scrollable area —
 * an earlier version of this comment claimed exactly that, and it was wrong.
 * An out-of-flow box contributes its overflow to the box that ESTABLISHES ITS
 * CONTAINING BLOCK, which is the nearest POSITIONED ancestor, and a scroller is
 * not positioned merely by being a scroller. With no positioned ancestor inside
 * the scroller these spans resolved against the initial containing block and
 * extended the DOCUMENT's scrollable overflow instead: 2950px of horizontal
 * document scroll at every width ≤767 (via `.sf-cat-table__mobile`) and 835/843
 * at 768/834 (via `.sf-cat-table__card`), with a latent vertical escape (then
 * live in the retired bounded scroll mode) on top.
 *
 * So the containment is a property of the SCROLLERS, and both of them now carry
 * `position: relative` for this and say so: `.sf-cat-table__card` above,
 * `.sf-cat-table__mobile` below. Anything that adds a third scroller around
 * this markup has to do the same.
 *
 * Clipped rather than `display: none`, which no screen reader reads.
 */
.sf-cat-table__caption,
.sf-cat-table__sr {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* -------------------------------------------------------- the header row -- */

.sf-cat-table__head {
  /* `background-color`, NEVER the `background` shorthand: the shorthand resets
   * `background-image`, and the AI run wash IS a background image layered over
   * this one. See the AI-highlight block at the foot of this sheet. */
  background-color: var(--brand-color-surface, #ffffff);
  /* Above the rows that scroll under it (gotcha #3). */
  z-index: 2;
}

/*
 * THE STICKY OFFSET, and the reason nothing below this table can jump.
 *
 * The row's BOTTOM — not its top — is what comes to rest under the pinned nav
 * bar, `--sf-cat-strip-h` px below it, because the compact strip is pinned to
 * that bottom edge. So the row sticks at its own height ABOVE the bar, and the
 * photographs scroll off the top of the viewport on the way there under their
 * own steam. The row's FLOW HEIGHT never changes between the two states, so the
 * document's height never changes either.
 *
 * The three lengths are measured and published by `table-sticky.tsx`; the pin
 * is gated on `data-driven` because before they are known this offset would
 * resolve out of zeroes and park a 340px block across the viewport.
 */
@media (min-width: 1024px) {
  .sf-cat-table[data-driven] .sf-cat-table__head {
    position: sticky;
    /*
     * THE MODEL ROW PINS BELOW THE STUCK TITLE BAND, not under it — a JUDGMENT
     * CALL, flagged as one (Ryan did not spec which way it went, 2026-08-26).
     *
     * By the time this row pins, the page is far below the band and the band is
     * pinned too, so the chrome above this table is the nav row PLUS the band's
     * stuck line. Sliding the model row under the band would put the compact
     * strip — the thing this whole offset exists to land on the bar — behind a
     * paper band with a title on it. `--sf-band-stuck-h` is 0 wherever the band
     * does not stick and on any page that has no band at all, so this reads as
     * the bare nav row everywhere else.
     *
     * `--sf-cat-stack-h` is the same argument one rung further down: while a
     * conversation is open the section head and the conversation stick between
     * the band and this row, so the chrome this row pins under is that much
     * taller. The rung carries its own two RULED SEAMS as well as the block's
     * measured height (12px above it, 8px below it — the foot is what keeps
     * this row's compact strip off the conversation's bottom edge), which is
     * why nothing here has to add them. It is 0 with no conversation open,
     * which is the resting page.
     */
    top: calc(
      var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px) +
        var(--sf-cat-stack-h, 0px) - var(--sf-cat-head-h, 0px) +
        var(--sf-cat-strip-h, 0px)
    );
  }
}

.sf-cat-table__head--label {
  /* The compact strip is absolutely positioned and 856-1311px wide; it has to
   * resolve against THIS CELL, not against the first positioned ancestor it
   * finds. Below the pin cut this cell is not sticky, and without a position of
   * its own the strip would hang off `.sf-cat-table` instead — outside the
   * card that is supposed to be clipping it, and measured: 120px of horizontal
   * scrollbar on the whole PAGE at 768. Overridden by `sticky` wherever the
   * pin is on, which is the same containing block by another name. */
  position: relative;
  width: 16%;
  /* Above its four siblings, because the compact strip lives inside this cell
   * and spans all five: at equal z-index the later cells' opaque backgrounds
   * would paint over it. */
  z-index: 3;
}

.sf-cat-table__head--model {
  border-left: var(--border-hairline);
  /* The board declares no size on this cell, so its line STRUT is the browser's
   * 16px default — and the strut is load-bearing here twice over: it sets the
   * descender space under the inline photograph, and it sets the height of the
   * price line, whose two spans both declare their own size. globals.css's 17px
   * body would deepen the header row by 2.67px. This is the board's rendered
   * pixel, not its authored value. */
  font-size: var(--text-emphasis);
  padding: 18px 14px 20px;
  text-align: center;
  vertical-align: bottom;
  width: 21%;
}

/* THE RECOMMENDED MODEL'S TREATMENT IS GONE (Ryan, 2026-08-26). The boards draw
 * Gold's header cell in an accent wash with accent borders down its sides — a
 * featured-model marker, and a perfectly good one on a board that has no AI
 * highlight engine on it. In situ it reads as a stray highlight sitting beside
 * the real ones: the same accent-soft ground and the same 2px accent edge the
 * assistant paints when it points at a column, present before anybody asked
 * anything. So the wash, the borders and the `highlighted` flag that drove them
 * are removed, and every model column renders like its siblings. The "Most
 * popular" line in the Best-for row stays — that is row CONTENT, and content is
 * what the assistant is meant to be pointed at.
 *
 * The compact strip's half of the same treatment went with it; see the strip
 * below.
 */

/*
 * The tall block, and the OUTGOING half of the swap in one direction and the
 * incoming half in the other. THE SWAP IS INSTANT in both directions — see the
 * compact strip below for why the 140ms crossfade that used to be here went.
 */
.sf-cat-table__head-block {
  display: block;
}

.sf-cat-table__head-photo {
  display: inline-block;
  position: relative;
}

.sf-cat-table__head-photo img {
  display: inline-block;
  height: 180px;
  object-fit: contain;
  /* The intrinsic attributes reserve the box before the file lands; the height
   * above is what actually sizes it, so the width attribute must not win as a
   * presentational hint. */
  width: auto;
}

.sf-cat-table__badge {
  background: var(--brand-color-accent-soft, #fbefe5);
  border-radius: var(--radius-inner);
  color: var(--brand-color-accent-strong, #9c420a);
  font-size: var(--text-micro);
  font-weight: 600;
  left: -10px;
  padding: 2px 8px;
  position: absolute;
  top: 4px;
}

.sf-cat-table__head-name {
  display: block;
  font-size: var(--text-product);
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-top: 10px;
}

.sf-cat-table__head-link {
  color: var(--brand-color-ink, #17181a);
  text-decoration: none;
}

.sf-cat-table__head-money {
  display: block;
  margin-top: 6px;
}

.sf-cat-table__head-list {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-body);
  font-weight: 400;
  text-decoration: line-through;
}

.sf-cat-table__head-price {
  color: var(--brand-color-good, #1f7a4d);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-body);
  font-weight: 700;
  margin-left: 6px;
}

.sf-cat-table__head-cta-wrap {
  display: block;
  margin-top: 12px;
}

/* The TOP call to action is INK; the one at the bottom of the table is orange.
 * Wayfinding above, commerce below — the tokens' own split. */
.sf-cat-table__head-cta {
  background: var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  display: block;
  font-size: var(--text-support);
  font-weight: 600;
  padding: 11px 0;
  text-align: center;
  text-decoration: none;
}

/* ---------------------------------------------------- the compact strip -- */

/*
 * The stuck state, drawn on the board as its own demo strip above the table.
 *
 * ABSOLUTELY POSITIONED against the header row's bottom edge and spanning the
 * whole table from inside the 16%-wide leading cell — which is why it costs the
 * row no height, and why the row's sticky offset lands it exactly under the
 * pinned nav bar. `--sf-cat-table-w` is the table's measured width.
 *
 * Its tracks are the TABLE'S OWN COLUMN EDGES (`--sf-cat-cols`, measured), not
 * the flat 16% plus four shares the board's standalone demo strip is drawn
 * with: `border-collapse` gives the auto table algorithm the last word on
 * column widths, and in situ the strip sits directly over the columns it names,
 * where a seven-pixel drift in Gold's accent border would show. The board's own
 * split is the fallback, for the state in which nothing has measured yet —
 * which is also the state in which the strip is invisible.
 */
.sf-cat-table__compact {
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  bottom: 0;
  box-shadow: 0 10px 28px rgba(23, 24, 26, 0.14);
  display: grid;
  grid-template-columns: var(--sf-cat-cols, 16% 1fr 1fr 1fr 1fr);
  left: 0;
  opacity: 0;
  overflow: hidden;
  position: absolute;
  visibility: hidden;
  width: var(--sf-cat-table-w, 100%);
}

/*
 * THE SWAP IS INSTANT, IN BOTH DIRECTIONS (Ryan, 2026-08-26).
 *
 * There used to be a 140ms opacity transition here with the `visibility` flips
 * delayed by the same 140ms on all four rules, so that the arriving block never
 * entered the tab order before the leaving one had left it. What that actually
 * drew was a fade-OUT of the leaving block followed by an instant swap on the
 * last frame — the two blocks' combined painted opacity dipped to 0.048 — and
 * that dip is exactly what Ryan saw: old fades out, white shows through, new
 * appears. It was recorded here as a deviation from the playbook's crossfade
 * floor. It is now gone rather than tuned.
 *
 * With no transition, `opacity` and `visibility` both flip in the SAME style
 * recalc: there is no window in which one block is painted and the other is
 * focusable, no window in which neither is painted, and no intermediate frame
 * for `tools/flip.mjs` to find. The same-frame focus-order discipline that the
 * delays were bought to protect now holds for free, and the `inert` alternative
 * the old comment left open is moot.
 *
 * `opacity` is kept beside `visibility` deliberately: `visibility` alone would
 * do the swap, and the pair is what makes the flip legible in a computed-style
 * trace — the tools read both.
 *
 * There is nothing left for `prefers-reduced-motion` to turn off, which is why
 * the block that used to do so is gone too.
 */
@media (min-width: 1024px) {
  .sf-cat-table[data-stuck="true"] .sf-cat-table__compact {
    opacity: 1;
    visibility: visible;
  }

  .sf-cat-table[data-stuck="true"] .sf-cat-table__head-block {
    opacity: 0;
    visibility: hidden;
  }
}

.sf-cat-table__compact-label {
  background: var(--brand-color-well, #f1f0ec);
}

.sf-cat-table__compact-cell {
  align-items: center;
  border-left: var(--border-hairline);
  display: flex;
  gap: 10px;
  /* The tracks belong to the table, so a cell that would rather be wider than
   * its track has to give: the price and the button hold their size, the name
   * ellipses. */
  min-width: 0;
  padding: 10px 14px;
}

/* Gold used to keep the featured-model wash here when the row compacted. That
 * treatment is retired with the header cell's — see the header block above. */

.sf-cat-table__compact-name {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-emphasis);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* No struck retail price here: the compact line is the price you pay. */
.sf-cat-table__compact-price {
  color: var(--brand-color-good, #1f7a4d);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-body);
  font-weight: 700;
  margin-left: auto;
}

.sf-cat-table__compact-cta {
  background: var(--brand-color-ink, #17181a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-caption);
  font-weight: 600;
  padding: 8px 14px;
  text-decoration: none;
  white-space: nowrap;
}

/* ------------------------------------------------------------- the rows -- */

.sf-cat-table__row {
  border-top: var(--border-control);
}

.sf-cat-table__row--tint {
  /* `background-color`, NEVER the `background` shorthand: the shorthand resets
   * `background-image`, and the AI run wash IS a background image layered over
   * this one. See the AI-highlight block at the foot of this sheet. */
  background-color: var(--brand-color-paper, #fbfbfa);
}

/* The money rule: the 2px `faint` line that separates what a hoop IS from what
 * it costs (tokens §3). */
.sf-cat-table__row--money {
  border-top: var(--border-money);
}

/*
 * THE TINTED LABEL COLUMN. It carries every explanation and every learn-more
 * link in the table, which is what lets the value cells stay bare.
 */
.sf-cat-table__label {
  /* `background-color`, NEVER the `background` shorthand: the shorthand resets
   * `background-image`, and the AI run wash IS a background image layered over
   * this one — every desktop ROW run paints this cell. See the AI-highlight
   * block at the foot of this sheet. */
  background-color: var(--brand-color-well, #f1f0ec);
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-body);
  font-weight: 600;
  padding: 15px 16px 15px 20px;
  text-align: left;
  vertical-align: top;
}

.sf-cat-table__label--blank {
  padding: 0;
}

.sf-cat-table__explain {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-support);
  font-weight: 400;
  line-height: 1.5;
  margin-top: 4px;
}

/*
 * THE LEARN-MORE, WHICHEVER ELEMENT IT IS.
 *
 * One class for four elements (`category-table.tsx`'s `ExplanationLink`): an
 * anchor to a page, a button that opens a film, a button that opens a modal,
 * and an inert button standing in for a modal nobody has built. To the reader
 * they are one thing — the quiet underlined phrase that closes an explanation —
 * so they are drawn by one rule.
 *
 * THE BUTTON RESETS ARE THE WHOLE POINT of the second half. A `<button>` arrives
 * with the UA's own face, size, background, border, padding and `text-align:
 * center`, none of which an anchor brings, and a phrase that swapped element
 * silently would change size and gain a grey box. `font: inherit` takes the
 * face, size, weight and leading from the explanation in one declaration;
 * `text-align: left` matters because a button inside a centred cell would centre
 * its own wrapped lines against the sentence they finish.
 *
 * `display: inline` rather than the UA’s `inline-block`, and it is not
 * cosmetic: this phrase sits INSIDE a running sentence and has to break across
 * its lines the way the words before it do. Measured in the label column at
 * 1440, the Adjusts row’s film link breaks mid-word across three lines
 * ("Lift-assisted · / easy enough for a 6- / year-old") exactly as the anchor
 * beside it would. As `inline-block` the whole button would have moved to a
 * line of its own and left the middot hanging.
 */
.sf-cat-table__explain-link {
  color: var(--brand-color-muted, #6c6a62);
  text-decoration: underline;
}

button.sf-cat-table__explain-link {
  background: none;
  border: 0;
  cursor: pointer;
  display: inline;
  font: inherit;
  letter-spacing: inherit;
  padding: 0;
  text-align: left;
  text-transform: inherit;
}

/*
 * A CONTROL THAT CANNOT ACT — AND IT IS NOT DIMMED (Ryan's WP4, 2026-08-29).
 *
 * THE DEFECT HE SAW: "the 'Safety comes standard with THOR' link is LIGHTER
 * than the other links — unify." It is the table's ONLY `kind: "pending"`
 * explanation link (`comparison.ts`, the Pole & board pads row), so it was the
 * only one of the ten wearing `aria-disabled` and the only one this rule
 * reached. Same class, same tag as the film and modal buttons beside it; one
 * attribute, and `opacity: 0.55`.
 *
 * `opacity: 0.55` IS RETIRED HERE, by name. It was borrowed from the
 * platform's disabled BUTTON treatment (globals.css `.button[aria-disabled]`),
 * and that is where the reasoning went wrong: dimming says "this control is
 * unavailable to you", which is the right thing to say about a greyed Add-to-
 * Cart and the wrong thing to say about a sentence in a table header. The
 * reader is not being denied anything — the explanation simply has no page
 * behind it yet — and the dimming made it read as a broken link rather than as
 * one of the row's ten explanations.
 *
 * WHAT STAYS IS THE HONEST HALF. `aria-disabled` stays on the element (the
 * house rule: present, in the tab order, announced), and `cursor: not-allowed`
 * still tells a mouse reader at the moment they reach for it. Both are TRUE at
 * the point of interaction; the ink is not an interaction, and the ink is now
 * the ink every other explanation link in this table has.
 */
.sf-cat-table__explain-link[aria-disabled="true"] {
  cursor: not-allowed;
}

.sf-cat-table__cell {
  border-left: var(--border-hairline);
  font-size: var(--text-body);
  padding: 15px 16px;
  vertical-align: top;
}

.sf-cat-table__cell--rating {
  padding: 14px 16px;
  text-align: center;
}

.sf-cat-table__rating {
  align-items: center;
  display: inline-flex;
  gap: 10px;
}

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

.sf-cat-table__review-count {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-body);
  text-decoration: underline;
}

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

.sf-cat-table__figure[data-nowrap="true"] {
  white-space: nowrap;
}

/* FREE is the one word in the table that is a price, so it is set like one. */
.sf-cat-table__figure--free {
  color: var(--brand-color-good, #1f7a4d);
  font-family: var(--brand-font-mono, monospace);
}

.sf-cat-table__figure-marked {
  align-items: center;
  display: inline-flex;
  gap: 10px;
}

.sf-cat-table__figure-mark {
  display: block;
  /* Sized from the width, as everywhere else the mark appears. */
  height: auto;
  width: 26px;
}

.sf-cat-table__value-note {
  color: var(--brand-color-muted, #6c6a62);
  display: block;
  font-size: var(--text-support);
  line-height: 1.5;
  margin-top: 3px;
}

/* globals.css strips `list-style` site-wide, so the board's own disc bullets
 * have to be asked for again. `role="list"` in the markup keeps the semantics
 * that stripping the marker takes away in Safari. */
.sf-cat-table__bullets {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-support);
  line-height: 1.5;
  margin: 5px 0 0;
  padding-left: 20px;
}

.sf-cat-table__bullets li {
  list-style: disc;
  margin-top: 3px;
}

/* ------------------------------------------------------ the price block -- */

.sf-cat-table__list-price {
  color: var(--brand-color-muted, #6c6a62);
  font-weight: 400;
  text-decoration: line-through;
}

.sf-cat-table__sale {
  color: var(--brand-color-accent, #c4530a);
  font-weight: 600;
}

.sf-cat-table__price {
  color: var(--brand-color-good, #1f7a4d);
  font-family: var(--brand-font-mono, monospace);
  font-weight: 700;
}

.sf-cat-table__row--cta {
  border-top: var(--border-control);
}

.sf-cat-table__cell--cta {
  padding: 16px;
}

/* The bottom call to action is ORANGE — commerce, not wayfinding. */
.sf-cat-table__cta {
  background: var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  display: block;
  font-size: var(--text-body);
  font-weight: 700;
  padding: 12px 0;
  text-align: center;
  text-decoration: none;
}


/* -------------------------------------------- the band below the board -- */

/*
 * THE BOARD DRAWS THIS TABLE AT 1440 AND AT 390, AND NOTHING BETWEEN. Rendered
 * as drawn, it does not fit: `white-space: nowrap` on the "Best for" headlines
 * gives the table a MIN-CONTENT width of 1008.5px, which needs a viewport of
 * 1138 to clear the gutters — and `overflow: clip` on the card does not
 * complain, it silently cuts the Silver column off. Measured: 304px of it gone
 * at 768. Losing a column is the worst outcome available, so the band is
 * treated in two measured steps, on ladder rungs that already exist:
 *
 *  - Below 1240 the headlines are allowed to WRAP. The one-line rule is a 1440
 *    drawing; releasing it takes min-content from 1008.5 to 856 and is the
 *    smallest thing that can be given up — a headline on two lines against a
 *    column that is not there at all.
 *  - Below 1024, 856 no longer fits either (measured: it needs 938), so the
 *    card SCROLLS sideways. That makes the card a scroll container, which is
 *    what turns the pin off — see gotcha #1 — so the compaction above is scoped
 *    to the same rung and the two states can never disagree. Keyboard readers
 *    still reach the far columns: every column carries links, and focusing one
 *    scrolls it into view.
 *
 * FLAGGED FOR RYAN: both cuts are derived from what the table MEASURES, not
 * from a board. A words-spec for this band — and for the hero's, which pass 1
 * flagged for the same reason — would replace them.
 */
@media (max-width: 1239.98px) {
  .sf-cat-table__figure[data-nowrap="true"] {
    white-space: normal;
  }
}

@media (max-width: 1023.98px) {
  .sf-cat-table__card {
    overflow-x: auto;
  }
}

/* ------------------------------------------------- the transposed table -- */

/*
 * MOBILE. Models become rows, specifications become columns you swipe, and the
 * model column PINS to the left carrying the money block and the orange CTA.
 * The strip runs full bleed — the board takes it edge to edge while the heading
 * above it keeps the page's gutter.
 */
@media (max-width: 767.98px) {
  .sf-cat-table-section {
    background: var(--brand-color-paper, #fbfbfa);
    border-top: var(--border-hairline);
    padding: 30px 0 34px;
  }

  /* The head's own mobile treatment is the PLATFORM's and lives with the
   * pattern — see the section-head block near the top of this sheet. The swipe
   * hint was the one line this table dressed here, and it was retired on
   * 2026-08-26. */
}

.sf-cat-table__mobile {
  background: var(--brand-color-surface, #ffffff);
  border-bottom: var(--border-control);
  border-top: var(--border-control);
  margin-top: 16px;
  overflow-x: auto;
  /* THE CONTAINING BLOCK FOR THE `__sr` SPANS THIS STRIP CARRIES — the same
   * mechanism, and the same fix, as `.sf-cat-table__card` (see the `__caption`
   * / `__sr` block for both). Every transposed cell holds one, and without a
   * positioned ancestor inside this scroller they resolved against the initial
   * containing block: the document read 2950px of horizontal scrollable width
   * at EVERY width ≤767. Geometry-neutral — the spans have no offsets. */
  position: relative;
  /* The specification columns are not a uniform width, so there is no page to
   * snap to — the columns themselves are the snap points, and `proximity` lets
   * a reader stop between two of them. The padding keeps a snapped column clear
   * of the pinned model column sitting over the scrollport's left edge. */
  scroll-padding-left: 128px;
  scroll-snap-type: x proximity;
}

/*
 * ONE GRID, AND EVERY COLUMN SUBSCRIBES TO ITS ROWS.
 *
 * It was a flex row of independent columns, and that worked for exactly as long
 * as every header was the same 35px: each column stacked a fixed-height eyebrow
 * over four fixed-height cells, and four independent stacks of identical boxes
 * line up by arithmetic. Ryan's 2026-08-27 ruling put the desktop's sales
 * points into those headers, so they are now VARIABLE HEIGHT — "Rust Armor"
 * carries two lines of label-and-sentence where "Rating" carries one word — and
 * four independent stacks would start their first model row at four different
 * heights. The pinned model column would line up with none of them.
 *
 * SO THE ROWS ARE THE GRID'S, not each column's. `__mgrid` declares the track
 * list — one `auto` row for the headers, then one 148px row per model — and the
 * pinned column and every swiped column take `grid-template-rows: subgrid` over
 * the whole of it. The header row is then sized ONCE, by the tallest header in
 * the table, and every column's first model row starts at the same y by
 * construction rather than by everyone agreeing on a number.
 *
 * `--sf-cat-mrows` is the model count, published inline by `category-table.tsx`:
 * `repeat()` needs an integer and only the render knows it. `1 / -1` is the
 * whole explicit grid, which is why the track list has to be explicit rather
 * than left to `grid-auto-rows`.
 *
 * `__mspecs` becomes `display: contents` so the swiped columns are grid items of
 * `__mgrid` itself. It was a flex row whose only job was to hold them side by
 * side, which is the outer grid's job now.
 *
 * SUBGRID IS BASELINE across every engine this storefront targets (Chrome/Edge
 * 117, Safari 16, Firefox 71) — a later floor than the `:has()` this sheet
 * already depends on, and it degrades the way this composition can afford:
 * without it each column falls back to its own row track sizing, which is the
 * misalignment described above rather than a broken page.
 */
.sf-cat-table__mgrid {
  display: grid;
  grid-auto-columns: max-content;
  grid-auto-flow: column;
  grid-template-rows: auto repeat(var(--sf-cat-mrows, 4), 148px);
  width: max-content;
}

.sf-cat-table__mpinned {
  background: var(--brand-color-surface, #ffffff);
  box-shadow: 6px 0 14px rgba(23, 24, 26, 0.08);
  display: grid;
  grid-row: 1 / -1;
  grid-template-rows: subgrid;
  left: 0;
  position: sticky;
  width: 128px;
  z-index: 2;
}

/*
 * THE SPECIFICATION HEADER, AND THE DESKTOP'S WEIGHT HIERARCHY AT MOBILE SCALE
 * (Ryan's ruling, 2026-08-27: the label BOLDER, the description lighter
 * beneath it — "just like you've weighted that properly for the desktop
 * version").
 *
 * The desktop states that hierarchy as ink-600 over muted-400 in a smaller cut
 * (`.sf-cat-table__label` / `.sf-cat-table__explain`), and it is restated here
 * in this composition's own vocabulary rather than copied: the label keeps the
 * mono, tracked, uppercase eyebrow it has always been and goes to 700 in INK,
 * and the sentence under it is the body face, unmodified case, muted, 400. Two
 * axes of contrast where the desktop has one, because at 10.5px weight alone
 * is not enough of a difference to read as a hierarchy.
 *
 * IT IS NO LONGER A FIXED 35px BAND. The board's 34+1 was the height of a
 * one-word header; a header that now carries a sentence sizes itself, and the
 * grid above sizes the ROW to the tallest of them. 35 survives as the `min-height`
 * so a table of one-word headers still reproduces the board exactly.
 *
 * TOP-ALIGNED, not centred: with the row as tall as its tallest header, centring
 * would put every short label on a different baseline from its neighbours, and
 * the labels reading as one line across the strip is what makes the header row
 * scannable at all.
 */
.sf-cat-table__meyebrow {
  align-items: stretch;
  background: var(--brand-color-well, #f1f0ec);
  border-bottom: var(--border-control);
  color: var(--brand-color-muted, #6c6a62);
  display: flex;
  flex-direction: column;
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-micro);
  justify-content: flex-start;
  letter-spacing: 0.12em;
  /* 35, not the board's authored 34: the boards run in the browser's default
   * CONTENT BOX, so a 34px band with a 1px rule under it RENDERS 35px tall, and
   * the board's rendered pixels are the specification. Everything on this site
   * is border-box, so the number that reproduces the drawing is the outer one —
   * the same conversion the hero's 30px orbs needed. */
  min-height: 35px;
  /* NO PADDING OF ITS OWN any more, and that is the mechanism rather than
   * tidiness: the band and the support area under it are two GROUNDS, and a
   * shared inset would have run the band's tint short of the column rules on
   * both sides — a mini-row with a gap at each end is not a row. The inset is
   * PUBLISHED here instead and applied by each half, so the two grounds still
   * agree on where the text starts. The board's is 12 in the pinned column and
   * 14 in a swiped one (see the width conversion below), which is why it is a
   * variable and not a number. */
  --sf-cat-mepad: 12px;
  padding: 0;
  text-transform: uppercase;
}

/*
 * THE LABEL BAND — "it should look like another row" (Ryan's ruling,
 * 2026-08-27).
 *
 * The header used to be one box: label, then sentence, then whatever tinted
 * space the tallest header in the table left over. On the short headers that
 * left-over was most of the box — "MODEL" was 35px of label above 65px of empty
 * well — and it read as a hole punched in a label rather than as a header with
 * nothing more to say.
 *
 * SPLITTING IT IN TWO fixes both halves at once. The BAND is a fixed 34px on a
 * ground one step darker than the header's, so the fourteen labels sit on one
 * continuous strip straight across the swipe and read as a row of their own. The
 * SUPPORT AREA below it is the header's normal `well`, and it is the only part
 * that varies — so a column with no sales point simply has more of the ground it
 * was already standing on, and nothing looks unfinished.
 *
 * `line` is the tint, from the vocabulary rather than mixed: #E8E7E3 against
 * `well`'s #F1F0EC is one step of the same grey ramp the sheet uses everywhere
 * else, and it is a 9/255 delta — enough to read as a band, nowhere near enough
 * to read as a second colour.
 *
 * BOTTOM-ALIGNED, which is the ruling's own word and is what makes the band read
 * as a row rather than as a caption: the label hugs the boundary it shares with
 * the sentence it introduces, and the air goes above it where the column rules
 * are already drawing verticals.
 *
 * 34 + the 1px rule under the whole header is the board's own 35px band, so a
 * table with no sales points anywhere on it is still drawn exactly as the board
 * drew it — the same promise the old `min-height: 35px` made and the reason it
 * survives above.
 */
.sf-cat-table__mband {
  align-items: flex-end;
  background: var(--brand-color-line, #e8e7e3);
  display: flex;
  min-height: 34px;
  padding: 8px var(--sf-cat-mepad, 12px);
}

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

/*
 * THE SALES POINT UNDER IT. The same `Explanation` element the desktop label
 * column renders — one renderer, so the two compositions cannot drift — wearing
 * this composition's type.
 *
 * Every one of the eyebrow's five type declarations has to be named back,
 * because they are all inherited: the mono face, the micro size, the tracking
 * and the uppercasing are the LABEL's costume, and a sentence wearing them is
 * unreadable at 10.5px. `--text-caption` reads as prose beside a tracked
 * 10.5px label without out-shouting it — the size relationship inverts from the
 * desktop's and the hierarchy does not, because here the label carries case,
 * tracking, face and weight instead.
 */
.sf-cat-table__meyebrow .sf-cat-table__explain {
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-caption);
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.35;
  /* TOP-ALIGNED UNDER THE BAND, which is what the eyebrow's `flex-start`
   * already does — this only sets the gap. It carries the eyebrow's published
   * inset itself, because the eyebrow no longer applies one (see the band). */
  margin-top: 0;
  padding: 6px var(--sf-cat-mepad, 12px) 8px;
  text-transform: none;
}

/*
 * THE SWIPED COLUMN'S WIDTH, converted the same way.
 *
 * The board authors 150px (210 for the one wide column) and means CONTENT: on
 * content-box those columns render 150 + 28px of padding + the 1px rule = 179.
 * Taking the authored number as a border-box width would squeeze every
 * specification into 122px and shift the whole strip 29px per column — the
 * widest single error a transplant of this table can make. `--sf-cat-mcol`
 * carries the board's own number and the conversion happens here, once.
 */
.sf-cat-table__mcol .sf-cat-table__meyebrow,
.sf-cat-table__mcol .sf-cat-table__mcell {
  border-left: var(--border-hairline);
  width: calc(var(--sf-cat-mcol, 150px) + 29px);
}

/*
 * THE 28px OF IT, applied where each box can take it. A cell takes it directly;
 * the HEADER hands it to the two grounds inside it instead, so the label band
 * still runs rule to rule while its text starts on the same 14px as the figures
 * below. The border-box width above is unchanged either way — 150 + 28 + the 1px
 * rule — so the strip's geometry is exactly what it was.
 */
.sf-cat-table__mcol .sf-cat-table__mcell {
  padding-left: 14px;
  padding-right: 14px;
}

.sf-cat-table__mcol .sf-cat-table__meyebrow {
  --sf-cat-mepad: 14px;
}

.sf-cat-table__mmodel {
  background: var(--brand-color-surface, #ffffff);
  border-bottom: var(--border-control);
  height: 148px;
  padding: 10px 12px;
}

/* The mcell half is COMPOUNDED and the mmodel half is not, and the asymmetry is
 * the cascade, not an oversight: `.sf-cat-table__mmodel` is declared ABOVE this
 * rule and `.sf-cat-table__mcell` below it, so at equal specificity a bare
 * `--tint` wins against the first and loses to the second. Compounding buys the
 * one that needs it a class of specificity — the same move the highlight rules
 * further up make against this very stripe. */
.sf-cat-table__mmodel--tint,
.sf-cat-table__mcell.sf-cat-table__mcell--tint {
  /* `background-color`, NEVER the `background` shorthand: the shorthand resets
   * `background-image`, and the AI run wash IS a background image layered over
   * this one. See the AI-highlight block at the foot of this sheet. */
  background-color: var(--brand-color-paper, #fbfbfa);
}

.sf-cat-table__mphoto {
  display: block;
  height: 52px;
  margin: 0 auto;
  width: auto;
}

.sf-cat-table__mname {
  color: var(--brand-color-ink, #17181a);
  font-size: var(--text-support);
  font-weight: 700;
  margin-top: 4px;
  text-align: center;
}

.sf-cat-table__mmoney {
  /* The strut, as in the desktop header cell: the board declares no size on
   * this line, so its line box is the browser default 16px while both spans
   * inside it declare their own. globals.css 17px body would make it 2px
   * deeper and push the call to action down with it. */
  font-size: var(--text-emphasis);
  margin-top: 1px;
  text-align: center;
}

.sf-cat-table__mlist {
  color: var(--brand-color-muted, #6c6a62);
  font-size: var(--text-caption);
  text-decoration: line-through;
}

.sf-cat-table__mprice {
  color: var(--brand-color-good, #1f7a4d);
  font-family: var(--brand-font-mono, monospace);
  font-size: var(--text-caption);
  font-weight: 700;
}

.sf-cat-table__mcta-wrap {
  margin-top: 7px;
}

.sf-cat-table__mcta {
  background: var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  display: block;
  font-size: var(--text-caption);
  font-weight: 700;
  padding: 7px 0;
  text-align: center;
  text-decoration: none;
}

/* The swiped columns are grid items of `__mgrid` itself — see the `__mgrid`
 * block. This wrapper's only job was to lay them side by side, which is the
 * grid's job now, and a box that still generated one would break the subgrid
 * chain between them and the rows they have to line up on. */
.sf-cat-table__mspecs {
  display: contents;
}

/* No width of its own: a `max-content` grid column shrink-wraps its children,
 * and the children are the cells the conversion above has already sized.
 * `subgrid` over the whole track list is what lines this column's four model
 * rows up with the pinned column's. */
.sf-cat-table__mcol {
  display: grid;
  grid-row: 1 / -1;
  grid-template-rows: subgrid;
  scroll-snap-align: start;
}

/*
 * THE SWIPED CELL, AND THE SUPPORTING TEXT IT NOW CARRIES (Ryan's ruling,
 * 2026-08-27 — the desktop's weight hierarchy at mobile scale).
 *
 * It was a one-line box: a row of centred content, with `Value` returning the
 * figure alone on this side. The figure's supporting line and its bullets were
 * DROPPED, silently, and what a phone lost was the reason a number matters —
 * Diamond's "5'" without "Extra long — for regulation striping", every Best for
 * cell without the bullets naming who it is for.
 *
 * A COLUMN, THEN, not a row: the figure on its own line and the support under
 * it, the whole block still centred in the 148px the board draws. `column`
 * needs the two axes swapped — `justify-content` centres the block vertically
 * and `align-items: flex-start` keeps the lines left, which is where a row of
 * figures being compared has to start.
 *
 * THE 148 DOES NOT MOVE, and that is what makes this affordable: the board's
 * model row is four or five lines tall and was holding one. The support fits in
 * the room that was already there, which is why nothing below the strip shifts
 * and why the pinned column still lines up cell for cell.
 *
 * THE TYPE IS THE DESKTOP'S HIERARCHY, unmodified — `__value-note` and
 * `__bullets` are the SAME rules the desktop cell uses (muted, `--text-support`,
 * 1.5 leading), because the ruling is that the phone gets the desktop's
 * weighting rather than a phone-specific one. Only the leading tightens, below.
 */
.sf-cat-table__mcell {
  align-items: flex-start;
  background: var(--brand-color-surface, #ffffff);
  border-bottom: var(--border-control);
  color: var(--brand-color-ink, #17181a);
  display: flex;
  flex-direction: column;
  font-size: var(--text-body);
  /* Border-box here and NOT converted, because the board sets `box-sizing:
   * border-box` on this row itself: 148 is the whole row, rule included. The
   * pinned column's cells are the same 148, which is what keeps a model's row
   * lined up across the seam. */
  height: 148px;
  justify-content: center;
}

/*
 * THE SUPPORT AT MOBILE SCALE. Same colour, same size, same weight as the
 * desktop's — only the leading and the bullet indent tighten, because 1.5 lines
 * of 14.5px inside a 148px box that also holds a figure and up to three bullets
 * is the one place the desktop's air does not fit.
 */
.sf-cat-table__mcell .sf-cat-table__value-note {
  line-height: 1.3;
  margin-top: 2px;
}

.sf-cat-table__mcell .sf-cat-table__bullets {
  line-height: 1.3;
  margin-top: 4px;
  padding-left: 15px;
}

.sf-cat-table__mcell .sf-cat-table__bullets li {
  margin-top: 1px;
}


.sf-cat-table__mcell .sf-cat-table__rating {
  gap: 7px;
}

.sf-cat-table__mcell .sf-cat-table__review-count {
  font-size: var(--text-support);
}

/* -------------------------------------------------------- progress dots -- */

.sf-cat-table__dots {
  display: flex;
  gap: 6px;
  justify-content: center;
  padding-top: 12px;
}

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

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

/* ============================================== the AI conversation embed == */

/*
 * THE AI CONVERSATION SURFACE (tokens §4), transplanted from
 * CategoryPage.dc.html — the ask strip drawn beside the section header and the
 * embedded conversation drawn under it.
 *
 * WHAT THE BOARD DRAWS AND THE SPEC OVERRULES, recorded because the difference
 * is deliberate: the board draws a REPLY FIELD with its own dropdown inside the
 * conversation and a caption beside the "Continue this conversation" button.
 * Ryan's words-spec of 2026-08-25 ruled both out afterwards — the embed is ONE
 * question deep (asking again replaces the section; the widget is where an
 * actual conversation happens) and the button explains itself. The reply
 * field's drawing is still the specification for how the ASK field looks when
 * it is focused with its list open, which is the only place the board draws
 * that state, so it is transplanted onto the ask field instead.
 *
 * The 3px corner on the user bubble is the board's own value and is off the
 * radius scale on purpose (no normalization — §3 of the playbook): it is the
 * tail of a speech bubble, not a control corner.
 */

/*
 * THE HEAD ROW. Board: header left, ask field right, 24px apart, centred.
 *
 * The mobile swipe instruction is a third child of this flex box and is
 * `display: none` on desktop, so it is not a flex item there at all and the
 * row is exactly the board's two-item `space-between`. Below the mobile cut all
 * three stack and `order` puts the ask field under the instruction — see the
 * mobile block at the end of this section.
 */
.sf-cat-embed__row {
  align-items: center;
  display: flex;
  gap: 24px;
  justify-content: space-between;
  /* The suggestion list hangs out of the ask field and over the table below.
   * The table's pinned header cells are positioned with a z-index of their own
   * (gotcha #3), so "later in the document" is not enough to paint over them. */
  position: relative;
  z-index: 5;
}

/* The header is a flex item here and must not absorb the row's spare width —
 * the ask field is what grows.
 *
 * SCOPED to the embed's row, for the reason the mobile `order` rule at the foot
 * of this section is scoped: `.sf-sec-head` is the PLATFORM's header
 * and the photo section puts one in a flex row of its own, beside the "Browse
 * all" link. Unscoped, this rule stopped that header from growing there too —
 * the link then took the spare width and squeezed "Browse all" onto two lines
 * at ≤360. Second leak of this exact shape; the rule is that nothing in this
 * block may name a shared class without a scope in front of it. */
.sf-cat-embed__row .sf-sec-head {
  flex: none;
}

/* ------------------------------------------------------- the ask field -- */

/*
 * The QUIET ASK FIELD, with the star that says the answer comes from the AI.
 *
 * The star is the whole identity claim: this surface is EXPLICITLY the
 * assistant, where the chat widget stays deliberately ambiguous about who is
 * typing. Same two-star glyph the board draws, at the board's 16px.
 */
.sf-cat-embed__ask {
  align-items: center;
  background: var(--brand-color-surface, #ffffff);
  border: var(--border-control);
  border-radius: var(--radius-control);
  display: flex;
  flex: 1;
  gap: 10px;
  /* THE BOARD AUTHORS 520 AND RENDERS 554. Its span is content-box (browser
   * default; the board sets no reset), so its 520px cap is the CONTENT width
   * and the 16px paddings and 1px borders are added to it. This site is
   * border-box, so the rendered pixel is the number that has to be written —
   * the same trap the probe tool's own header calls out. */
  max-width: 554px;
  /* THE RIGHT PADDING RESERVES THE X's ROOM (Ryan's DD-C, reaching this field
   * via AI6 — "the revamp philosophy applies to ALL embed locations"). The
   * button is absolutely positioned and takes no width in the row, so "text can
   * never run under it" is the field's to guarantee.
   *
   * IT MOVES NO DRAWN PIXEL AT REST, and that is why the recorded `ask` gate
   * holds across it: the placeholder is 200-odd px of type in a ~496px input,
   * so shortening the run leaves every glyph where it was. The two sibling
   * fields reserve 44 the same way. */
  padding: 12px 44px 12px 16px;
  position: relative;
}

/* The list is flush under the field, so the two read as one control. */
.sf-cat-embed__ask[data-open="true"] {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/*
 * FOCUS IS DRAWN ON THE FIELD, not on the input inside it.
 *
 * The board draws its focused field as a 1.5px accent border, which is the
 * selection language everywhere else on the site (§1: 1.5px accent, never
 * normalised to 1px). A 1.5px BORDER would resize the box by half a pixel and
 * shift the row, so the extra half pixel is painted OUTSIDE the 1px border as a
 * spread shadow: same drawn thickness, same colour, zero layout change.
 *
 * WHAT THIS IS NOT: the focus INDICATOR. Ryan read 1.5px of drawn border as too
 * thin, and 1.5px is under the 2px the platform pattern requires anyway, so the
 * board's treatment now sits UNDER the ring rather than in place of it — the
 * ring is drawn on this same element, in the focus-ring block at the top of the
 * sheet. The input's own outline stays suppressed for the original reason (the
 * ring is on the field, which is the box the reader is looking at), and that is
 * the only thing suppression is ever allowed to mean.
 */
.sf-cat-embed__ask:focus-within {
  border-color: var(--brand-color-accent, #c4530a);
  box-shadow: 0 0 0 0.5px var(--brand-color-accent, #c4530a);
}

.sf-cat-embed__ask-mark {
  align-items: center;
  color: var(--brand-color-muted, #6c6a62);
  display: inline-flex;
  flex: none;
}

.sf-cat-embed__ask:focus-within .sf-cat-embed__ask-mark {
  color: var(--brand-color-accent, #c4530a);
}

/* An input inherits none of this, so all of it is written out. */
.sf-cat-embed__ask-input {
  background: none;
  border: 0;
  color: var(--brand-color-ink, #17181a);
  flex: 1;
  font-family: var(--brand-font-body, system-ui, sans-serif);
  font-size: var(--text-support);
  line-height: normal;
  margin: 0;
  /* Without this a flex item refuses to shrink below its content width and the
   * field grows past its 520px cap as the reader types. */
  min-width: 0;
  outline: none;
  padding: 0;
}

.sf-cat-embed__ask-input::placeholder {
  color: var(--brand-color-muted, #6c6a62);
  /* Firefox dims placeholders by default; the board's grey is the grey. */
  opacity: 1;
}

/* ------------------------------------------------------- the suggestions -- */

/*
 * THE WHOLE BLOCK IS RETIRED BY NAME (DD-UNIFY, Ryan's ruling 2026-08-28).
 *
 * What stood here: `.sf-cat-embed__menu`, `.sf-cat-embed__menu-list`,
 * `.sf-cat-embed__menu-eyebrow`, `.sf-cat-embed__option`, its `:last-child`
 * and its `--active`/`:hover` pair. Every one of those declarations also stood
 * in pdp.css as `.sf-pdp-ans__*` — two hand copies of one drawing, and a third
 * in photos.css that had drifted into a different one.
 *
 * They are `.sf-suggest*` in chrome.css now, VERBATIM: the same panel at the
 * field's border box with no top border of its own, the same 320px scroller,
 * the same mono eyebrow over its hairline, the same 9/16 rows. `tools/aigate.mjs`
 * holds the ask crops that prove it did not move.
 *
 * WHAT STAYED HERE is the FIELD's half of the seam — `.sf-cat-embed__ask` and
 * its `[data-open="true"]` radius rule, a few rules above. The field is this
 * surface's box, drawn to this surface's board; only the panel is shared.
 */


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

/*
 * The conversation card. Board: `.card` at `margin-top: 16px; padding: 18px
 * 22px`.
 *
 * `scroll-margin-top` is what parks the block clear of the pinned nav bar when
 * a question is asked — the same token the page's other stickies rest against.
 */
.sf-cat-embed {
  /*
   * TWELVE PIXELS, AND ONLY TWELVE. The chrome this block parks under is
   * already paid for by `scroll-padding-top` on `html` (chrome.css: the nav row
   * plus the stuck title band), and CSSOM-View ADDS the two — the scroll port's
   * padding and the target's own margin are separate terms of the same sum, not
   * alternatives. Declaring the chrome here as well double-counted it: measured
   * at 1440, the first ask parked this block at 294px instead of 153px, with a
   * dead strip of page between the stuck band and the conversation. (It hid
   * behind any answer that also parked a `scroll_to`, because that later scroll
   * re-landed the page correctly; answers with no tool call stayed wrong.)
   *
   * So `scroll-padding-top` owns the CLEARANCE and this owns the BREATHING
   * ROOM: the 12px of paper the board draws between the band's bottom edge and
   * the card's top. Landing = `--sf-pinned-offset + --sf-band-stuck-h + 12px`,
   * asserted live in `tools/aiguard.mjs` §B — a pixel diff cannot see a landing,
   * because scroll-padding changes no paint at any fixed scroll position.
   *
   * IT IS THE SAME 12 THE STUCK STACK RESTS AT, and as of 2026-08-26 it is the
   * same DECLARATION: `--sf-cat-stack-gap`, at the foot of this sheet. The block
   * used to land 12px under the band and then snap flush to it on the next
   * scroll-wheel notch, which is half of what Ryan read as the band crowding the
   * head. One token means the arrival and the stuck state cannot disagree again.
   */
  scroll-margin-top: var(--sf-cat-stack-gap, 12px);
}

/*
 * ─── THE WHOLE CONVERSATION BLOCK IS RETIRED BY NAME ────────────────────────
 * (the shared-embed-core extraction + Ryan's AI1-AI5, 2026-08-27/28)
 *
 * What stood here, and where each one went:
 *
 *   `.sf-cat-embed__convo`, `__convo:focus`, `__from-ai`, `__ai-mark`,
 *   `__answer`, `__answer-strong`, `__thinking`, `__dots`, `__dot`,
 *   `__dot:nth-child(2)`, `__dot:nth-child(3)`, `__actions`, `__continue`,
 *   `__clear`, `@keyframes sf-cat-embed-think`, and the reduced-motion rule on
 *   `__dot` further down this sheet
 *       -> `sections.css`, under `sf-embed-`, as ONE drawing shared by three
 *          surfaces. Every one of these declarations ALSO stood in pdp.css as
 *          `.sf-pdp-ans__*` — two hand copies of one card — and Ryan's R5 would
 *          have made a third with the reviews search.
 *
 *   `.sf-cat-embed__from-user`, `.sf-cat-embed__bubble` (and its mobile
 *   `max-width: 80%`)
 *       -> NOWHERE. AI1 retires the reader's own bubble outright: "the query
 *          STAYS in the search box — never cleared, never re-bubbled". The
 *          question is still in the field a few pixels above; a grey copy of it
 *          below was the mode-switch jar the ruling exists to remove.
 *
 * THREE OF THE FIVE RULINGS CHANGED THE DRAWING RATHER THAN MOVING IT, and the
 * new declarations carry their own reasoning where they now live: the box is
 * FAR NARROWER and right-aligned at the ask field's own measure (AI3), the
 * hand-off button is INK-FILLED with a mark at the right edge (AI4), and "Clear
 * Highlighting" is a quiet underlined link at the left (AI5). The "…" moved
 * with them into the answer's own place, which is Ryan's original complaint —
 * he never saw it, because it sat left while his eye was right.
 *
 * WHAT STAYED IN THIS SHEET is what is TABLE-COUPLED and nothing else: the
 * block's `scroll-margin-top` above, the at-rest table gap below, and the
 * sticky stack after it.
 */

/* The conversation sits ON the table, so the table's own 16px gap closes up.
 *
 * `--sf-cat-stack-foot` rather than the literal 8: this is the AT-REST half of
 * A3's ruled seam, and the STUCK half (the `--sf-cat-stack-h` rung at the foot
 * of this sheet) is the same number by construction. Two declarations of 8
 * would drift the first time either was tuned. */
.sf-cat-table-section[data-embed="active"] .sf-cat-table {
  margin-top: var(--sf-cat-stack-foot);
}

/* --------------------------------------------------- the sticky stack -- */

/*
 * THE STICKY STACK (Ryan's ruling, 2026-08-26) — what replaced the bounded
 * scroll mode, and why the replacement is smaller than the thing it replaced.
 *
 * THE OLD MODE. While a conversation was open the CARD became a scroll region
 * with a `max-height`, and `scroll_to` scrolled that container instead of the
 * page — so the sentence the reader was in the middle of could not move. It
 * worked, and Ryan ruled it out anyway: an inner scroller inside a page that
 * also scrolls is two scrollbars arguing, and the reader's wheel lands in
 * whichever one the pointer happens to be over.
 *
 * THE RULING, in his words: kill the inner scrollable box, the page is the only
 * scroller, always. When the assistant highlights something off-screen, scroll
 * the PAGE so the section head lands at the chrome; if the highlight is still
 * below the fold, keep scrolling — and while that happens the SECTION HEAD and
 * the EXPANDED AI SECTION stick below the chrome, with the table's model header
 * row below them, and the table's rows slide up BEHIND all of it. The reason he
 * gave is the same one the bounded mode existed for, reached the other way
 * round: never lose the question and answer that prompted the highlight.
 *
 * IT IS ORDINARY `position: sticky`, and that is the point. Nothing freezes a
 * scroll position, nothing fights the wheel, and manual scrolling behaves
 * exactly as the assistant's does — scroll up and the stack un-sticks on its
 * own, because that is what sticky does. There is no mode, no script, and
 * nothing for `prefers-reduced-motion` to turn off.
 *
 * THE OFFSET CASCADE, which is this codebase's established pattern and now has
 * a fourth rung:
 *
 *   chrome.css   `--sf-pinned-offset`    the pinned nav row               54px
 *   chrome.css   `--sf-band-stuck-h`     the stuck title band             87px
 *   HERE         `--sf-cat-stack-h`      the section head + conversation  var
 *   the header row's own `top`, below    the compact strip                55px
 *
 * Each rung sticks at the sum of the ones above it, so the four come to rest as
 * one column. `--sf-cat-stack-h` is `0px` unless a conversation is open, which
 * is what leaves the resting page — and every width below the cut, and every
 * brand that never loads this sheet — at exactly the offsets it had before this
 * block existed.
 *
 * TWO OF THE SEAMS ARE RULED GAPS, NOT ZEROES (Ryan's walkthrough, 2026-08-26).
 * The stack shipped packed: the section head's top edge on the stuck band's
 * bottom edge, and the conversation's bottom edge on the compact strip's top
 * edge. He read the first as the band crowding the head AND the ask field, and
 * the second as the table losing a gap it has at rest. So the rung is no longer
 * the block's height — it is the block's height PLUS its two seams:
 *
 *   `--sf-cat-stack-gap`   12px  band bottom -> stack top
 *   `--sf-cat-embed-h`     var   the block itself, measured
 *   `--sf-cat-stack-foot`   8px  stack bottom -> compact strip top
 *
 * NEITHER NUMBER IS NEW, and that is the point of both.
 *
 *  - 12 is the paper the board draws between the band's bottom edge and the
 *    card's top, and it is ALREADY this block's `scroll-margin-top` — where an
 *    ask parks the conversation. It was 12 on arrival and 0 a scroll-wheel
 *    notch later; the two are now one declaration, so the block lands where it
 *    sticks and sticks where it lands.
 *  - 8 is the gap the table already takes while a conversation is open ("the
 *    conversation sits ON the table", further up this sheet). Retaining it in
 *    the stuck state is A3's whole content: the same seam in both states.
 *
 * FOLDED INTO `--sf-cat-stack-h` RATHER THAN ADDED AT EACH READER. Three places
 * consume this rung — the sentinel's `scroll-margin-top`, the model row's
 * sticky `top`, and `stackHeight()` in table-highlights.ts through the first of
 * them. A gap added to two of the three is exactly the invisible-at-rest defect
 * the cascade test exists to catch, so the rung carries its own seams and the
 * readers are untouched. The stack's OWN `top` is the exception, and has to be:
 * it counts the gap above it and nothing else, because counting its own height
 * would be circular.
 *
 * `--sf-cat-embed-h` is the block's MEASURED height, published on the section by
 * `ai-embed.tsx` for the reason every other length in this chrome is measured:
 * it depends on how long the answer is, on where it wraps, and on which
 * breakpoint's recipe is in force. The bounded mode used the same number to size
 * its region; it is the same observer, pointed at a different job.
 *
 * IT IS NO LONGER SCOPED TO THE 768 CUT (Ryan's ruling, 2026-08-27: "keep the
 * desktop functionality the same here" — THE ANSWER MUST NEVER SCROLL AWAY).
 *
 * It shipped desktop-only, and the flag above this line said so: on a phone the
 * stack is the section head, a full-width ask field and the conversation, and a
 * phone would be reading the table through a letterbox. That objection was
 * measured and it was put to Ryan as measured — the stack takes most of a phone
 * viewport before a single table row appears under it — and he ruled anyway,
 * knowingly, on the ground that "it's just supplemental information": the
 * ANSWER is the product of this surface and the table is the supplement, so
 * losing the answer to keep the supplement on screen is the wrong trade at any
 * width. A highlight that cannot clear the stack on a phone is ACCEPTED.
 *
 * WHAT THE MOBILE STACK IS, and it is one rung shorter than the desktop's: the
 * band's stuck line (77px on a phone since the 2026-08-27 title-band ruling),
 * then this block — section head, ask field, conversation. There is NO compact
 * model strip below it, because the transposed table has no pinned model row to
 * compact: its model column pins sideways, not down, and the strip's own rule
 * is scoped to 1024. So the mobile cascade is chrome -> title line -> head and
 * conversation, and the transposed table scrolls under all three.
 *
 * `--sf-cat-stack-h` is therefore consumed on a phone by exactly one reader —
 * the sentinel's `scroll-margin-top`, and through it `stackHeight()` in
 * table-highlights.ts, which is what makes the assistant's own `scroll_to` land
 * a highlight under the stack rather than behind it. The model row's sticky
 * `top` reads the same rung and is simply not in force below 1024.
 *
 * THE GROUND IS PAINTED, and it has to be: the section head and the ask field
 * have no background of their own, and rows sliding up behind a transparent
 * stack would read straight through it. `--brand-color-paper` is the ground the
 * page already draws there (measured `rgb(251,251,250)` on `body`), so the fill
 * is invisible at rest and opaque in motion.
 *
 * `z-index: 10` is over the table's own pinned furniture — `__head` at 2 and
 * `__head--label` at 3 — and under the title band's 20, which is under the
 * header's 40. (The AI note chip sat at 4 here until 2026-08-26, when Ryan
 * ruled it UNDER the pinned bar at 1; nothing in this table declares 4 now.) The conversation's own controls (Continue,
 * Clear Highlighting) stay clickable while stuck because a sticky element is
 * still in normal flow; the z-index only settles who paints over whom.
 */

.sf-cat-table-section {
  /* The two ruled seams, declared unconditionally so the resting page's own
   * `margin-top` rule can read the foot from the same place the stuck rung
   * does — one number, two states, no drift. */
  --sf-cat-stack-gap: 12px;
  --sf-cat-stack-foot: 8px;
  --sf-cat-stack-h: 0px;
}

.sf-cat-table-section[data-embed="active"] {
  --sf-cat-stack-h: calc(
    var(--sf-cat-stack-gap) + var(--sf-cat-embed-h, 0px) + var(--sf-cat-stack-foot)
  );
}

.sf-cat-table-section[data-embed="active"] .sf-cat-embed {
  background: var(--brand-color-paper, #fbfbfa);
  /*
   * THE GAP'S GROUND, and it has to be painted for the same reason the
   * block's own is: rows travelling up behind a transparent 12px strip read
   * straight through it, and this strip is covered by neither neighbour — the
   * band ends at its own bottom edge and the block now starts 12px lower.
   *
   * A BOX-SHADOW rather than padding, a border or a pseudo-element, because
   * this rule may not move a single pixel of LAYOUT. `padding-top` here would
   * push the head and the conversation 12px down the RESTING page and take
   * the document height with them — which is aiguard §B(4), the invariant
   * that says `data-embed` changes the document by the 8px table gap and
   * nothing else. A shadow is paint only: offset up by the gap, no blur, no
   * spread, so it is exactly this block's own box translated onto the strip
   * above it — and it is paper on paper, therefore invisible, in every state
   * but the stuck one.
   */
  box-shadow: 0 calc(-1 * var(--sf-cat-stack-gap)) 0 0 var(--brand-color-paper, #fbfbfa);
  position: sticky;
  top: calc(
    var(--sf-pinned-offset, 0px) + var(--sf-band-stuck-h, 0px) +
      var(--sf-cat-stack-gap)
  );
  z-index: 10;
}

/*
 * ─── MC6: THE STICKY BOX KEEPS A 15px FRAME WHILE THE TABLE RUNS UNDER IT
 *     (Ryan, 2026-08-29) ──────────────────────────────────────────────────
 *
 * "When it's sticky and the table scrolls under it, the table just vanishes at
 * the box's edge with nothing delineating why. Hold the 15px buffer between the
 * AI section's bottom and the table AT ALL TIMES — a buffer all the way around
 * it, even when it's sticky and it's scrolling."
 *
 * WHAT HE SAW, MEASURED at 390 mid-scroll: the answer card's bottom border at
 * y=507.81 and the table's next row starting at 507.81. The block's painted
 * ground stops exactly where its card does, so a row travelling up is cut in
 * half by a hairline that reads as a rendering fault rather than an edge. The
 * SIDES were never the problem and are the model for the fix: the card is
 * inset 15px from each gutter, so paper already frames it left and right — the
 * bottom was the one edge with no ground under it.
 *
 * TWO DECLARATIONS, AND NEITHER IS NEW MACHINERY.
 *
 *   THE SEAM BECOMES THE GUTTER. `--sf-cat-stack-foot` is A3's ruled gap
 *   between the stack's bottom and what follows it, read in BOTH states from
 *   one place — the at-rest table's `margin-top` and the `--sf-cat-stack-h`
 *   rung. Re-declaring it here therefore moves the rest state and the stuck
 *   state together, which is what "at all times" means.
 *
 *   IT IS A LITERAL AND NOT `var(--sf-gutter)`, and the reason is scope rather
 *   than preference: `--sf-gutter` is declared ON `.sf-container`, which is a
 *   DESCENDANT of this section, so a `var()` here would silently take its
 *   fallback and stop tracking the ruling it claims to read — the quietest kind
 *   of wrong. `tools/aiguard.mjs` asserts the computed foot against the
 *   container's computed gutter instead, so a future gutter re-rule fails
 *   loudly here rather than drifting.
 *
 *   AND THE SEAM IS PAINTED. The block already casts its own paper onto the
 *   12px strip ABOVE it, for exactly this reason and by exactly this device; a
 *   second shadow does the same below. Paint, not layout — `padding-bottom`
 *   here would push the card up the RESTING page and take the document height
 *   with it, which is the invariant `aiguard`'s §B(4) exists to hold. At rest
 *   the shadow lands in the gap the table's own margin has just opened, so it
 *   covers nothing; stuck, it is the 15px of ground the rows disappear under.
 *
 * SCOPED TO THE PHONE CUT, because the ruling is. At the wide cuts the stack
 * has a fourth rung under it — the compact model strip — and its 8px seam is
 * against that strip rather than against bare rows; nothing has been reported
 * there and the recorded desktop numbers stay recorded. FLAGGED for Ryan: one
 * media query moves it up the ladder if he wants the frame everywhere.
 *
 * THE TOP SEAM IS STILL 12. It is A3's ruled gap between the band and the
 * stack — a distance to the CHROME, not to the table — and MC6 is about the
 * table. FLAGGED as a judgment call rather than taken silently.
 */
@media (max-width: 767.98px) {
  .sf-cat-table-section {
    /* THE RULED PHONE GUTTER (chrome.css: "15px everywhere. Settled.") — see
     * the note above on why this is a literal and where it is gated. */
    --sf-cat-stack-foot: 15px;
  }

  .sf-cat-table-section[data-embed="active"] .sf-cat-embed {
    box-shadow:
      0 calc(-1 * var(--sf-cat-stack-gap)) 0 0 var(--brand-color-paper, #fbfbfa),
      0 var(--sf-cat-stack-foot) 0 0 var(--brand-color-paper, #fbfbfa);
  }
}

/* ------------------------------------------------------ reduced motion -- */

/* THE THINKING DOTS' REDUCED-MOTION RULE RETIRED WITH THEM (the shared-embed-
 * core extraction, 2026-08-28) — it is `sections.css`'s now, beside the
 * animation it stills, along with the AI2 reveal's own reduced-motion pair. */

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

/*
 * THE MOBILE ASK FIELD IS DERIVED — CategoryMobile.dc.html draws none.
 *
 * The header row becomes a stack: heading, then the ask field full width under
 * it. `order` rather than a different DOM order because the desktop arrangement
 * is the one the board draws and the desktop is the one with a pixel gate on
 * it. (The stack was three deep until 2026-08-26 — the swipe instruction sat
 * between them, and Ryan retired it.)
 */
@media (max-width: 767.98px) {
  .sf-cat-embed__row {
    align-items: stretch;
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  /* SCOPED to the embed's row. Unscoped this reordered the section header in
   * every section that has one — the photo section's header row is the same
   * `.sf-sec-head` beside a link, and an inherited `order: 1` put the
   * link to its left. */
  .sf-cat-embed__row .sf-sec-head {
    order: 1;
  }

  .sf-cat-embed__ask {
    margin-top: 14px;
    max-width: none;
    order: 2;
  }

  /* `.sf-cat-embed__convo`'s phone padding and `.sf-cat-embed__bubble`'s
   * `max-width: 80%` ARE RETIRED BY NAME with the card and the bubble (the
   * shared-embed-core extraction + AI1). The box's phone treatment is
   * `sections.css`'s — the cap is wider than any phone, so the box fills the
   * column and only its action line has a rule at this rung. */
}

/* --------------------------------------------------- AI highlight states -- */

/*
 * APPLIED BY `table-highlights.ts`, AND BY NOTHING ELSE. Gotcha #2 is a design
 * constraint, not an implementation detail: the highlight has to be a CLASS ON
 * A CELL. An absolutely positioned outline fights the sticky header above it,
 * and with `border-collapse: collapse` a 2px cell border is centred on the grid
 * line while a content-box overlay drifts by its own border width.
 * `TABLE_HIGHLIGHT_CLASSES` is the contract these answer to.
 *
 * Inset shadows rather than borders: a border on a collapsed cell steals space
 * from its neighbour and shifts the whole column by a pixel, which is the one
 * thing a highlight may not do.
 *
 * TWO THINGS KEEP THE WASH FROM LOSING A FIGHT IT SHOULD NOT BE IN, and both
 * were paid for in a measured wrong pixel:
 *
 *  1. THE RUN WASH IS A background-IMAGE, not a background-colour. Every
 *     background under it belongs to somebody else — the mobile row STRIPE
 *     (`__mcell--tint`), the desktop row stripe (`__row--tint`), the tinted
 *     label column — and replacing those would delete a stripe at the exact
 *     moment the assistant pointed at it. A background image LAYERS, so both
 *     survive and the wash reads on either ground. The single-cell highlight is
 *     the deliberate exception: one cell singled out gets the full accent-soft
 *     ground, because that is the whole point of singling it out.
 *  2. NOTHING UNDER THE WASH WRITES THE `background` SHORTHAND ANY MORE. The
 *     shorthand resets `background-image` to none, so any ground the wash can
 *     land on would have wiped it off no matter how specific this selector was,
 *     if it were ever declared after this block. FIVE grounds exist and all
 *     five now write `background-color`, which touches only the layer it
 *     names: the three compounds this block names for a ground of their own —
 *     `__mcell--tint`, `__mmodel--tint`, `__row--tint` — plus the two BASE
 *     cells that those modifiers sit on and that a run paints far more often
 *     than any of them: `__label`, the tinted first column every desktop ROW
 *     run runs straight through, and `__head`, which every model COLUMN run
 *     starts at. Each carries the warning at its own site so the trap is
 *     visible where it would be re-introduced.
 *
 *     `__compact-cell` IS STILL IN THE SELECTOR LISTS BELOW and no longer has a
 *     ground of its own: Gold's featured-model wash was the ground it used to
 *     have, and that treatment is retired (Ryan, 2026-08-26). The compound is
 *     kept because the strip BEHIND those cells is opaque, so a compact cell is
 *     one declaration away from needing a ground again, and a compound that
 *     wins is cheaper than re-deriving why it has to.
 *
 *     WHAT THAT DOES AND DOES NOT BUY. With no shorthand left, the wash — a
 *     background-IMAGE — no longer contends with anything at all, so it lands
 *     order-independently everywhere. The single-cell `--highlight`, which
 *     writes background-COLOR, still contends; that is what the compounds are
 *     for, and `__label` is now among them.
 *
 *     `__head` DELIBERATELY IS NOT, and this is the one place the sheet still
 *     leans on being LAST. A `.sf-cat-table__head.<highlight>` compound would
 *     be (0,2,0) — a tie with `.sf-cat-table[data-driven] .sf-cat-table__head`,
 *     which is where the pinned header row gets `position: sticky`. Declared
 *     later, this block's `position: relative` would win that tie and UNSTICK
 *     the header row for as long as a header cell were highlighted. So header
 *     cells keep the bare selector, which loses that tie and leaves the pin
 *     alone; their background-colour ordering rides on this block's position at
 *     the foot of the sheet, and moving the block is what would break it.
 *     Everything else is independent of declaration order, which is what the
 *     pass-2 stripe bug taught.
 *
 * THE NAMES CARRY GEOMETRY, NOT SEMANTICS, and that is what makes the
 * transposition work. `--highlight-column` insets its accent LEFT and RIGHT:
 * rails that run down a VERTICAL run. `--highlight-row` insets TOP and BOTTOM:
 * rails along a HORIZONTAL band. The engine picks by the run's DIRECTION, so on
 * the transposed table — where models are rows and specifications are columns —
 * a model highlight paints `--highlight-row`'s class and a specification
 * highlight paints `--highlight-column`'s. Picking by the name instead would
 * draw rungs ACROSS each run rather than rails along it. The mapping lives in
 * `table-highlights.ts`, which is the only thing that knows which composition
 * it is painting.
 *
 * END CAPS are deliberately not drawn: a run is two rails with open ends, which
 * is what the desktop board draws. That is a look, and looks are Ryan's.
 *
 * `position: relative` on all three: the note chip below anchors to the cell it
 * belongs to, and without a positioned cell it would resolve against the table
 * and land somewhere else entirely.
 */
.sf-cat-table__cell--highlight,
.sf-cat-table__mcell.sf-cat-table__cell--highlight,
.sf-cat-table__mmodel.sf-cat-table__cell--highlight,
.sf-cat-table__meyebrow.sf-cat-table__cell--highlight,
.sf-cat-table__label.sf-cat-table__cell--highlight,
.sf-cat-table__compact-cell.sf-cat-table__cell--highlight {
  background-color: var(--brand-color-accent-soft, #fbefe5);
  box-shadow: inset 0 0 0 2px var(--brand-color-accent, #c4530a);
  position: relative;
}

.sf-cat-table__cell--highlight-column,
.sf-cat-table__mcell.sf-cat-table__cell--highlight-column,
.sf-cat-table__mmodel.sf-cat-table__cell--highlight-column,
.sf-cat-table__meyebrow.sf-cat-table__cell--highlight-column,
.sf-cat-table__label.sf-cat-table__cell--highlight-column,
.sf-cat-table__compact-cell.sf-cat-table__cell--highlight-column {
  background-image: linear-gradient(rgba(196, 83, 10, 0.04), rgba(196, 83, 10, 0.04));
  box-shadow:
    inset 2px 0 0 0 var(--brand-color-accent, #c4530a),
    inset -2px 0 0 0 var(--brand-color-accent, #c4530a);
  position: relative;
}

.sf-cat-table__cell--highlight-row,
.sf-cat-table__mcell.sf-cat-table__cell--highlight-row,
.sf-cat-table__mmodel.sf-cat-table__cell--highlight-row,
.sf-cat-table__meyebrow.sf-cat-table__cell--highlight-row,
.sf-cat-table__label.sf-cat-table__cell--highlight-row,
.sf-cat-table__compact-cell.sf-cat-table__cell--highlight-row {
  background-image: linear-gradient(rgba(196, 83, 10, 0.04), rgba(196, 83, 10, 0.04));
  box-shadow:
    inset 0 2px 0 0 var(--brand-color-accent, #c4530a),
    inset 0 -2px 0 0 var(--brand-color-accent, #c4530a);
  position: relative;
}

/* The note the AI anchors to a cell it has highlighted: a filled accent chip
 * hanging off the cell's top edge, out of flow so it cannot change a column's
 * width.
 *
 * `z-index: 1` IS THE WHOLE POINT OF THIS DECLARATION, and it is RULED rather
 * than measured (Ryan's walkthrough, 2026-08-26). It read 4 until then, which
 * drew the chip OVER the pinned model bar — flagged here at the time as a look
 * nobody had chosen. He chose: a note chip is TABLE CONTENT, so it slides UNDER
 * the bar exactly as the rows it annotates do.
 *
 * The number is the one that flag named. The chip's host cell is `position:
 * relative` with `z-index: auto`, so it opens no stacking context of its own
 * and the chip competes directly with the table's pinned furniture:
 * `.sf-cat-table__head` at 2 and `.sf-cat-table__head--label` at 3, the latter
 * being the cell the COMPACT STRIP lives inside. 1 is under both, and still
 * over the rows — which it needs, because the chip hangs 14px above its own
 * cell and therefore over the row ABOVE it. `auto` would not be enough against
 * a HIGHLIGHTED neighbour: the run wash gives those cells `position: relative`,
 * so they paint in the positioned phase, where a later-in-document sibling
 * beats a `z-index: auto` chip.
 *
 * `scrollToTarget` does the other half, and it is LOAD-BEARING now rather than
 * cosmetic: an arrival at a cell carrying a chip adds this rule's own 14px of
 * overhang to its margin, so the chip lands BELOW the bar instead of behind it.
 * Over-painting made a short arrival merely ugly; under-painting makes it
 * invisible. */
.sf-cat-table__cell-note {
  background: var(--brand-color-accent, #c4530a);
  border-radius: var(--radius-control);
  color: var(--brand-color-surface, #ffffff);
  font-size: var(--text-caption);
  font-weight: 600;
  left: 14px;
  padding: 5px 11px;
  pointer-events: none;
  position: absolute;
  top: -14px;
  white-space: nowrap;
  z-index: 1;
}

/* ============================================================== reviews == */

/*
 * THE REVIEW BAR LEFT THIS SHEET on 2026-08-26, with the section header, to
 * `public/storefront/sections.css` under the `sf-sec-` prefix. It was a PLATFORM
 * pattern (tokens §4) drawn on two pages from two boards, and it lived in two
 * sheets held together by a pairwise parity test; there is one copy now, and the
 * test is retired with the duplication it guarded.
 *
 * WHERE IT SITS is the only thing this page ever said about it that the homepage
 * did not, and that survives as a MODIFIER the markup renders:
 * `.sf-sec-reviews--stage` — inside the hero's white stage, pulled 95px up so
 * its top lands at the bottom of the system's cast shadow, and its own paper
 * band below the mobile cut. The hero's `padding-bottom` still meets it here:
 * `.sf-cat-hero` keeps the board's 64px at 1440 and drops to 0 at the mobile cut
 * because the bar's own 52px margin closes the white there instead.
 */

/* ------------------------------------ WP3: this surface's count line ----- */

/*
 * THE RESULT-COUNT LINE SITS UNDER ITS FIELD (Ryan's WP3, 2026-08-29).
 *
 * The drawing is the shared one (`.sf-embed-field__status`, sections.css); what
 * belongs to THIS surface is where it lands, and that is a fact about this
 * surface's row rather than about the line: `.sf-cat-embed__row` is
 * `space-between` above the phone cut, so the ask field sits at the RIGHT and a
 * line starting at the block's left edge would read as a caption on the section
 * header instead of an answer from the field.
 *
 * BELOW THE CUT NOTHING IS DECLARED, deliberately: the row stacks there (the
 * field takes `max-width: none` and a top margin), the field is the full
 * measure, and the line's own left edge is already the field's.
 */
@media (min-width: 768px) {
  .sf-cat-embed .sf-embed-field__status {
    text-align: right;
  }
}
