/*
 * THE POP-UP SHELL — one structure for every modal on this storefront.
 *
 * ═══ W26, RYAN'S STANDING RULE (2026-09-02) ═══
 *
 * "I've seen a lot of different pop-ups and drawers… let's start to get real
 * consistent on how all these things are going to be working."
 *
 * This sheet is that consistency, and it exists because two files predicted it.
 * `delivery.css`'s header ends "Nothing here is written as a reusable modal kit
 * — the second modal to be built is what will say where the seam belongs", and
 * `installer-modal.tsx`'s says the five frame behaviours are "REPEATED here
 * rather than extracted, deliberately, and it is a flagged debt… THE THIRD
 * MODAL SHOULD DO THE EXTRACTION". Ryan's rule is what forced the debt to be
 * paid before a third modal arrived.
 *
 * ═══ THE REFERENCE IS THE DELIVERY-TIMELINE POP-UP ═══
 *
 * Ryan's words on the mobile walkthrough: it is "looking pretty good", and what
 * he named about it is exactly what this sheet moves — the panel's edge reading
 * top/left/right against the scrim, and the close cross at the top right. The
 * installer pop-up was measured beside it at 390 and was "not even close":
 * scrim `padding: 20px 0` and `border-radius: 0` put its panel at x=0, w=390,
 * with no edge to read at all. Every number below is the delivery modal's own,
 * so the reference did not move — the other instantiation came to it.
 *
 * ═══ HOW IT IS WORN: TWO CLASSES, ONE STRUCTURE ═══
 *
 * Every element carries the shared `sf-modal__*` class AND the modal's own
 * (`sf-dlv__panel`, `sf-inst__panel`). That is deliberate and it is not
 * belt-and-braces:
 *
 *   - THE SHELL PROPERTIES LIVE HERE AND ONLY HERE. `delivery.css` and
 *     `installers.css` no longer declare the scrim wash, the panel's ground,
 *     radius or shadow, the rise, or the close cross's box — so there is no
 *     cascade race between two sheets whose load order is React's business.
 *   - WHAT REMAINS PER-MODAL IS WHAT GENUINELY DIFFERS: the panel WIDTH (780 vs
 *     820, both board figures), the footer button's TREATMENT (accent vs ghost),
 *     and each modal's own content rhythm at its own rung.
 *   - AND THE RECORDED GATES KEEP THEIR SELECTORS. `dlvgate.mjs` reads
 *     `.sf-dlv__panel`, `.sf-dlv__scrim`, `.sf-dlv__close` and `.sf-dlv__footer`
 *     by name in nine places, and `instmodal.mjs` reads `.sf-inst__panel` and
 *     `.sf-inst__foot`. A rename would have been a gate rewrite disguised as a
 *     refactor, and a gate rewritten in the same pass as the thing it gates
 *     proves nothing.
 *
 * Served as a static asset and pulled in by `<ModalStyles>` rather than
 * `import`ed — same mechanism and same reasoning as chrome.css, delivery.css and
 * installers.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.
 *
 * Class prefix `sf-modal-`.
 */

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

/*
 * The two controls this sheet owns, on the two grounds it puts them on — the
 * platform pattern and its contrast arithmetic are documented once, at the top
 * of chrome.css.
 *
 * The close cross sits on the panel's surface white or a head band's well tint
 * (accent 4.58:1 and 4.01:1); both clear the 3:1 bar.
 */
.sf-modal__close:focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--brand-color-accent, #c4530a));
  outline-offset: var(--sf-focus-ring-offset, 2px);
}

/*
 * The scrim can hold focus: it is the modal's scroll container, and Chrome makes
 * an overflowing scroll container focusable so it can be scrolled without a
 * pointer. INSET, because it is the viewport — an outward ring on a full-bleed
 * box is drawn off-screen. WHITE, and this is the one control in the storefront
 * that takes neither the accent nor the accent-on-dark ring: its ground is its
 * own wash, which caps the ground's luminance at 0.220 (the composite over pure
 * white), where accent reads 1.16:1 and accent-on-dark 2.24:1 — both fail —
 * while surface white reads 3.89:1 and only climbs as the page behind darkens.
 */
