@font-face{font-family:"Arimo";src:url(/fonts/arimo/arimo-variable.6a22ce0b.woff2) format("woff2");font-display:swap;font-weight:400 700;font-style:normal}@font-face{font-family:"Arimo Fallback";src:local("Arial");ascent-override:90.53%;descent-override:21.19%;line-gap-override:3.27%;size-adjust:100.00%}:root{color-scheme:light;--fx-accent:#A45A05;--fx-accent-2:#A48F05;--fx-ink:#0F5132;--fx-bg:#FFFFFF;--fx-card:#FFFFFF;--fx-line:#E2EAE6;--fx-muted:#5B7E6D;--fx-on-accent:#F5F5F5;--fx-field:#FFFFFF;--fx-field-border:#E5E7EB;--fx-field-text:#0F5132;--fx-option-surface:#E5CFB6;--fx-option-label-bar:#A45A05;--fx-option-label-text:#F5F5F5;--fx-danger:#B42318;--fx-success:#067647;--fx-button-hover:#945105;--fx-outline-hover:#A45A051A;--fx-display-font:"Arimo", "Arimo Fallback", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--fx-body-font:"Arimo", "Arimo Fallback", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--fx-radius:12px;--fx-media-radius:12px;--fx-col:448px;--fx-col-wide:896px;--fx-gutter:24px;--fx-media-max:none;--fx-gap:16px;--fx-stack:16px;--fx-control-h:56px;--fx-button-h:58px;--fx-tile-media-h:132px;--fx-logo-h:48px;--fx-trust-logo-h:40px;--fx-error-h:24px;--fx-question-size:30px;--fx-question-lh:1.15;--fx-question-track:-0.02em;--fx-question-weight:700;--fx-headline-size:24px;--fx-headline-lh:1.25;--fx-headline-track:-0.02em;--fx-headline-weight:700;--fx-body-size:20px;--fx-body-lh:1.25;--fx-body-track:0em;--fx-body-weight:400;--fx-caption-size:12px;--fx-caption-lh:1.5;--fx-caption-track:0em;--fx-caption-weight:400;--fx-list-item-size:16px;--fx-list-item-lh:1.4;--fx-list-item-track:0em;--fx-list-item-weight:400}@media (min-width:640px){:root{--fx-col:576px;--fx-question-size:36px;--fx-question-lh:1.15;--fx-question-track:-0.02em;--fx-question-weight:700;--fx-headline-size:30px;--fx-headline-lh:1.2;--fx-headline-track:-0.02em;--fx-headline-weight:700;--fx-body-size:20px;--fx-body-lh:1.25;--fx-body-track:0em;--fx-body-weight:400;--fx-caption-size:12px;--fx-caption-lh:1.5;--fx-caption-track:0em;--fx-caption-weight:400;--fx-list-item-size:16px;--fx-list-item-lh:1.4;--fx-list-item-track:0em;--fx-list-item-weight:400}}@media (min-width:1024px){:root{--fx-col:896px;--fx-gutter:0px;--fx-media-max:608px;--fx-media-radius:16px;--fx-question-size:48px;--fx-question-lh:1.15;--fx-question-track:-0.02em;--fx-question-weight:700;--fx-headline-size:36px;--fx-headline-lh:1.15;--fx-headline-track:-0.02em;--fx-headline-weight:700;--fx-body-size:24px;--fx-body-lh:1.25;--fx-body-track:0em;--fx-body-weight:400;--fx-caption-size:12px;--fx-caption-lh:1.5;--fx-caption-track:0em;--fx-caption-weight:400;--fx-list-item-size:16px;--fx-list-item-lh:1.4;--fx-list-item-track:0em;--fx-list-item-weight:400}}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--fx-bg);color:var(--fx-ink);font-family:var(--fx-body-font);font-size:16px;line-height:1.5}
img,svg{max-width:100%;height:auto;display:block}
button{font:inherit;color:inherit}
:focus-visible{outline:3px solid var(--fx-accent);outline-offset:2px}

.fx-page{min-height:100dvh;display:flex;flex-direction:column;overflow-x:hidden}
.fx-main{flex:1 0 auto;width:100%;max-width:var(--fx-col);margin-inline:auto;padding:20px var(--fx-gutter) 40px;display:flex;flex-direction:column;gap:var(--fx-stack)}
.fx-main[data-wide="true"]{max-width:var(--fx-col-wide)}

/* Logo: inside the column on mobile, pinned to the top left of the viewport at
   desktop (spec 5.4.1, measured 205x96 source rendered at 103x48). */
.fx-logo{height:var(--fx-logo-h);width:auto;object-fit:contain;object-position:left center}
.fx-logo-slot{display:flex;justify-content:flex-start}
@media (min-width:1024px){
  .fx-logo-slot[data-pin="pinned"]{position:absolute;top:20px;left:24px;z-index:2}
  .fx-logo-slot[data-pin="center"]{justify-content:center}
}

/* The five type roles. */
.fx-question{font-family:var(--fx-display-font);font-size:var(--fx-question-size);line-height:var(--fx-question-lh);letter-spacing:var(--fx-question-track);font-weight:var(--fx-question-weight);margin:0}
.fx-headline{font-family:var(--fx-display-font);font-size:var(--fx-headline-size);line-height:var(--fx-headline-lh);letter-spacing:var(--fx-headline-track);font-weight:var(--fx-headline-weight);margin:0}
.fx-body{font-size:var(--fx-body-size);line-height:var(--fx-body-lh);letter-spacing:var(--fx-body-track);font-weight:var(--fx-body-weight);margin:0}
.fx-caption{font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);font-weight:var(--fx-caption-weight);margin:0}
.fx-list-item{font-size:var(--fx-list-item-size);line-height:var(--fx-list-item-lh);font-weight:var(--fx-list-item-weight);margin:0}
.fx-al-left{text-align:left}
.fx-al-center{text-align:center}
.fx-rich a{color:inherit;text-underline-offset:2px}

/* Option tiles (spec 5.4.1): two across on mobile, four across at desktop, 16 px
   gap, the label as ONE text run so a word cannot split across two weights, and
   a width floor with an automatic shrink one size before wrapping. */
.fx-tiles{display:flex;flex-wrap:wrap;gap:var(--fx-gap);justify-content:center;align-items:stretch}
.fx-tile{flex:0 0 calc((100% - var(--fx-gap)) / 2);display:flex}
@media (min-width:1024px){.fx-tile{flex-basis:calc((100% - 3 * var(--fx-gap)) / 4)}}
.fx-tile-button{width:100%;display:flex;flex-direction:column;background:var(--fx-option-surface);border:0;border-radius:var(--fx-radius);overflow:hidden;padding:0;cursor:pointer;text-align:center}
.fx-tile-media{flex:1 1 auto;min-height:var(--fx-tile-media-h);display:grid;place-items:center;padding:16px}
.fx-tile-media img,.fx-tile-media svg{width:56px;height:56px}
.fx-tile-label{background:var(--fx-option-label-bar);color:var(--fx-option-label-text);font-size:var(--fx-body-size);line-height:var(--fx-body-lh);font-weight:700;padding:8px 6px;
  /* Shrink before wrap: one text run, no hyphenation, and a font size that
     steps down rather than breaking "Independence" across two lines the way the
     live funnel does (spec 5.4.1, defect 3). */
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fx-tile-label[data-long="true"]{font-size:calc(var(--fx-body-size) * 0.8)}
.fx-tile-label[data-long="wrap"]{white-space:normal;font-size:calc(var(--fx-body-size) * 0.8);text-overflow:clip}
.fx-tile-button[aria-pressed="true"]{box-shadow:inset 0 0 0 3px var(--fx-accent)}
.fx-tile-button:active{transform:scale(0.985)}

/* List and pill option styles, for a brand that does not want cards. */
.fx-options-list .fx-tile{flex-basis:100%}
.fx-options-list .fx-tile-button{flex-direction:row;align-items:center;gap:12px;padding:12px 16px;background:var(--fx-card);border:1px solid var(--fx-line);text-align:left}
.fx-options-list .fx-tile-label{background:none;color:var(--fx-ink);padding:0;white-space:normal}
.fx-options-pill .fx-tile{flex-basis:auto}
.fx-options-pill .fx-tile-button{border-radius:999px;padding:10px 18px;background:var(--fx-option-surface)}
.fx-options-pill .fx-tile-media{display:none}
.fx-options-pill .fx-tile-label{background:none;color:var(--fx-ink);padding:0}

/* Buttons (spec 5.4.1: full column width, 12 px radius, 58 px tall, 20/700). */
.fx-button{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:var(--fx-button-h);padding:12px 20px;border:0;border-radius:var(--fx-radius);background:var(--fx-accent);color:var(--fx-on-accent);font-size:var(--fx-body-size);font-weight:700;line-height:1.2;cursor:pointer;text-decoration:none}
.fx-button:hover{background:var(--fx-button-hover)}
.fx-button[data-variant="outline"]{background:transparent;color:var(--fx-accent);box-shadow:inset 0 0 0 1px var(--fx-accent)}
.fx-button[data-variant="outline"]:hover{background:var(--fx-outline-hover)}
.fx-button[disabled]{opacity:.55;cursor:not-allowed}

/* Fields (spec 5.4.1: 56 px tall, 12 px radius, 1 px border, 16 px text, a
   leading icon inside the wrapper, and the error message in RESERVED space so
   the layout does not shift). */
.fx-field{display:flex;flex-direction:column;gap:6px}
.fx-field-label{font-size:14px;font-weight:700}
.fx-field-box{position:relative;display:flex;align-items:center;background:var(--fx-field);border:1px solid var(--fx-field-border);border-radius:var(--fx-radius);min-height:var(--fx-control-h)}
.fx-field-box[data-invalid="true"]{border-color:var(--fx-danger)}
.fx-field-icon{flex:0 0 auto;display:grid;place-items:center;width:44px;height:var(--fx-control-h)}
.fx-field-icon img,.fx-field-icon svg{width:20px;height:20px}
.fx-field-box input,.fx-field-box textarea,.fx-field-box select{flex:1 1 auto;min-width:0;border:0;background:transparent;color:var(--fx-field-text);font:inherit;font-size:16px;padding:0 14px;height:calc(var(--fx-control-h) - 2px);outline:none}
.fx-field-box[data-icon="true"] input,.fx-field-box[data-icon="true"] textarea,.fx-field-box[data-icon="true"] select{padding-left:0}
.fx-field-box textarea{height:auto;min-height:130px;padding:14px;resize:vertical}
.fx-field-status{flex:0 0 auto;display:grid;place-items:center;width:40px;height:var(--fx-control-h)}
.fx-field-error{min-height:var(--fx-error-h);font-size:14px;line-height:var(--fx-error-h);color:var(--fx-danger);display:flex;align-items:center;gap:6px}
.fx-field-error:empty::before{content:"";display:block}
.fx-consent{display:flex;gap:10px;align-items:flex-start;font-size:14px;line-height:1.4}
.fx-consent input{margin-top:2px;width:20px;height:20px;flex:0 0 auto}

/* The Turnstile container (task 1.6). It has NO height of its own: the widget
   renders in interaction-only mode, so on almost all traffic the div stays
   empty and must take no vertical space at all. When Cloudflare does draw a
   challenge the iframe gives the box its own height and the CTA moves down,
   which is the one time that movement is what the visitor wants. */
.fx-turnstile:empty{display:none}
.fx-turnstile{display:flex;justify-content:center}
/* The submit failure line (spec 6.4's "plain please try again with the phone
   number"). Bordered rather than coloured text alone, so it reads as a state
   and not as small print. */
.fx-submit-error{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--fx-danger);border-radius:var(--fx-radius,10px);color:var(--fx-ink)}
.fx-submit-error .fx-body{margin:0;color:var(--fx-danger)}
.fx-submit-error .fx-caption{margin:0;color:var(--fx-muted)}
/* An outcome SEQUENCE (task 1.6, decision 5): every result and content step in
   order, one under the next, scrolling. The gap is the step stack's own, so
   two outcome pages read as one document rather than two crammed screens. */