.sf-modal__scrim:focus-visible {
  outline: 2px solid var(--brand-color-surface, #ffffff);
  outline-offset: var(--sf-focus-ring-inset, -2px);
}

/*
 * W174 RETIRES THE ON-DARK RING (W71b): the header's ground is `well` now, not
 * navy, and the base `.sf-modal__close:focus-visible` rule two rules up
 * already names this exact ground in its own comment — "the close cross sits
 * on the panel's surface white OR a head band's well tint (accent... 4.01:1)"
 * — written before W71b ever landed and never wrong, only unused for one
 * pass. No scoped override is needed for the header any more than one was
 * needed before W71b: both the bare corner and the header row are light
 * grounds the one base rule already covers.
 */

/* ----------------------------------------------------------- the frame -- */

/*
 * THE DIALOG ELEMENT ITSELF is only a transparent, full-viewport host. Every
 * pixel is painted by `__scrim` and `__panel` inside it, which is what lets the
 * wash be read back by computed style — `::backdrop` cannot be queried, and a
 * gate that cannot read a value cannot hold it.
 *
 * `max-width`/`max-height`/`margin`/`border`/`padding` are all reset because the
 * UA stylesheet gives a `<dialog>` a centred, `max-width: calc(100% - 6px - 2em)`
 * box with a solid border. None of that is wanted.
 */
.sf-modal {
  background: transparent;
  border: 0;
  color: var(--brand-color-ink, #17181a);
  font-family: var(--brand-font-body, system-ui, sans-serif);
  height: 100%;
  inset: 0;
  /* globals.css sets `line-height: 1.65` on `body` and the boards set no
   * leading at all, so every line box that does not name one has to be `normal`
   * or every block in the panel deepens. */
  line-height: normal;
  margin: 0;
  max-height: none;
  max-width: none;
  overflow: hidden;
  padding: 0;
  /*
   * A MODAL INHERITS FROM WHEREVER IT IS MOUNTED, and it is mounted next to its
   * trigger — which can be a centred hero, a right-aligned toolbar or a table
   * cell. `text-align` inherits, so without this every left-aligned paragraph,
   * bullet and caption in the panel silently follows the alignment of the button
   * that opened it. Measured on the delivery modal: mounted inside a centred
   * section, its tips list and expedite band both centred and the two regions
   * read 8.7% and 11.9% against the board. Header bands set their own `center`
   * back.
   */
  text-align: left;
  width: 100%;
}

/*
 * The scrim carries the wash so that ONE element is responsible for it. A
 * `::backdrop` left at the UA default would darken underneath and the two would
 * compound.
 */
.sf-modal::backdrop {
  background: transparent;
}

/*
 * `rgba(23,24,26,0.55)` — ink at 55%, the wash Modal.dc.html uses, which makes
 * it the design system's modal scrim rather than any one modal's choice. A
 * literal rather than a token because a colour token cannot carry an alpha
 * channel and `color-mix()` on a `var()` would resolve the brand's ink at a
 * different opacity per brand — the scrim is a platform constant, like the radii.
 *
 * `align-items: flex-start`: a modal taller than the viewport must start at the
 * top and scroll, never centre and hang off both ends.
 *
 * `position: relative` because it is a scroll container: any absolutely
 * positioned descendant whose ancestors are all static resolves against the
 * INITIAL containing block and extends the DOCUMENT's scrollable overflow rather
 * than this box's — the sr-span overflow bug (playbook §5).
 */
.sf-modal__scrim {
  align-items: flex-start;
  background: rgba(23, 24, 26, 0.55);
  display: flex;
  height: 100%;
  justify-content: center;
  overflow-y: auto;
  padding: 36px;
  position: relative;
  /* iOS momentum scrolling inside the scrim rather than behind it. */
  -webkit-overflow-scrolling: touch;
}

/*
 * THE PANEL, AND ITS EDGE IS THE POINT (W24).
 *
 * `border-radius` + the scrim's inset is what draws the border Ryan reads at the
 * top, the left and the right; the shadow is what lifts it off the page. The
 * bottom edge is usually past the fold on a phone, which is why his description
 * names three sides and not four.
 *
 * NO WIDTH HERE. Each modal declares its own board figure — 780 for the delivery
 * timeline, 820 for the installer widget — and `max-width: 100%` is what makes
 * both give before the scrim's inset does.
 *
 * `overflow: hidden` rather than `clip`: gotcha #1 only bites when something
 * inside must be `position: sticky`, and nothing in either panel is.
 */
.sf-modal__panel {
  background: var(--brand-color-surface, #ffffff);
  border-radius: var(--radius-control);
  box-shadow: 0 24px 60px rgba(23, 24, 26, 0.35);
  display: flex;
  flex-direction: column;
  max-width: 100%;
  overflow: hidden;
  position: relative;
}

/*
 * The open transition. The boards carry appearance and never motion (playbook
 * §2), so this is the smallest honest thing: the panel rises 8px and fades in
 * over 160ms. Reduced motion gets the end state with no animation at all — not
 * a shorter one.
 */
@media (prefers-reduced-motion: no-preference) {
  .sf-modal[open] .sf-modal__panel {
    animation: sf-modal-rise 160ms ease-out;
  }
}

@keyframes sf-modal-rise {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/*
 * THE CLOSE CROSS, TOP RIGHT (W24). Modal.dc.html draws the 20px mark alone at
 * 16/16; the button around it is this build's, because a bare SVG is not
 * operable. It is sized so the MARK still lands on the board's pixel: a 20px
 * mark inside 8px of padding is a 36px target inset by 8, which puts its
 * top-left at 16 − 8 = 8.
 *
 * `position: absolute` against `.sf-modal__panel`, so it rides the panel's own
 * corner whatever the modal draws under it — a tinted head band in both of
 * today's callers, but nothing here depends on there being one.
 */
.sf-modal__close {
  align-items: center;
  background: none;
  border: 0;
  border-radius: var(--radius-inner);
  color: var(--brand-color-muted, #6c6a62);
  cursor: pointer;
  display: flex;
  justify-content: center;
  padding: 8px;
  position: absolute;
  right: 8px;
  top: 8px;
  /* It is the panel's furniture, not the content's: a head band that paints a
   * ground of its own must not paint over the way out. */
  z-index: 1;
}

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

/*
 * W174: INK AT REST, both the title's colour and the site's literal token —
 * REPLACES W71b's white-on-navy pairing (that comment's contrast arithmetic
 * was for a ground that no longer exists here; it is not left standing as
 * stale prose beside a rule it no longer describes). The muted-grey / ink pair
 * on the base rule above is tuned for the bare panel corner's plain white,
 * kept exactly as it was for a headless caller; this scoped rule stays because
 * the header's ground and the bare corner's are still two different colours
 * (well vs. surface), even though both are light now — collapsing the two
 * rules into one would have the header's X drawn one shade lighter (muted)
 * than the title beside it draws ink, for no reason either state chose on
 * purpose.
 *
 * THE CONTRAST, DOCUMENTED HERE rather than left to `btngate.mjs` (same
 * exclusion as W71b's — icon-only controls are off that sweep's roster). Ink
 * on well is the SAME 15.58:1 the title above computes, comfortably clearing
 * both the 3:1 UI-component bar and the 4.5:1 text bar.
 *
 * HOVER MOVES TO ACCENT rather than darkening further — ink is already this
 * design system's darkest ink-family stop, so there is nowhere for a same-
 * family hover to go; a family SWITCH is what the base rule's own hover does
 * too (muted to ink), and this shell's OWN top-of-file comment has carried
 * this exact pairing's number since before W71b ever landed: "the close cross
 * sits on... a head band's well tint (accent... 4.01:1)". FLAGGED — a same-
 * family darken was not tried, only reasoned past, so this is Ryan's to veto.
 */
.sf-modal__header .sf-modal__close {
  color: var(--brand-color-ink, #17181a);
}

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


/*
 * The close button's accessible name. Both boards draw a bare cross, and a
 * control whose only content is a decorative SVG has no name at all, so the word
 * is RENDERED and clipped rather than supplied as an `aria-label` — which is
 * also what keeps the name readable to `textContent`, the way `dlvgate.mjs`
 * asserts it.
 *
 * `position: absolute` on a clipped span is the pattern that caused the page
 * overflow bug (playbook §5). Its containing block here is `.sf-modal__close`,
 * which is itself positioned, so it cannot escape; the panel and the scrim are
 * `position: relative` for the same reason one level up.
 */
.sf-modal__sr {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

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

/*
 * THE WAY OUT AT THE BOTTOM, and it is a BAND rather than a button: both modals
 * are long enough that a reader reaches the end of them, and the end of them is
 * where the second close belongs. The band's inset is the panels' own 36; the
 * button's treatment is each modal's (accent on the delivery timeline, ghost on
 * the installer widget) because that is a commerce-vocabulary decision and not a
 * structural one.
 *
 * WHERE IT SITS IS THE CALLER'S. The delivery modal makes it the panel's last
 * child; the installer widget places it ABOVE its own disclaimer (W18, Ryan:
 * "exiting > disclaimer") and therefore inside the widget body. That is why this
 * is a band a caller can put down rather than a slot the shell fills.
 *
 * ═══ THE TWO ADJUSTABLE EDGES ARE CUSTOM PROPERTIES, AND THAT IS LOAD-ORDER ═══
 *
 * A caller that needed more lead (W25) or a narrower inset at its own rung would
 * naturally write `.sf-inst__foot { padding-top: 44px }` — and that is a BUG
 * waiting to happen, because this rule's `padding` SHORTHAND and that longhand
 * carry the same specificity and are resolved by which stylesheet loaded last.
 * These are two `<link>`s hoisted by React with the same precedence; their order
 * is React's business, not a thing to design against. A custom property cascades
 * on its own name and cannot be clobbered by the shorthand that reads it, so the
 * caller's value holds whichever sheet lands first.
 */
.sf-modal__footer {
  display: flex;
  justify-content: center;
  padding: var(--sf-modal-foot-lead, 30px) var(--sf-modal-foot-inline, 36px) 30px;
}

/* ----------------------------------------------------------- the phone -- */

/*
 * ONE MOBILE RUNG FOR THE FRAME (W24 + W26), at the site's own mobile boundary —
 * 767.98, wider-side convention. A modal that reflows at a different width from
 * the page behind it reads as a bug, and two modals that reflow at different
 * widths from EACH OTHER is the inconsistency Ryan's rule is about: the
 * installer widget's frame used to cut at 559.98 and go full-bleed, the delivery
 * modal's cut at 767.98 and kept its inset. The inset one is the reference.
 *
 * 15px is the page's own mobile gutter (Ryan, 2026-08-26: "15px everywhere.
 * Settled."). The scrim is a fixed overlay with no `.sf-container` around it, so
 * it is a literal here and moves with the rung in chrome.css by hand.
 *
 * THIS SUPERSEDES THE INLINE HALF OF W11/W19 for the installer widget, and does
 * not undo the ruling behind it. Ryan's W11 was "'View N Pros' = full-screen
 * takeover → make it a POP-UP like the delivery timeline (never feel taken
 * away)"; the `min-height: 100%` that made it read as a new page went then and
 * stays gone. What W24 corrects is the half of that pass which went further than
 * the reference and took the panel's side edges with it.
 *
 * THE BOTTOM CLOSE GOES FULL WIDTH (W25). On a 360px panel a centred 78px ghost
 * is a small target at the end of a long scroll; the band already spans the
 * measure, so the button takes it. Above the rung both modals keep the centred
 * intrinsic width their boards draw.
 *
 * AND IT APPLIES TO THE DELIVERY TIMELINE TOO — CONFIRMED INTENDED (Ryan,
 * 2026-09-02). W25 was raised against the installer widget, so it was worth
 * asking whether the reference pop-up should have been scoped out of it; the
 * answer is no. One structure means one structure, and a full-width close at
 * the end of a phone-length scroll is right on both. Desktop is untouched on
 * both, which is why `dlvgate`'s 1440 footer crop never saw this.
 */
@media (max-width: 767.98px) {
  .sf-modal__scrim {
    padding: 15px;
  }

  .sf-modal__footer > * {
    width: 100%;
  }
}

/* -------------------------------- W55/56/68/69/70: the header row + content -- */

/*
 * ═══ AN ESCAPE REACHABLE MID-LIST (Ryan, 2026-09-02) ═══
 *
 * "The only exits today are the X at the top of a scroll or the close at the
 * bottom of it — an escape must be reachable mid-list." On a phone the installer
 * pop-up runs several screens; a reader ten reviews down had to travel to one
 * end or the other to get out.
 *
 * IT IS THE SHELL'S, NOT THE INSTALLER WIDGET'S (W56), which is the standing
 * pop-up rule paying off: the mechanism is written once here and any modal
 * adopts it by handing `ModalShell` a `header` node. What a caller cannot do on
 * its own is take the CLOSE CROSS with it — the cross is the shell's furniture,
 * absolutely positioned against the panel — which is exactly why this is a shell
 * prop rather than a class.
 *
 * ═══ W70 (Ryan, 2026-09-03): "IT'S NOT SO MUCH STICKY ANYMORE. IT'S LIKE A REAL
 * HEADER WITH A SCROLLABLE CONTAINER UNDERNEATH IT." ═══
 *
 * W55 through W69 built this as a STICKY child of a scrolling panel — correct on
 * a phone, where "the panel is the scroller" was invisible plumbing nobody had
 * to look at. W69 carried the same plumbing to desktop and Ryan saw exactly what
 * it was: the panel's native scrollbar ran its FULL height (there was only ever
 * one box to scroll), which put the thumb where the X belongs and squared off
 * the top-right corner a sticky-in-a-scroller has no way to round.
 *
 * THE FIX IS STRUCTURAL, NOT COSMETIC: the header stops being a scrolling
 * child that pins itself in place and becomes a REAL ROW — a static sibling
 * ABOVE a second row that is the one and only scroller. Nothing is sticky here
 * any more; `.sf-modal__header` never moves because it was never inside the
 * thing that moves.
 *
 *     .sf-modal__panel        overflow: hidden, flex column, radius kept
 *       .sf-modal__header     row 1 — static, full width, the hairline
 *       .sf-modal__content    row 2 — flex: 1, overflow-y: auto — THE SCROLLER
 *
 * THE CROSS DOES NOT MOVE IN THE CSS. It is still `position: absolute` against
 * `.sf-modal__panel`, and the header still starts flush at the panel's own
 * top-left corner and spans its full width — so the panel's top-right corner
 * and the header's are the same point, and the same `top: 8px; right: 8px`
 * lands in the same place it always has.
 *
 * ONE STRUCTURE, BOTH RUNGS, from the day this landed — there is no longer a
 * phone-only or desktop-only branch to keep in step with each other, which is
 * also why this is described as HARDENING and not just a redesign: the
 * sticky-in-a-scroller geometry was the thing W67's stale-measurement bug grew
 * out of (a hidden `<dialog>`'s panel collapsing to zero and being re-measured
 * as the scroller). A container that is ALWAYS the same element, at rest or
 * scrolled, on a phone or a desktop, is one fewer case for a reader-invisible
 * bug to hide in.
 */
/*
 * ═══ W71a (Ryan, live on W70): THE SHADOW MUST WIN THE STACKING ORDER ═══
 *
 * Ryan's report: the scrolled-state shadow does not paint over everything —
 * installer cards visibly paint OVER it. The cause is CSS's own painting
 * order, not a bug in the shadow's own rule: `.sf-modal__header` was
 * `position: static`, so its box-shadow paints in the "in-flow, non-positioned
 * descendants" step of `.sf-modal__panel`'s stacking order. W39's cards are
 * `position: relative` (for their own two-layer lift) with no z-index of their
 * own — and ANY positioned element, even at the default `z-index: auto`,
 * paints in a LATER step than non-positioned flow content, `position` alone
 * being enough to promote it. A `position: static` header was always going to
 * lose to a `position: relative` card sitting under it, regardless of which
 * one is later in the DOM.
 *
 * THE FIX IS STRUCTURAL, not a bigger shadow: `position: relative` +
 * `z-index: 2` promotes the header itself to a POSITIVE stack level, which
 * paints after (above) every `z-index: auto` positioned descendant content can
 * produce — this is the same "positioned wins over static" rule that caused
 * the bug, now working for the header instead of against it.
 *
 * AND `.sf-modal__content` GETS `isolation: isolate` SO THIS CANNOT RECUR AT A
 * HIGHER NUMBER: without it, the fix is "z-index 2 beats whatever content has
 * today", which is exactly the kind of number that stops being true the day
 * someone adds a `z-index: 3` deep inside a card for an unrelated reason.
 * `isolation: isolate` gives the content row its OWN stacking context, so
 * every z-index any descendant ever sets is scoped to compare only against
 * its OWN siblings inside that box — none of it can ever reach outside to
 * compete with the header's again, at any number. Proven with a real pixel
 * sample, not a computed-style z-index read — see `instmodal.mjs`'s driven
 * check, added because a `z-index` read is exactly what looked right while
 * this bug was live.
 */
.sf-modal__header {
  /*
   * ═══ W174 (Ryan, live): "I DO NOT LIKE THE NAVY BLUE... IT'S GETTING LOST.
   * LET'S GO WITH A LIGHTER GRAY AND INK FOR THE TEXT." ═══
   *
   * REVERSES W71b, not because W71b's reasoning was wrong — `--brand-color-nav`
   * really was "the site's own header chrome" — but because a strong, saturated
   * band competing with the panel's own accent-orange controls and photography
   * is exactly what "getting lost" describes: the header stopped reading as
   * calm furniture and started reading as another loud thing on the page.
   *
   * `--brand-color-well` IS THE TOKEN, named here the way W71b named its own
   * pick, for the same veto. The candidate it beat was `--brand-color-paper`
   * (`#fbfbfa`/`#faf9f6` depending on the sheet) — measured, not eyeballed:
   * paper sits only ~0.035 of a luminance-ratio step off the panel's own white,
   * against well's ~0.14 — roughly FOUR TIMES the separation, and the same
   * order of magnitude as the hairline below (~0.24). Paper next to the panel's
   * white would have been almost exactly the "stripe that doesn't quite read as
   * intentional" problem this ruling is trying to avoid, just in gray instead
   * of navy. Well is also not a new colour to this shell: it is the same tint
   * `.sf-la__howto` wore before W138 struck it for THAT surface's own reasons
   * (a diagram wanted a plain white so the line art didn't sit in a box) — two
   * different call, because a modal header's job is to separate itself from
   * the panel, exactly the property W138 didn't want there.
   */
  background: var(--brand-color-well, #f1f0ec);
  /*
   * THE HAIRLINE RETURNS. W71b retired it on the grounds that "a dark navy row
   * over a light content row is already an unmistakable edge" — true of navy,
   * not true of a light gray next to the panel's white: a `#f1f0ec`-to-`#ffffff`
   * seam with nothing marking it is close to the exact "reads as a rendering
   * artefact, not a deliberate line" failure mode W71b was reasoning AWAY FROM,
   * now landing on the new ground instead. `--brand-color-line` is this sheet's
   * pre-W71b value, restored rather than reinvented — it was never wrong, it
   * was only made redundant by navy for one pass.
   */
  border-bottom: 1px solid var(--brand-color-line, #e8e7e3);
  flex: none;
  /* The two declarations the block above argues for. They were lifted out
   * briefly to run the negative control that proved the diagnosis (the pixel
   * under the header reading a flat 255,255,255 — Ryan's report exactly) and
   * are restored here; the gate that reads those pixels is what noticed they
   * had not been put back. */
  position: relative;
  z-index: 2;
  /* W70 research addition 1 — the scrolled-state shadow fades in and out
   * rather than snapping, on the one property this rule ever changes. */
  transition: box-shadow 120ms ease-out;
}

/*
 * THE HEADER'S TITLE — the shell's, since W68 adopted the band everywhere, W69
 * un-gated it from the phone rung, and W70 turned the row itself into a real
 * header rather than a sticky overlay.
 *
 * It was `.sf-inst__sticky-title` while exactly one pop-up had a band. Ryan's
 * W68 ("make sure that new design proliferated") made it three, and three
 * copies of one type treatment is how the pop-ups drift apart again — which is
 * the whole reason the shell exists. The type size is UNCHANGED since W69
 * (Ryan's own words on the delivery modal's head applied here too: "don't
 * bring the font size down") — one declaration, both rungs.
 *
 * THE 52px OF TRAILING ROOM IS THE CROSS, absolutely positioned at the row's
 * top-right at every rung: a title long enough to reach it would otherwise run
 * underneath, and both "Who installs your hoop" on a 390 screen and every
 * title on a wider one are long enough to make this matter.
 *
 * COLOUR, W174: ink on the new gray ground — Ryan's own word, and the site's
 * literal ink token, replacing W71b's white-on-navy pairing wholesale (that
 * pairing's own comment is gone with the navy it was tuned for, not left
 * behind as dead prose). `--brand-color-ink` (`#17181a`) on `--brand-color-well`
 * (`#f1f0ec`) computes to 15.58:1 (WCAG relative luminance) — clears the 4.5:1
 * text bar with more room than the white-on-navy pairing it replaces (14.24:1)
 * had, because ink on a near-white ground is close to the highest-contrast
 * pairing this design system has at all.
 */
.sf-modal__header-title {
  color: var(--brand-color-ink, #17181a);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: normal;
  margin: 0;
  padding: 14px 52px 13px 20px;
}

/*
 * ═══ THE CONTENT ROW IS THE ONE AND ONLY SCROLLER, AT EVERY RUNG (W70) ═══
 *
 * `flex: 1` inside the panel's fixed-height column means this row takes
 * exactly the space the header does not — the panel's `max-height` (below)
 * minus the header's own natural height — and `overflow-y: auto` is what
 * scrolls WITHIN that space when the caller's `children` are taller than it.
 * The header, one flex item up, is never part of this box and so is never
 * asked to move.
 *
 * `max-height: 100%` ON THE PANEL is the same declaration W69 put on the panel
 * itself, moved down one level in the box it applies to: it reads as 100% of
 * the SCRIM's content box, which is already `height: 100%` with a `padding` of
 * its own — 15px on a phone, 36px from 768 up, both read off `.sf-modal__scrim`
 * below without this rule needing to know which is live. A panel shorter than
 * that never engages `.sf-modal__content`'s scrolling at all; it simply is not
 * tall enough to need it, which is the same "a short review just flows"
 * philosophy the rest of this shell already keeps.
 *
 * BOTH CORNERS ARE FREE, not just the top one W69 already had: `.sf-modal__
 * panel`'s `border-radius` clips every descendant to the rounded rect the
 * instant its `overflow` is anything but `visible` (per spec, `hidden` and
 * `auto` both establish that clip), and `.sf-modal__content` is now the row
 * that touches the panel's BOTTOM edge — so its own native scrollbar, drawn
 * inside its box, has its last pixels clipped under the curve exactly the way
 * the header's top corners already were. This is the "better idea" Ryan
 * invited in place of the square bottom corner he was willing to accept: no
 * extra markup, no mask, just letting the panel's existing clip reach the
 * scroller that used to be the whole panel.
 *
 * THE SCRIM DOES NOT SCROLL A HEADED PANEL. Before this it sometimes had to
 * (a panel taller than the viewport, headless, or the pre-W55 shape); now that
 * `.sf-modal__content` is bounded by the panel's own `max-height`, the panel
 * never grows past what the scrim leaves it, so the scrim's `overflow-y: auto`
 * simply never triggers for a headed one — declared `hidden` anyway so there
 * is never a double scrollbar in an edge case this reasoning missed.
 *
 * ONLY FOR A HEADED PANEL (`--headed`). A headless one keeps the scrim as its
 * one scroller and every pixel it has ever drawn.
 */
.sf-modal__scrim:has(.sf-modal__panel--headed) {
  overflow-y: hidden;
}

.sf-modal__panel--headed {
  max-height: 100%;
}

/*
 * ═══ W199 ITEM 2 (Ryan, 2026-09-06): THE THIRD ROW — A DOCKED FOOTER ═══
 *
 *   "Consider the subtotal bar + Checkout as a STICKY footer of the drawer, so
 *    Checkout is always visible while the lines scroll."
 *
 * IT IS THE W70 STRUCTURE, ONE ROW LONGER, and deliberately not a `position:
 * sticky` child of the scroller. W70's own lesson is the argument: the panel is
 * a fixed-height flex column, the CONTENT ROW is the one and only scroller, and
 * a header "sticks" because it was never inside the thing that moves. A footer
 * earns its place the same way —
 *
 *     .sf-modal__panel--headed   overflow: hidden, flex column, radius kept
 *       .sf-modal__header        row 1 — static
 *       .sf-modal__content       row 2 — flex: 1, overflow-y: auto — THE scroller
 *       .sf-modal__dock          row 3 — static, the caller's own band
 *
 * — and the same two things fall out for free that fell out for the header. It
 * cannot fight the scroller for a position, because it is not in it. And the
 * panel's own `overflow: hidden` + radius clip its BOTTOM corners, which is the
 * property W70 had to restructure the whole panel to win for the scrollbar.
 *
 * `flex: none` IS THE WHOLE FRAME. Everything drawn inside the band is the
 * caller's, exactly as `children` is — the shell owns that the row exists, is
 * last, and does not scroll.
 *
 * DISTINCT FROM `.sf-modal__footer`, which is `ModalFooter`: a band the caller
 * PUTS DOWN inside its own content (the installer widget places it above its
 * disclaimer). That one scrolls with the content because it is part of it; this
 * one is a row of the panel. Two different things, two names, no overload.
 */
.sf-modal__dock {
  flex: none;
}

.sf-modal__content {
  flex: 1;
  /*
   * W71a: CONTAINS EVERY z-index CONTENT EVER SETS. A card's own
   * `position: relative` (W39's lift) is what let it outrank the header's
   * `position: static` box-shadow in the paint order — the header's own
   * `z-index: 2` fixes today's version of that, but `isolation: isolate` is
   * what stops the NEXT one: it gives this row its own stacking context, so
   * every z-index any descendant sets, at any number, is scoped to compare
   * only against its own siblings inside this box and can never again reach
   * outside to compete with the header's.
   */
  isolation: isolate;
  min-height: 0;
  overflow-y: auto;
  /* iOS momentum inside the content row now that it is the scroller. */
  -webkit-overflow-scrolling: touch;
  /*
   * ═══ W70 RESEARCH ADDITION 3 — A SCROLLBAR THAT STAYS, QUIETLY ═══
   *
   * Fully hiding this scrollbar (the review list's own `scrollbar-width: none`
   * pattern) was ruled out for THIS box: research on scroll affordances warns
   * that hiding position feedback entirely is a mouse/trackpad hazard unless
   * another clear cue already exists, and this box's other cue (the fade,
   * below) only says "there is more", never "how much" or "where you are".
   * `overflow: overlay` — which would float the bar over the content instead
   * of reserving track width — is deprecated and the CSSWG's replacement
   * (`scrollbar-gutter`'s sibling proposal for STYLED overlay scrollbars) has
   * not shipped anywhere; `thin` + a low-contrast colour is the real ceiling
   * cross-browser today, Windows Chrome included.
   *
   * TRANSPARENT TRACK, quiet thumb: the track would otherwise be a second
   * vertical line beside the panel's own edge, and this chrome should read as
   * barely-there until a reader actually reaches for it — which is what the
   * `:hover` darken just below is for. Firefox has no separate hover colour to
   * give `scrollbar-color`, so it keeps the resting tint always; that is a
   * smaller gap than not having `scrollbar-color` at all.
   *
   * THE TRACK RUNS TO THE PANEL'S OWN BOTTOM EDGE, and the panel's `overflow:
   * hidden` + radius (above) clips its last pixels under the curve exactly the
   * way it clips the rest of this row — the same "better idea" that replaced
   * the square corner, applied to the one piece of this box that is not
   * ordinary content.
   */
  scrollbar-color: rgba(23, 24, 26, 0.22) transparent;
  scrollbar-width: thin;
}

.sf-modal__content::-webkit-scrollbar {
  width: 8px;
}

.sf-modal__content::-webkit-scrollbar-track {
  background: transparent;
}

.sf-modal__content::-webkit-scrollbar-thumb {
  background: rgba(23, 24, 26, 0.22);
  border-radius: 999px;
}

.sf-modal__content:hover::-webkit-scrollbar-thumb {
  background: rgba(23, 24, 26, 0.38);
}

/*
 * ═══ W70 RESEARCH ADDITION 1 — THE HEADER'S SCROLLED-STATE SHADOW ═══
 *
 * Material's dialog/app-bar pattern: a header communicates "you are not at the
 * top" once its content has scrolled, and stops the instant it has not. The
 * flag is `modal-shell.tsx`'s (`.sf-modal__panel[data-scrolled]`, read off
 * `.sf-modal__content`'s own `scrollTop` — the panel is the shared ancestor of
 * both, which is what lets this rule reach sideways from one child's state to
 * the other's paint without either knowing about the other directly.
 *
 * HAIRLINE-STRENGTH, NOT MATERIAL ELEVATION: a flat house does not grow a
 * card-shadow the instant a reader nudges a scrollbar. The two shadows are a
 * 1px seam (doing the hairline's own job at zero blur, so it reads crisp
 * rather than soft) plus a very short, very faint spread that gives the seam
 * a hint of lift without a visible blur radius most flat UI would reject.
 *
 * FLAGGED FOR RYAN: this is a research-informed addition, not a walked
 * ruling — subtle by design and one rule to delete if he would rather the
 * header stay flat.
 *
 * CHECKED AGAIN FOR W174, NOT ASSUMED: this shadow's own colour (ink at low
 * alpha) never depended on the header's background, so reasoning alone said
 * it should read the same under a light-gray header as it did under navy —
 * verified with real pixels rather than left as reasoning. Sampled straight
 * down the header/content seam on the installer pop-up: AT REST the new
 * hairline is a crisp single pixel (232,231,227, `--brand-color-line`
 * exactly); SCROLLED the row directly under it darkens to (228,228,228) and
 * softens through (248,248,248) into the content — the shadow's 1px seam and
 * short spread both still land and both still read as an intensification of
 * the resting hairline, not a duplicate or a competing line beside it.
 */
.sf-modal__panel[data-scrolled="true"] > .sf-modal__header {
  box-shadow:
    0 1px 0 rgba(23, 24, 26, 0.08),
    0 4px 6px -4px rgba(23, 24, 26, 0.1);
}

/*
 * ═══ W141 (Ryan, live on Lightning Adjust): THE FADE ONLY WORKED OVER WHITE
 * ═══
 *
 * Ryan's report: the W70 bottom cue "works well when the background color is
 * white. It looks odd with any other background color... obvious on the
 * Lightning Adjust popup because we have darker background in several cases
 * (but not always)." That popup alone proves it — `lightning-adjust.css`
 * puts a near-black masthead, a `--brand-color-media-well-a` intro and a
 * `--brand-color-nav` answers band all inside this one scroller, alongside
 * the light `--brand-color-well` how-to and the plain white included section.
 *
 * THE MECHANISM WAS THE BUG. `mask-image` does not paint a colour over the
 * row's last 36px — it ERASES those pixels to transparent, letting whatever
 * sits BEHIND `.sf-modal__content` show through, which is the panel's own
 * `background: var(--brand-color-surface, #fff)` one flex level up. Over
 * white content that erasure is invisible (white fading to the panel's own
 * white); over the masthead or the answers band it reads exactly as
 * reported — a pale, wrong-coloured wash eating a deliberately dark ground.
 * The fix could not be "a better mask" — no erasure of ANY kind can be
 * ground-agnostic, because erasing always reveals a fixed colour behind it,
 * and this row's content is not one colour.
 *
 * RESEARCHED, NOT GUESSED. Lea Verou's `background-attachment: local`
 * scrolling-shadow trick was the first candidate and it is disqualified for
 * the same structural reason as the mask: her own write-up warns it breaks
 * the moment the scrolled content carries its own backgrounds or images,
 * which describes every section in this row (cards, the masthead's own
 * background-image, the callout photograph). `animation-timeline: scroll()`
 * was weighed as the DRIVER and rejected too, not for support (~84% global
 * as of this build, which would have been enough) but because it would
 * duplicate a decision `modal-shell.tsx` already makes correctly — the
 * `data-more` attribute, computed off real scroll position AND a
 * `ResizeObserver`/`MutationObserver` pair so it stays right when the
 * content's own height changes after paint. A second, parallel "am I at the
 * end" computed by the animation timeline is a second place for those two to
 * drift apart; JS keeps the one true answer, CSS only needs to be told it.
 * Sampling the ground colour under the cue and matching it (`elementFromPoint`
 * plus a canvas read) was weighed and rejected on cost and correctness both:
 * there is no live "effective composited background colour" API, a canvas
 * snapshot is CORS-fragile against this row's photographs and would have to
 * re-run every scroll frame, and it would still guess wrong the instant a cue
 * straddled two different backgrounds at once — which W139's masthead-into-
 * intro seam already does.
 *
 * THE ANSWER IS THE ONE EVERY SCROLL-SHADOW LIBRARY CONVERGES ON: an OVERLAY,
 * not an erasure — a translucent dark tint painted OVER the last 36px rather
 * than a hole cut into them. A tint reads as a soft shadow falling across
 * WHATEVER is under it, light or dark, because it never claims a ground
 * colour of its own — the same reasoning `.sf-modal__scrim`'s own wash above
 * already banks on. It will not shout on the masthead's own near-black
 * (ink-on-ink has nowhere to go), and that is the correct outcome, not a
 * miss: a shadow cannot darken what is already this dark, and a cue that
 * quietly does nothing over black is a smaller wrong than the mask's own
 * glowing white patch was.
 *
 * THE ELEMENT IS A `::after`, sticky, zero net height — W65b's own sibling
 * trick before mask replaced it, brought back for exactly the ground-
 * agnostic property mask never had. `height: 36px` plus `margin-top: -36px`
 * contribute nothing to `scrollHeight` (the negative margin cancels the
 * height in the same flow-height sum), so it cannot itself cause the
 * scrollbar it is meant to sit quietly beside. `position: sticky; bottom: 0`
 * inside `.sf-modal__content` — the pseudo-element's own nearest scrolling
 * ancestor is that row, so it needs no extra wiring to track its scroll
 * position; its "static" position (per the zero-height trick) is already at
 * the row's own end, so it is glued to the visible bottom edge for exactly as
 * long as there IS a bottom edge to sit at. `pointer-events: none` — a
 * sticky, positioned element covers real pixels a drag or a tap must still
 * reach; the mask never needed this because erasing pixels leaves nothing
 * there to intercept, which is the one property this rewrite has to restate
 * by hand.
 *
 * SAME `data-more` GATE, UNCHANGED JS. `modal-shell.tsx`'s effect (added W70)
 * already sets `data-more="true"` on `.sf-modal__content` exactly when it is
 * scrollable and not at its end and clears it at the end — that computation
 * did not know or care whether the CSS reading it painted a mask or an
 * overlay, so none of it changes here.
 *
 * PEAK ALPHA 24% OF INK, not the mask's own 0%-to-100% erasure — a shadow
 * that fully opaqued the panel's own colour at the bottom edge would clip a
 * card's last line rather than hint that one follows, which is the opposite
 * of the NN/g "illusion of completeness" cue this row exists for. The 16-stop
 * curve is the SAME eased shape W65b and the original W70 mask both used
 * (still not linear, for the reason W65b's research settled — a linear alpha
 * ramp reads as a harder cut than an eased one at equal endpoints, Weber-
 * Fechner perceptual lightness rather than raw alpha), just re-aimed: the
 * mask's per-stop ERASURE fraction (how much of the content was cut) is
 * exactly this overlay's per-stop TINT fraction (how much shadow is painted)
 * — the two are the same curve read the other way round, so the shape this
 * cue draws is pixel-for-pixel the same taper the mask drew, only now it
 * darkens forward instead of erasing toward a fixed white.
 *
 * THE SCROLLBAR-THUMB QUESTION W70 FLAGGED IS MOOT NOW, NOT JUST ANSWERED. A
 * `mask-image` on `.sf-modal__content` erases that element's own rendered
 * output, which was a live question for whether it also touched the
 * UA-painted scrollbar thumb sharing the same box; an overlay is an ordinary
 * painted pseudo-element sitting ABOVE the content in stacking order and
 * never applies to the box's own chrome at all, so there is no longer a mask
 * whose reach into scrollbar territory needs checking.
 *
 * NEEDS THE SAME z-index DISCIPLINE W71a ESTABLISHED. `.sf-modal__content`
 * already carries `isolation: isolate` (W71a) precisely so a number set here
 * can never leak out to compete with the header's — `z-index: 1` is enough
 * to paint over every `position: relative`, `z-index: auto` card W39 draws,
 * for the identical reason a `position: static` header lost to one before.
 */
.sf-modal__content[data-more="true"]::after {
  background: linear-gradient(
    to bottom,
    rgb(23 24 26 / 0%) calc(100% - 36px),
    rgb(23 24 26 / 0.3%) calc(100% - 33.08px),
    rgb(23 24 26 / 1.2%) calc(100% - 30.42px),
    rgb(23 24 26 / 2.5%) calc(100% - 27.9px),
    rgb(23 24 26 / 4.2%) calc(100% - 25.56px),
    rgb(23 24 26 / 6.2%) calc(100% - 23.29px),
    rgb(23 24 26 / 8.4%) calc(100% - 21.17px),
    rgb(23 24 26 / 10.8%) calc(100% - 19.04px),
    rgb(23 24 26 / 13.2%) calc(100% - 16.96px),
    rgb(23 24 26 / 15.6%) calc(100% - 14.83px),
    rgb(23 24 26 / 17.8%) calc(100% - 12.71px),
    rgb(23 24 26 / 19.8%) calc(100% - 10.48px),
    rgb(23 24 26 / 21.5%) calc(100% - 8.1px),
    rgb(23 24 26 / 22.8%) calc(100% - 5.58px),
    rgb(23 24 26 / 23.7%) calc(100% - 2.92px),
    rgb(23 24 26 / 24%) 100%
  );
  bottom: 0;
  content: "";
  display: block;
  height: 36px;
  margin-top: -36px;
  pointer-events: none;
  position: sticky;
  z-index: 1;
}

/* ══════════════ W192: THE SIDE-ANCHORED VARIANT (the cart drawer) ══════════ */

/*
 * Ryan's W192 drawer is a right-anchored full-height sheet, and everything it
 * needs the shell already owns: the native `<dialog>` and its top layer, the
 * scrim and its began-and-ended press rule, Escape, the history marker, the
 * scroll lock, and the W70 header row over a scrolling content row.
 *
 * SO THIS IS GEOMETRY AND NOTHING ELSE. `ModalShell` adds one class when a
 * caller asks for `anchor="side"`; every rule below is a position or a size.
 * No second component, no second frame, no second row in the enforcement table
 * to forget.
 */

/*
 * ═══ W199 ITEMS 4 & 7 (Ryan, 2026-09-06): EVERY RULE HERE IS A CHILD RULE ═══
 *
 *   "'See it in action' tapped from the drawer opens as ANOTHER DRAWER, not
 *    the standard pop-up. Did you mean to do that?"
 *
 * Nobody meant to do that, and NOTHING ABOUT THE SHADE POP-UP WAS WRONG — the
 * anchor never leaked through a prop, a context or a default. Measured at 390
 * with the drawer open and Lightning Adjust tapped from an option row:
 *
 *     dialog 1  class "sf-modal sf-modal--side sf-cart-drawer"   panel 343x844
 *     dialog 2  class "sf-modal sf-la"          <- NO --side     panel 343x844
 *                                                  justify: flex-end, radius 0
 *
 * The second dialog is a plain centred pop-up by its own class list and drew as
 * a side sheet anyway, because it is a DOM DESCENDANT of the first: the shade is
 * mounted by an option row, the option row is inside the drawer's `children`,
 * and a nested `<dialog>` — top layer or not — is still a descendant in the
 * tree. Every rule in this block was written as `.sf-modal--side <descendant>`,
 * so all of them reached straight through the drawer's own panel and into the
 * pop-up inside it: the scrim's `flex-end`, the panel's `height: 100%`, its
 * squared corners, its 88% phone width, and the slide that replaced the rise.
 *
 * SO THE COMBINATOR IS THE FIX, and it is the fix for the whole class rather
 * than for these two shades: `>` cannot reach past this dialog's own panel, so
 * ANY pop-up opened from inside the drawer presents exactly as it does from the
 * flow. `modal-shell.test.ts` pins it — a `--side` rule written with a
 * descendant combinator fails the suite.
 *
 * THE TWO DIALOGS STACK; THE DRAWER IS NOT CLOSED FIRST. That was the
 * alternative and it is the worse one: a shade is an ASIDE to the option row
 * the shopper is editing, and closing the cart to read what Rust Armor is —
 * then making them reopen it and re-enter Edit — loses their place to work
 * around a CSS bug. Layering is already supported and already proven: the scroll
 * lock is reference-counted for exactly this (`use-scroll-lock.ts`, W54), each
 * surface stamps its own history marker so Back unwinds one layer at a time,
 * and the shade opens from a REAL press so its close watcher is its own (a
 * synthetic one would be grouped — playbook §5).
 */

/* The scrim stops centring and starts pinning to the trailing edge. */
.sf-modal--side > .sf-modal__scrim {
  align-items: stretch;
  justify-content: flex-end;
  overflow-y: hidden;
  padding: 0;
}

/*
 * The panel becomes a full-height sheet. `100%` of the scrim rather than a
 * viewport unit: the scrim is already the dialog's full box, and `100dvh` here
 * would fight the mobile URL bar the scrim has already accounted for.
 */
.sf-modal--side > .sf-modal__scrim > .sf-modal__panel {
  border-radius: 0;
  height: 100%;
  max-height: 100%;
  /*
   * ═══ W200 ITEM 3 (Ryan, 2026-09-06): THE SWIPE'S HALF OF THE CONTRACT ═══
   *
   *   "Gesture: left-to-right swipe dismisses the drawer."
   *
   * `pan-y` KEEPS THE SCROLLER AND HANDS US THE OTHER AXIS. The panel's content
   * row scrolls vertically and must keep doing so at native speed, so the
   * browser is told that vertical panning is still its business and horizontal
   * panning is not. That is what lets `modal-shell.tsx` follow the finger with
   * no `preventDefault` — which it could not call anyway, since React attaches
   * `touchmove` PASSIVELY at the root.
   *
   * `pinch-zoom` IS NAMED BECAUSE OMITTING IT WOULD TAKE IT AWAY: `pan-y`
   * alone disables zoom inside the sheet, and a shopper reading a $3,900 line
   * on a phone is exactly the reader who zooms.
   */
  touch-action: pan-y pinch-zoom;
}

/*
 * THE RELEASE, EITHER WAY (W200 item 3). During the drag the panel's transform
 * is written straight to the node once per frame with no transition — a
 * transition there would put the sheet behind the finger. On release the shell
 * stamps `data-swipe="settling"`, which is the one moment the travel should be
 * eased: home again under the threshold, or off the edge past it.
 *
 * THE DURATION IS `SWIPE_SETTLE_MS`'s, and the shell waits exactly this long
 * before calling `onClose` on the way out, so the sheet is gone from view
 * before the dialog closes under it. Two numbers, one distance — if either
 * moves the other has to.
 */
.sf-modal--side > .sf-modal__scrim > .sf-modal__panel[data-swipe="settling"] {
  transition: transform 200ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  /* NO FOLLOW AND NO SETTLE — the shell dismisses instantly instead, and the
   * panel never carries a transform for this rule to ease. Declared anyway so
   * the sheet cannot animate through a stale attribute. */
  .sf-modal--side > .sf-modal__scrim > .sf-modal__panel[data-swipe="settling"] {
    transition: none;
  }
}

/*
 * ═══ THE SLIDE REPLACES THE RISE ═══
 *
 * The shell's own open transition lifts the panel 8px, which is the right
 * motion for something that appears in the middle of the screen and the wrong
 * one for something that arrives from an edge. Same duration, same easing, same
 * reduced-motion contract — only the axis changes.
 *
 * IT IS NOT A SECOND `-rise`. `modal-shell.test.ts` forbids a block sheet from
 * declaring a rise of its own; this is the SHELL's sheet declaring the shell's
 * other anchor, which is where a variant belongs.
 */
/*
 * ═══ W195: IT HAS TO TRAVEL THE PANEL, NOT NUDGE IT ═══
 *
 * Ryan, walking the drawer: "the cart drawer isn't smooth coming out."
 *
 * MEASURED BEFORE IT WAS CHANGED. A frame trace of the open at 1440 read the
 * panel's left edge at 1044 → 1020 over ~180ms while the opacity went 0 → 1:
 * TWENTY-FOUR PIXELS of movement on a 420px sheet, and on a phone the same 24px
 * on a full-width one. That is a fade with a nudge in it, not a sheet arriving
 * from an edge — the eye reads the fade and never sees the travel, which is
 * exactly what "not smooth" describes.
 *
 * WHY IT WAS 24. The number came straight from the centred pop-up's 8px rise,
 * scaled up a little for a horizontal axis. The rise is right for something
 * that APPEARS in the middle of the screen; a side sheet is a different motion
 * and 24px was the rise wearing a different axis.
 *
 * SO IT STARTS OFF-SCREEN, at `100%` of its own width, whatever that width is —
 * which is what makes the phone's full-bleed sheet and the desktop's 420px
 * panel one motion instead of two. The duration goes to 240ms because the
 * distance went up by more than an order of magnitude; the CURVE is unchanged
 * (`ease-out`, the shell's own vocabulary) and so is the reduced-motion
 * contract below: the end state, with no animation at all.
 *
 * FLAGGED, NOT BUILT: the EXIT is still instant. Animating a `<dialog>` out
 * needs the panel to stay displayed while it leaves, which means replacing this
 * keyframe with `transition` + `@starting-style` + `transition-behavior:
 * allow-discrete` on `display` and `overlay` — a change to the SHARED shell
 * that all four pop-ups would take at once. That is a ruling and a battery of
 * its own, not a rider on a defect fix.
 */
@keyframes sf-modal-slide {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

/* CHILD RULES, like every other `--side` rule — see the block above. Without
 * the combinator a pop-up opened from inside the drawer SLID IN FROM THE EDGE
 * instead of rising, because this out-specified nothing and simply reached it. */
.sf-modal--side[open] > .sf-modal__scrim > .sf-modal__panel {
  animation: sf-modal-slide 240ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .sf-modal--side[open] > .sf-modal__scrim > .sf-modal__panel {
    animation: none;
  }
}

/*
 * ═══ W198 ITEM 3 (Ryan, 2026-09-06): A PARTIAL SHEET ON A PHONE ═══
 *
 *   "The drawer must not take the whole screen on mobile — leave a strip of
 *    site visible on the left with a scrim; tap outside closes."
 *
 * REVERSES THE FULL-BLEED SHEET THIS BLOCK USED TO ARGUE FOR, and the old
 * argument is worth keeping in view because it was not wrong about anything it
 * measured: a skinny side panel does cramp the rows, and the cart-drawer
 * literature does treat that as a drag on mobile conversion. What it missed is
 * the OTHER half of the same literature — NN/g and the cart-drawer research
 * both favour a partial overlay with click-outside over a full takeover,
 * because a sheet that covers everything reads as a PAGE the reader has been
 * sent to, and the way back out of a page is not "tap the part of it you can
 * still see". The strip of scrim is what says "you are still here, and this
 * closes."
 *
 * 88% IS MEASURED, NOT A ROUND NUMBER PICKED FOR LOOKING PARTIAL. At 390 it
 * leaves a 47px strip — wider than the chrome's own 15px gutter, so the site
 * behind reads as a page rather than as a sliver of misalignment, and wide
 * enough to be an easy thumb target away from the panel's own controls. It
 * costs the panel 47px of measure: its inner width goes 360 → 313 against the
 * rail's own 300px column at the same rung, so every row this panel draws is
 * still wider than the surface it is a copy of.
 *
 * TAP-OUTSIDE IS THE SHELL'S ALREADY — `onScrimPointerDown`/`onScrimClick`, the
 * began-and-ended press rule every pop-up shares. It was simply unreachable
 * while the panel covered the scrim; nothing here turns it on.
 *
 * THE DESKTOP PANEL IS UNTOUCHED at 420px, and the reduced-motion contract and
 * the slide's `translateX(100%)` are both relative to the panel's own width, so
 * the motion follows the new number with no second rule.
 */
/*
 * ═══ W202 ITEM 4 (Ryan, 2026-09-09): THE DESKTOP DOCK HUGS ITS CONTENT ═══
 *
 *   "On desktop the subtotal+Checkout dock sits directly under the last line
 *    with the empty space BELOW it, instead of pinned to the panel bottom with
 *    a gap above. Phone stays pinned exactly as-is."
 *
 * THE PANEL IS A FULL-HEIGHT SHEET, so its three-row column has more height
 * than a short order needs, and `flex: 1` on the scroller spends all of it:
 * the content row stretches, the dock is pushed to the panel's bottom edge,
 * and a two-line order reads as a subtotal marooned under a field of white.
 *
 * `flex: 0 1 auto` STATES WHAT THE ROW ACTUALLY IS — take what you need, give
 * it back under pressure. A short order draws its lines, the dock follows
 * immediately underneath, and the leftover height falls BELOW the band where
 * a reader reads it as the panel's ground rather than as a gap.
 *
 * THE TWO STATES CONVERGE, WHICH IS THE POINT. Once the content is taller
 * than the room, `1` (shrink) bounds the row at the panel's own height, the
 * dock lands on the bottom edge exactly as it does today, and the scroll cue
 * sits immediately above the subtotal's bar as W199 ruled — the same
 * geometry, reached by the same arithmetic. Nothing about the cue's three
 * states changes: it is painted from `data-more`, which is a fact about
 * whether this row scrolls, and a row that no longer scrolls correctly says
 * it has nothing below it.
 *
 * MIN-WIDTH RATHER THAN A CARVE-OUT: the phone rung is `max-width: 767.98px`
 * throughout this sheet, so the named cut belongs to the wider side (playbook
 * §5) and the phone keeps the pinned dock verbatim.
 */
@media (min-width: 768px) {
  .sf-modal--side > .sf-modal__scrim > .sf-modal__panel > .sf-modal__content {
    flex: 0 1 auto;
  }
}

@media (max-width: 767.98px) {
  /*
   * THE DIALOG HAS TO BE GIVEN A WIDTH FOR THE PANEL'S PERCENTAGE TO MEAN
   * ANYTHING, and this is the bug the drawer's own gate caught: `.sf-modal`
   * declares `height: 100%` and `inset: 0` but no width, and the UA stylesheet
   * sizes a `<dialog>` `width: fit-content`. So a panel at `width: 100%` was a
   * percentage of a box that was shrink-wrapping to that same panel — circular,
   * and it resolved to ZERO. The sheet measured 0px wide with `display: flex`
   * and `visibility: visible`, which is exactly the kind of defect a screenshot
   * would have shown as "the drawer did not open".
   *
   * At desktop the question never arises: the panel's own 420px gives the dialog
   * a content width to wrap. Stating the dialog's width here is what makes the
   * full-width sheet resolve, and it mirrors the `height: 100%` already above.
   */
  .sf-modal--side {
    width: 100%;
  }

  /* A CHILD RULE — see the block above. This one is the most visible half of
   * the W199 defect: it is what made the nested shade 88% of the screen and
   * squeezed its bullets into three stacked lines. */
  .sf-modal--side > .sf-modal__scrim > .sf-modal__panel {
    width: 88%;
  }
}