.fx-outcome-step{display:flex;flex-direction:column;gap:var(--fx-stack)}
.fx-outcome-step[data-first="false"]{margin-top:var(--fx-stack)}

/* Media. A column image takes the media radius; the postcode step's map runs
   full bleed, which is the one image on the live funnel that does. */
.fx-media{border-radius:var(--fx-media-radius);overflow:hidden}
.fx-media[data-bleed="column"]{max-width:var(--fx-media-max);margin-inline:auto;width:100%}
/* The wrapper carries the aspect ratio (see components/blocks.tsx), so the
   image FILLS it and a broken source cannot collapse the box. */
.fx-media img{width:100%;height:100%;object-fit:inherit}
.fx-media[data-bleed="full"]{border-radius:0;width:100vw;max-width:none;margin-left:calc(50% - 50vw)}

/* The result page's numbered steps: one column on mobile, a 2 by 2 grid at
   desktop, keycap in a left column at 48 px (spec 5.4.1). */
.fx-steps{list-style:none;margin:0;padding:0;display:grid;gap:20px}
@media (min-width:1024px){.fx-steps{grid-template-columns:1fr 1fr;column-gap:32px}}
.fx-step-item{display:flex;gap:14px;align-items:flex-start}
.fx-step-key{flex:0 0 auto;width:48px;height:48px;display:grid;place-items:center;border-radius:8px;background:var(--fx-option-surface);color:var(--fx-option-label-bar);font-weight:700;font-size:20px}
.fx-step-body strong{font-weight:700}

.fx-divider{border:0;border-top:1px solid var(--fx-line);margin:8px 0;width:100%}

/* ── The parity-audit gap fixes (G3a, G3b, G3c) ──────────────────────────── */

/* columns (spec 5.2), gap G3b. The renderer used to emit NO container at all,
   so children stacked at every width. One column below the desktop breakpoint
   and an even track per column above it, which is `.fx-steps`' pattern rather
   than a second one. `minmax(0,1fr)` and not `1fr`: a grid track's default
   `min-width:auto` is the content's intrinsic width, so a long unbroken string
   or a wide image in one column would otherwise push the row past the content
   column instead of wrapping inside its own track. */
.fx-columns{display:grid;gap:var(--fx-gap);width:100%}
.fx-column{display:flex;flex-direction:column;gap:var(--fx-gap);min-width:0}
@media (min-width:1024px){
  .fx-columns[data-cols="2"]{grid-template-columns:repeat(2,minmax(0,1fr))}
  .fx-columns[data-cols="3"]{grid-template-columns:repeat(3,minmax(0,1fr))}
  .fx-columns[data-cols="4"]{grid-template-columns:repeat(4,minmax(0,1fr))}
}

/* review_badge (spec 5.2), gap G3a. The star row is TWO layers: a full grey
   five-star row, and a clipped gold copy over it whose width is the rating as a
   percentage. That draws a genuine 4.3 rather than rounding to the nearest half
   star while the number beside it says something else. The glyphs are text, not
   an image: this is the one place the platform's own star is right, because it
   is a shape and not a colour-critical mark, and five `<img>` requests for a
   decorative row would cost more than it is worth. */
.fx-review-badge{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:center}
.fx-review-badge.fx-al-left{justify-content:flex-start}
.fx-review-stars{position:relative;display:inline-block;line-height:1;font-size:18px;letter-spacing:2px;color:var(--fx-line);white-space:nowrap}
.fx-review-stars::before{content:"\2605\2605\2605\2605\2605"}
.fx-review-stars-on{position:absolute;inset:0;overflow:hidden;width:var(--fx-review-fill,0%);color:#f5a623}
.fx-review-stars-on::before{content:"\2605\2605\2605\2605\2605"}
.fx-review-text{color:var(--fx-muted)}

/* confetti (spec 5.2), gap G3c. Outcome pages only, CSS only, no dependency.
   The box is `position:relative` with a fixed height so the burst cannot
   overlay the copy under it or add scroll to the page; `overflow:hidden` keeps
   a drifting piece inside it. Twelve pieces, each given its own start, colour
   and delay below. UNDER `prefers-reduced-motion` THE PIECES NEVER APPEAR: the
   only rule that makes them visible lives inside the no-preference query with
   the rest of the motion, so "reduced" is a static empty box and not a slower
   animation. That is the promise the renderer's own comment makes. */
.fx-confetti{position:relative;height:120px;width:100%;overflow:hidden;pointer-events:none}
.fx-confetti-piece{position:absolute;top:-12px;width:8px;height:14px;opacity:0;border-radius:1px}
.fx-confetti-piece[data-piece="0"]{left:4%;background:var(--fx-accent);animation-delay:0ms}
.fx-confetti-piece[data-piece="1"]{left:12%;background:#f5a623;animation-delay:120ms}
.fx-confetti-piece[data-piece="2"]{left:21%;background:var(--fx-brand);animation-delay:240ms}
.fx-confetti-piece[data-piece="3"]{left:29%;background:#3ac47d;animation-delay:60ms}
.fx-confetti-piece[data-piece="4"]{left:38%;background:var(--fx-accent);animation-delay:300ms}
.fx-confetti-piece[data-piece="5"]{left:46%;background:#f5a623;animation-delay:180ms}
.fx-confetti-piece[data-piece="6"]{left:55%;background:var(--fx-brand);animation-delay:80ms}
.fx-confetti-piece[data-piece="7"]{left:63%;background:#3ac47d;animation-delay:360ms}
.fx-confetti-piece[data-piece="8"]{left:72%;background:var(--fx-accent);animation-delay:200ms}
.fx-confetti-piece[data-piece="9"]{left:80%;background:#f5a623;animation-delay:40ms}
.fx-confetti-piece[data-piece="10"]{left:89%;background:var(--fx-brand);animation-delay:280ms}
.fx-confetti-piece[data-piece="11"]{left:96%;background:#3ac47d;animation-delay:140ms}

/* ── Task 3.6's six block kinds ──────────────────────────────────────────── */

/* Video (spec 5.2). The 16:9 box is RESERVED the way `.fx-media` reserves an
   image's, and for the same measured reason: a player with no height until its
   source arrives collapses, and everything under it jumps when it does. The
   iframe and the `video` tag both fill the box absolutely so neither can give
   it a height of its own. */
.fx-video{position:relative;width:100%;max-width:var(--fx-media-max);margin-inline:auto;aspect-ratio:16 / 9;border-radius:var(--fx-media-radius);overflow:hidden;background:var(--fx-option-surface)}
.fx-video iframe,.fx-video video{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;object-fit:contain;background:#000}

/* Trust logos. A wrapping row of small marks, each capped so no logo reaches
   the content column's width whatever its intrinsic size. The cap is also
   `TRUST_LOGO_MAX_W` in `lib/images.ts`, which derives the `sizes` string from
   it; a test pins the two together. */
.fx-trust-logos{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--fx-gap)}
.fx-trust-logos.fx-al-left{justify-content:flex-start}
.fx-trust-logos .fx-media{width:auto;max-width:160px;height:var(--fx-trust-logo-h);border-radius:0;flex:0 0 auto}
.fx-trust-logos .fx-media img{width:auto;height:100%;max-width:160px;object-fit:contain}

/* Countdown (spec 5.2, 10.3). A fixed-height row of four units, so the box is
   the same size before the numbers arrive after mount as after: see
   `components/countdown.tsx` for why the server renders placeholders. */
.fx-countdown{display:flex;justify-content:center}
.fx-countdown.fx-al-left{justify-content:flex-start}
.fx-countdown-units{display:flex;gap:var(--fx-gap)}
.fx-countdown-unit{display:flex;flex-direction:column;align-items:center;min-width:64px}
.fx-countdown-value{font-family:var(--fx-display-font);font-size:var(--fx-headline-size);line-height:1.1;font-weight:700;font-variant-numeric:tabular-nums;color:var(--fx-ink)}
.fx-countdown-label{font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);color:var(--fx-muted)}
.fx-countdown-date{font-size:var(--fx-body-size);line-height:var(--fx-body-lh);color:var(--fx-muted)}

/* FAQ. Native `details`/`summary`, so the open and close need no script. The
   marker is left as the browser's own: replacing it is a cosmetic change that
   costs the platform's built-in expanded-state announcement. */
.fx-faq{display:flex;flex-direction:column;gap:0}
.fx-faq-item{border-bottom:1px solid var(--fx-line)}
.fx-faq-q{font-size:var(--fx-body-size);line-height:var(--fx-body-lh);font-weight:700;padding:14px 0;cursor:pointer;list-style-position:outside}
.fx-faq-a{padding:0 0 14px;color:var(--fx-muted)}

/* Testimonials, as a scroll-snap rail rather than a scripted carousel. One card
   fills the column on a phone and the row scrolls; two fit at desktop. */
.fx-testimonials{list-style:none;margin:0;padding:0;display:flex;gap:var(--fx-gap);overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}
.fx-testimonial{flex:0 0 100%;scroll-snap-align:start}
@media (min-width:1024px){.fx-testimonial{flex-basis:calc((100% - var(--fx-gap)) / 2)}}
.fx-testimonial figure{margin:0;height:100%;display:flex;flex-direction:column;gap:10px;padding:16px;background:var(--fx-card);border:1px solid var(--fx-line);border-radius:var(--fx-radius)}
.fx-testimonial blockquote{margin:0}
.fx-testimonial figcaption{display:flex;flex-wrap:wrap;gap:8px;color:var(--fx-muted);margin-top:auto}
.fx-testimonial-name{font-style:normal;font-weight:700;color:var(--fx-ink)}

/* image_choice (spec 5.2: "large imagery; swipe on mobile"), task 3.6.

   A SCROLL-SNAP RAIL BELOW THE DESKTOP BREAKPOINT AND A GRID ABOVE IT, the same
   mechanism `.fx-testimonials` uses and for the same reason: a scripted
   carousel is JS on the one route with a 100 KB budget, and it reimplements
   momentum, rubber-banding and the keyboard model the browser already has.

   The tile is `IMAGE_CHOICE_RAIL_FRACTION` of the rail, not all of it, so the
   next one peeks and the swipe is discoverable. `overflow-x` and the snap are
   RESET at desktop rather than left on a non-scrolling row, because a stale
   `scroll-snap-type` on a wrapped flex container snaps rows vertically in some
   engines. The media box is a FIXED ratio so a row of client photographs at
   four different aspect ratios still draws four tiles of one height, and it is
   reserved before the bytes arrive, which is what keeps this step out of the
   CLS budget. `lib/images.ts` derives the `sizes` string from these same
   numbers and `scripts/step-inputs.test.ts` pins the two together. */
.fx-imgchoice{display:flex;gap:var(--fx-gap);align-items:stretch;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scroll-padding-inline:var(--fx-gutter);padding-bottom:4px}
.fx-imgchoice-cell{flex:0 0 78%;scroll-snap-align:start;display:flex}
.fx-imgchoice-button{width:100%;display:flex;flex-direction:column;background:var(--fx-option-surface);border:0;border-radius:var(--fx-radius);overflow:hidden;padding:0;cursor:pointer;text-align:center}
.fx-imgchoice-media{position:relative;width:100%;aspect-ratio:4 / 3;display:grid;place-items:center;background:var(--fx-line)}
.fx-imgchoice-media img{width:100%;height:100%}
.fx-imgchoice-label{display:block;background:var(--fx-option-label-bar);color:var(--fx-option-label-text);font-size:var(--fx-body-size);line-height:var(--fx-body-lh);font-weight:700;padding:10px 8px}
.fx-imgchoice-desc{display:block;margin-top:2px;font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);font-weight:400;opacity:.85}
.fx-imgchoice-button[aria-checked="true"]{box-shadow:inset 0 0 0 3px var(--fx-accent)}
.fx-imgchoice-button:active,.fx-star:active{transform:scale(0.985)}
@media (min-width:1024px){
  .fx-imgchoice{flex-wrap:wrap;overflow-x:visible;scroll-snap-type:none;justify-content:center}
  .fx-imgchoice-cell{flex-basis:calc((100% - 1 * var(--fx-gap)) / 2)}
}

/* slider (spec 5.2: "steps, labels, live value"), task 3.6.

   `accent-color` AND NOT A VENDOR PSEUDO-ELEMENT LADDER. One declaration puts
   the native track and thumb on the brand's colour in every target engine, and
   it leaves the control NATIVE, which is the whole reason this step is an
   `input` rather than a drag handler on a div.

   NO `::-webkit-slider-thumb` RULE, and that is a deletion rather than an
   omission: one was written here first and would have been dead CSS. WebKit
   and Blink ignore geometry on that pseudo-element unless the input also
   carries `appearance:none`, and `appearance:none` throws away the whole
   native control, `accent-color` with it, so the track would then have to be
   redrawn by hand in three engines. The touch target comes from the input's own
   44 px height instead, which is what a thumb rule would have been reaching
   for. */
.fx-slider{display:flex;flex-direction:column;gap:var(--fx-stack)}
.fx-slider-readout{margin:0;text-align:center;font-family:var(--fx-display-font);font-size:var(--fx-headline-size);line-height:1.1;font-weight:700;color:var(--fx-accent)}
.fx-slider-input{width:100%;height:44px;margin:0;background:transparent;accent-color:var(--fx-accent);cursor:pointer}
.fx-slider-ends{margin:0;display:flex;justify-content:space-between;gap:12px;color:var(--fx-muted);font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh)}

/* rating (spec 5.2: "1 to 5 or 1 to 10"), task 3.6.

   The stars WRAP, because a ten-point scale at a 44 px touch target is 440 px
   and the mobile column is 327. A scale of ten on a phone is therefore two rows
   of five rather than ten stars shrunk below the minimum target size. */
.fx-rating{display:flex;flex-wrap:wrap;justify-content:center;gap:4px}
.fx-star{display:grid;place-items:center;min-width:44px;min-height:44px;padding:2px;background:none;border:0;cursor:pointer;color:var(--fx-line)}
.fx-star[data-on="true"]{color:var(--fx-accent)}

/* The hold step (spec 5.2): a bounded wait with a live region, never a spinner
   that a screen reader cannot see. */
.fx-hold{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 0}
.fx-hold-dial{width:48px;height:48px;border-radius:50%;border:4px solid var(--fx-line);border-top-color:var(--fx-accent)}

.fx-footer{flex:0 0 auto;padding:24px var(--fx-gutter) 32px;text-align:center;color:var(--fx-muted);font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh)}
.fx-footer a,.fx-footer button{color:inherit;background:none;border:0;padding:0;text-decoration:underline;cursor:pointer;font:inherit}
.fx-footer-sep{margin:0 8px}

.fx-progress{height:6px;border-radius:999px;background:var(--fx-line);overflow:hidden}
.fx-progress-fill{height:100%;background:var(--fx-accent)}
.fx-progress-dots{display:flex;gap:6px;justify-content:center}
.fx-progress-dot{width:8px;height:8px;border-radius:50%;background:var(--fx-line)}
.fx-progress-dot[data-on="true"]{background:var(--fx-accent)}

.fx-back{align-self:flex-start;background:none;border:0;padding:4px 0;color:var(--fx-muted);cursor:pointer;font-size:14px;text-decoration:underline}

/* The returning-visitor offer (spec 5.7). OUT OF FLOW so it cannot shift the
   page: it appears a paint after hydration, and anything inserted into the
   flow at that moment moves the content under it. CLS measures 0.0000 and this
   is one of the reasons it still does. */
.fx-resume{position:fixed;left:0;right:0;bottom:0;z-index:20;display:flex;flex-direction:column;gap:10px;padding:16px var(--fx-gutter,24px) calc(16px + env(safe-area-inset-bottom));background:var(--fx-card);border-top:1px solid var(--fx-line);box-shadow:0 -4px 16px #00000014}
.fx-resume-text{margin:0;font-size:var(--fx-body-size);color:var(--fx-ink)}
.fx-resume-actions{display:flex;flex-direction:column;gap:8px}
@media (min-width:640px){.fx-resume{align-items:center}.fx-resume-actions{flex-direction:row}.fx-resume .fx-button{width:auto;padding-left:28px;padding-right:28px}}

.fx-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* Consent (task 1.5, spec 6.5 and 10.1).

   THE BAR IS NON-BLOCKING AND OUT OF FLOW, which spec 6.5 requires in as many
   words: "a non-blocking bottom bar over a fully rendered step 1". Fixed to the
   bottom like `.fx-resume`, and for the same reason: it appears a paint after
   hydration, and anything inserted into the FLOW at that moment moves the
   content under it, which is a layout shift on the one page whose CLS is
   measured at zero.

   REJECT IS AS EASY AS ACCEPT (spec 10.1). The two buttons are the same
   element, the same size and the same weight; only the fill differs, and the
   outline variant is the existing `data-variant="outline"` rather than a
   quieter one invented here. A reject control that is smaller, greyer or a bare
   text link is the dark pattern every one of these regimes names by name. */
.fx-consent{position:fixed;left:0;right:0;bottom:0;z-index:30;display:flex;flex-direction:column;gap:12px;padding:16px var(--fx-gutter,24px) calc(16px + env(safe-area-inset-bottom));background:var(--fx-card);border-top:1px solid var(--fx-line);box-shadow:0 -4px 16px #00000014;max-height:80vh;overflow-y:auto}
.fx-consent-text{margin:0;font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);color:var(--fx-ink)}
.fx-consent-actions{display:flex;flex-direction:column;gap:8px}
.fx-consent-purposes{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;border:0}
.fx-consent-purpose{display:flex;gap:10px;align-items:flex-start}
.fx-consent-purpose input{margin:2px 0 0;flex:0 0 auto;width:18px;height:18px}
.fx-consent-purpose-label{display:block;font-size:var(--fx-body-size);color:var(--fx-ink)}
.fx-consent-purpose-body{display:block;margin-top:2px;font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);color:var(--fx-muted)}
.fx-consent-note{margin:0;font-size:var(--fx-caption-size);line-height:var(--fx-caption-lh);color:var(--fx-muted)}
.fx-consent-title{margin:0;font-size:var(--fx-body-size);font-weight:600;color:var(--fx-ink)}
@media (min-width:640px){.fx-consent{align-items:stretch}.fx-consent-actions{flex-direction:row;justify-content:flex-end}.fx-consent .fx-button{width:auto;padding-left:28px;padding-right:28px}}

/* Motion (spec 5.4.1: the live theme's transition is `slide`).
   React 19.2.7 exports no ViewTransition component and Next 15.5.25 wires none,
   verified in this repo's own install, so the slide is a CSS animation keyed to
   the step. `prefers-reduced-motion` wins over `theme.motion` in every case:
   the rule below only ever ADDS animation, and it is inside the
   no-preference query. */
@media (prefers-reduced-motion:no-preference){
  .fx-step[data-motion="full"]{animation:fx-slide 240ms cubic-bezier(.2,.6,.2,1)}
  .fx-step[data-motion="subtle"]{animation:fx-fade 160ms ease}
  .fx-hold-dial{animation:fx-spin 900ms linear infinite}
  .fx-tile-button,.fx-imgchoice-button,.fx-star{transition:transform 90ms ease,box-shadow 120ms ease}
  .fx-button{transition:background-color 120ms ease}
  /* Gap G3c. THE ONLY RULE THAT MAKES A CONFETTI PIECE VISIBLE, and it is
     inside this query on purpose: a visitor who asked for reduced motion gets a
     static, empty, 120 px box rather than a slower burst. `forwards` leaves
     the last frame in place, which is `opacity:0`, so the pieces clear
     themselves without a second rule or any script. */
  .fx-confetti-piece{animation:fx-confetti 2200ms cubic-bezier(.2,.6,.4,1) forwards}
}
@keyframes fx-slide{from{opacity:0;transform:translate3d(24px,0,0)}to{opacity:1;transform:none}}
@keyframes fx-fade{from{opacity:0}to{opacity:1}}
@keyframes fx-spin{to{transform:rotate(360deg)}}
@keyframes fx-confetti{
  0%{opacity:0;transform:translate3d(0,-10px,0) rotate(0deg)}
  10%{opacity:1}
  85%{opacity:1}
  100%{opacity:0;transform:translate3d(14px,132px,0) rotate(540deg)}
}
