/*
 * preview/css/01-tokens.css
 * Design tokens — colors, spacing, typography, motion.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L1–L303.
 */

  /* ─── Design tokens ──────────────────────────────────────────── */
  :root {
    --color-background-canvas: #f9f9ff;
    --color-background-primary: #ffffff;
    --color-background-secondary: #eaefff;
    --color-background-brand: #5856d6;
    --color-background-pink: #ff2da4;
    --color-background-cyan: #32ade6;
    --color-background-orange: #ffae00;
    --color-background-mint: #00c78f;
    --color-text-primary: #393c46;
    --color-text-secondary: #6E7F97;
    --color-text-tertiary: #8695aa99; /* Figma Text/tertiary — 60% alpha */
    --color-text-info: #0264f7;
    --color-text-success: #248a3d;
    --color-text-danger: #ff4d4f;             /* Figma Text/danger — flag if a stricter hex is supplied */
    --color-text-brand: #5856d6;
    --color-text-on-accent: #ffffff;
    --color-text-ghost: rgba(115, 114, 108, 0.50); /* Figma Text/ghost — used for disabled action labels */
    --color-border-primary: #dde4f9;
    --color-border-tertiary: #eef0f2;
    --color-border-brand: #5856d6;
    --color-border-disabled: rgba(31, 30, 29, 0.10); /* Figma Border/disabled — disabled action outlines */
    --color-ring-inverse: #ffffff;
    --color-ring-brand: rgba(88, 86, 214, 0.10);
    --color-ring-primary: rgba(58, 67, 82, 0.15); /* Figma Ring/primary */
    --color-ring-secondary: rgba(67, 78, 97, 0.05); /* Figma Ring/secondary — used for inline input plates on the Payment screen */
    --color-ring-accent-danger: rgba(255, 77, 79, 0.10); /* Figma Ring/accent-danger — same alpha pattern as ring-brand */

    /* ─── Positive / negative state tokens ─────────────────────────
       Used by the meaning-screen answer buttons (Meaning 1 / Meaning 2).
       Tokens follow the Figma names `fill-ring-success`, `border-mint`,
       `fill-ring-danger`, `border-danger`. The success/mint pair are
       derived from the existing `--color-background-mint` (10% alpha
       fill, solid mint border, mirroring the brand ring + brand border
       pattern). The danger pair re-uses `--color-ring-accent-danger`
       and `--color-text-danger` so we don't duplicate hex values.
       FLAG: these names did not exist in the design system before this
       commit; the values below are the closest safe interpretation
       and should be re-checked against Figma when the next design
       sync happens. */
    --color-fill-ring-success: rgba(0, 199, 143, 0.10);
    --color-ring-mint:       #00c78f;
    --color-fill-ring-danger:  rgba(255, 77, 79, 0.10);
    --color-border-danger:     #ff4d4f;

    --space-4: 4px;
    --space-8: 8px;
    --space-12: 12px;
    --space-16: 16px;
    --space-20: 20px;
    --space-24: 24px;
    --space-32: 32px;

    --radius-xxs: 4px;
    --radius-medium: 20px;

    --font-family-base: "SF Pro Display", -apple-system, BlinkMacSystemFont,
      "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    /* ─── Press / interaction motion tokens ─────────────────────
       Single, shared interaction pattern reused by every clickable
       surface in the project (CTAs, option cards, language tiles,
       pricing plans, persona buttons, theme toggle, etc.).
       Larger amplitude + softer "elastic" easing than before, but
       still controlled — no cartoon overshoot.

       Usage:
         .btn       { transition: var(--press-transition), …; }
         .btn:active{ transform: scale(var(--press-scale)); }
       and for compact controls:
         .icon-btn:active { transform: scale(var(--press-scale-small)); }
       --------------------------------------------------------- */
    --press-scale: 0.98;
    --press-scale-small: 0.985;
    --press-transition: transform 100ms ease;
    --press-scale-cta: 0.97;
    --press-transition-cta: transform 100ms ease;
  }

  /*
   * Dark theme — overrides the colour tokens only (spacing, radii,
   * typography stay identical so the design system stays consistent).
   * Activated by setting `data-theme="dark"` on the document root.
   *
   * Values are taken VERBATIM from the Figma "Colors" variable
   * collection ("Dark" mode column).  Notable points:
   *   - Text/brand swaps from purple #5856d6 → blue #4f84ff in dark
   *     (per Figma it's a different semantic colour, not a tint).
   *   - Text/secondary uses #73849D in both modes (kept identical
   *     across light/dark per Figma — do NOT lift it for dark).
   *   - Background/secondary is the lift-off-canvas grey-blue
   *     #434e61, not a deep #20253a — Figma uses it for context
   *     plates / hero backdrops which need to sit clearly above
   *     the canvas.
   */
  :root[data-theme="dark"] {
    --color-background-canvas:    #1e1f28;
    --color-background-primary:   #292b33;
    --color-background-secondary: #434e61;
    --color-background-brand:     #5e5ce6;
    /* Coloured accents shift slightly per Figma's dark palette. */
    --color-background-pink:      #ff37ca;
    --color-background-cyan:      #64d2ff;
    --color-background-orange:    #ffbe0a;
    --color-background-mint:      #63e6ab;
    --color-text-primary:    #ffffff;
    --color-text-secondary:  #6E7F97;
    --color-text-tertiary:   rgba(134, 149, 170, 0.6);
    --color-text-info:       #0a84ff;
    --color-text-success:    #30db5b;
    --color-text-danger:     #ff6b6d;
    --color-text-brand:      #4f84ff;
    --color-text-on-accent:  #ffffff;
    --color-text-ghost:      rgba(255, 255, 255, 0.30); /* dark equivalent of light text/ghost */
    --color-border-primary:  #3b4555;
    --color-border-tertiary: #3d3e46;
    --color-border-brand:    #5e5ce6;
    --color-border-disabled: rgba(255, 255, 255, 0.10); /* dark equivalent of light border/disabled */
    --color-ring-inverse:    #333c4b;
    --color-ring-brand:      rgba(154, 152, 255, 0.10);
    --color-ring-primary:    rgba(246, 247, 249, 0.15);
    --color-ring-secondary:  rgba(246, 247, 249, 0.06);
    --color-ring-accent-danger: rgba(255, 107, 109, 0.18);
    /* Dark-mode counterparts of the success / danger meaning tokens.
       Mint ring lifts slightly (18 % alpha) to read on the darker
       canvas, matching `--color-ring-accent-danger` alpha logic.
       Border colours track the dark mint / danger text tokens. */
    --color-fill-ring-success: rgba(99, 230, 171, 0.18);
    --color-ring-mint:       #63e6ab;
    --color-fill-ring-danger:  rgba(255, 107, 109, 0.18);
    --color-border-danger:     #ff6b6d;
  }

  /*
   * Smooth fade between themes — colour-only.  Applied broadly so
   * surface containers (cards, image plates, sticky footers) follow
   * the theme swap without lag, including .dyk__hero, .langCard,
   * .option, .topt, .plan, .actionContainer, etc.
   */
  html, body, .app, .screen,
  .langCard, .dykCard, .option, .topt, .plan, .actionContainer,
  .dyk__hero, .nameInputCard, .writing__card, .writing__contextCard,
  .writing__contextBadge, .header, .trustBlock, .trustLogo,
  .compareTable, .compareTable__cell,
  .themeSwitch__btn, .paywallTimer, .readiness__value,
  .source, .audioRow, .accordion, .tbody__title {
    transition:
      background-color 220ms ease,
      color 220ms ease,
      border-color 220ms ease;
  }

  * { box-sizing: border-box; }
  /*
   * Suppress the default Chromium/Android translucent tap-highlight
   * rectangle.  By default a tap on any focusable element (button,
   * link, role="radio", etc.) flashes a system blue/dark overlay,
   * which clashes with our own press feedback (the `.cta:active`
   * scale-down and the per-card hover/selected ring).  Setting this
   * to `transparent` removes the overlay without disabling pointer
   * events or the underlying focus-visible / active styles.
   * Desktop browsers and iOS Safari ignore the property — no-op
   * there.
   */
  * { -webkit-tap-highlight-color: transparent; }
  /*
   * Scroll model — carefully tuned so vertical inertial scroll
   * works on iOS Safari + iOS Simulator AND horizontal-bleed
   * carousels (Trusted-by, How-it-works) don't trigger a real
   * horizontal scrollbar.
   *
   *   1. `html` keeps `height: 100%` so percentage-based heights
   *      below have a concrete reference (`.viewport` uses
   *      `min-height: 100vh`).  Crucially `html` has NO explicit
   *      overflow set — implicit `overflow: visible` is required
   *      for the CSS "viewport propagation" rule (Overflow L3) to
   *      hand the body's overflow value off to the actual window
   *      scroll.  Earlier this had `overflow-x: hidden`, which
   *      resolved the unset y-axis to `auto` and turned `<html>`
   *      itself into a scroll container.  Mobile Safari handles
   *      that mode inconsistently — inertial scroll over the
   *      fixed action bar simply did not engage.
   *
   *   2. `body` uses `min-height: 100%` (NOT `height: 100%`) so it
   *      grows with its content past the viewport when needed.
   *      With `height: 100%`, body was clamped to the viewport and
   *      content overflowed silently.
   *
   *   3. `body { overflow-x: hidden }` is what actually clips the
   *      horizontal carousel break-outs.  Because `html` has
   *      visible overflow, the propagation rule applies the body's
   *      overflow to the viewport itself: x is clipped at the
   *      window edge, y scrolls natively.  Body is treated as if
   *      it had `overflow: visible` after propagation, so it does
   *      not establish its own scroll container — and that is what
   *      lets iOS run the native momentum scroll.
   */
  html {
    margin: 0;
    padding: 0;
    height: 100%;
  }
  body {
    margin: 0;
    padding: 0;
    min-height: 100%;
    overflow-x: hidden;
    font-family: var(--font-family-base);
    color: var(--color-text-primary);
    -webkit-font-smoothing: antialiased;
    background: var(--color-background-canvas);
  }

  /*
   * ─── Non-scroll (fixed) screen mode ──────────────────────────
   *
   * Some screens are designed as static scenes that should fit
   * inside the mobile viewport without ANY vertical scroll —
   * Welcome, Language pick, Name, Auth, Ratings, Offer, persona
   * Q&A flow, and so on.  Other screens (Paywall, FAQ, long
   * legal) are intentionally taller than the viewport and need
   * scroll — those keep the default flow.
   *
   * The decision is made per-screen via the JS `FIXED_SCREENS`
   * set wired in `showScreen()`: when the active screen is fixed,
   * `<html>` gets the `is-fixed-screen` class; the rules below
   * clamp the chain (html → body → .app → .viewport → .screen.active)
   * to the dynamic viewport height (`100dvh`) and disable overflow.
   *
   * Why `dvh` (dynamic viewport height) and NOT `vh`:
   *   On mobile Safari / Chrome the URL bar collapses on scroll,
   *   making `100vh` taller than the actually-visible window —
   *   which would push the bottom of a fixed scene past the screen
   *   edge.  `100dvh` tracks the visible window in real time, so a
   *   fixed scene always sits inside the *visible* viewport on
   *   every browser-bar state, including when the soft keyboard
   *   shrinks the available space on the input screens (Name / Auth).
   *
   * `100vh` is kept as a fallback first, then `100dvh` overrides
   * on browsers that support it (Safari 15.4+, Chrome 108+,
   * Firefox 101+ — all current target devices).
   *
   * If a screen turns out NOT to fit at runtime (e.g. a Russian
   * translation pushes the layout taller), remove the screen's
   * name from `FIXED_SCREENS` and it falls back to scrollable
   * with no other code changes.
   */
  html.is-fixed-screen {
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
  }
  html.is-fixed-screen body {
    height: 100vh;
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }
  html.is-fixed-screen .app {
    min-height: 0;
    height: 100vh;
    height: 100dvh;
    /* `.app` is the absolute-positioning anchor for the bottom
       action bar (`.actionContainer`).  `.app` spans the entire
       browser width (no max-width), so an absolute child with
       `left: 0; right: 0` covers the full viewport — even on
       desktop where `.viewport` is constrained to 375 px and
       centred inside `.app`.  Earlier we anchored the action bar
       to `.screen.active`, which inherited the 375-px width of
       `.viewport` and therefore never spanned the full browser. */
    position: relative;
  }
  html.is-fixed-screen .viewport {
    min-height: 0;
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    /* `overflow: hidden` deliberately NOT set here — would clip
       the full-width action bar (a sibling/descendant) at the
       viewport's 375-px boundary on desktop.  Vertical / horizontal
       page scroll is already prevented at html/body level above,
       so per-container clipping is redundant. */
    /* `position: static` overrides the default `.viewport
       { position: relative }`.  Without this, the absolute-positioned
       `.actionContainer` would climb to `.viewport` (max-width: 375)
       as its containing block instead of `.app` (full browser
       width), and its background would shrink to 375 px on desktop
       emulation.  Static here lets the action bar's containing
       block resolve up to `.app`, so the white shell spans the
       entire browser width regardless of `.viewport`'s narrower
       content slot above it. */
    position: static;
  }
  html.is-fixed-screen .screen.active {
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    /* same — no overflow clip here, see comment on `.viewport`. */
  }

/*
 * preview/css/02-shell.css
 * App shell — header, content region, action container, CTA, screen visibility.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L304–L649.
 */

  /* ─── App shell ──────────────────────────────────────────────── */
  /*
   * Baseline: viewport is 375 wide on every screen. The language
   * screen opts into a wider viewport by setting data-active-step.
   */
  .app {
    min-height: 100%;
    display: flex;
    justify-content: center;
    background: var(--color-background-canvas);
  }
  .viewport {
    width: 100%;
    max-width: 375px;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    position: relative;
    background: var(--color-background-canvas);
  }
  .app[data-active-step="language"] .viewport { max-width: 375px; }
  @media (min-width: 425px) {
    .app[data-active-step="language"] .viewport { max-width: 425px; }
  }
  @media (min-width: 768px) {
    .app[data-active-step="language"] .viewport { max-width: 768px; }
  }

  /* ─── Header — centered logo only (no progress on these screens). */
  .header {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-24) var(--space-16) var(--space-16);
    width: 100%;
  }

  /*
   * Theme switch — Figma 6252:4676 ("Frame 296484704").
   * Segmented pair of identical-width pill buttons (Dark / Light)
   * sitting between the welcome content and the bottom action
   * container.  Each button is 92.67 × 56, padding 16/24, radius 20,
   * gap 8 (icon ↔ label).  Inter-button gap is 14 (Figma).
   *
   * Selected state matches the rest of the design system —
   * Ring/brand background + 2 px Border/brand border + Text/brand
   * label, with a 1 px padding shave so the card doesn't grow.
   */
  /*
   * Theme switch — sized to fit "[icon] Dark" / "[icon] Light".
   * Figma sets a literal 92.67 px width per button, but with 24×24
   * icon + 8 px gap + label that overflows the 24 px horizontal
   * padding.  We use min-width (matching Figma's intended footprint)
   * and let the buttons grow content-first so the icons stay 24×24
   * and the labels are never clipped.
   */
  .themeSwitch {
    margin: 24px auto 0;
    display: inline-flex;
    align-items: center;
    gap: 14px;
  }
  .themeSwitch__btn {
    min-width: 92.67px;
    height: 56px;
    padding: var(--space-16) var(--space-12);
    border-radius: var(--radius-medium);
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    color: var(--color-text-brand);
    font-family: inherit;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    cursor: pointer;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transform-origin: center center;
    transition:
      background-color 150ms ease,
      border-color 150ms ease,
      var(--press-transition);
  }
  /*
   * Unified hover — 8 % brand-tinted lift over the surface.  Same
   * formula as .langCard:hover so every selectable button-card in
   * the project (lang / option / topt / plan / dykCard / themeSwitch)
   * shares one hover language.  Works in both light and dark mode
   * because both inputs are theme tokens.
   */
  /* (See block below — transform-origin/transition unified via
     press tokens before this hover state declaration.) */
  /* Hover gated to real pointer devices — see `.option:hover` for
     the touch-tap-sticks-hover rationale. */
  @media (hover: hover) {
    .themeSwitch__btn:hover:not(.themeSwitch__btn--selected) {
      background: color-mix(in srgb, var(--color-ring-inverse) 92%, var(--color-background-brand));
      border-color: var(--color-border-tertiary);
    }
  }
  .themeSwitch__btn:active { transform: scale(var(--press-scale-small)); }
  .themeSwitch__btn--selected {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    /* Compensate for the +1 px border so the cards don't shift. */
    padding: calc(var(--space-16) - 1px) calc(var(--space-12) - 1px);
  }
  /*
   * Icon stays its full 24×24 — `flex-shrink: 0` keeps the SVG from
   * collapsing inside the inline-flex row when the button is at
   * min-width.
   */
  .themeSwitch__btn svg {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    display: block;
    color: currentColor;
  }
  /* Demo-only note under the theme switch — explains that the
     switch itself won't ship to production. The label is in
     Russian per Alexey's request. */
  .themeSwitch__note {
    margin: 8px auto 0;
    max-width: 280px;
    text-align: center;
    font-weight: 400;
    font-size: 12px;
    line-height: 16px;
    color: var(--color-text-tertiary);
    font-style: italic;
  }
  /*
   * Stacked header — logo on top, progress bar right below.
   * Matches Figma frame "Progress Frame": pt 24, pb 16, px 16, gap 20.
   */
  .header--stacked {
    flex-direction: column;
    gap: var(--space-20);
  }
  /* v3: icon-only mark (square 24×24 viewBox) — the "LangArena"
     wordmark was dropped, so the box is square, not 129-wide. */
  .header svg.logo { display: block; height: 24px; width: 24px; }
  .backBtn {
    position: absolute;
    left: var(--space-16);
    top: calc(var(--space-24) - 4px);
    width: 40px;
    height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: 50%;
    color: var(--color-text-primary);
    cursor: pointer;
    transition: background-color 120ms ease;
  }
  .backBtn:hover { background: #f1f2f3; }

  /* ─── Content region ────────────────────────────────────────── */
  /*
   * Baseline content column: 343 (375 − 2×16 padding).
   * Only the language screen expands this width further.
   */
  .content {
    flex: 1 0 auto;
    padding: 0 var(--space-16);
    /*
     * Reserve room so the last element in the scroll doesn't stay
     * hidden under the fixed action overlay (CTA 56 + padding 24×2
     * + the iPhone home-indicator safe area folded into the bar).
     */
    padding-bottom: calc(128px + env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-32);
  }
  .contentInner {
    width: 100%;
    max-width: 343px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-32);
  }
  /* Language screen gets wider content. */
  .screen[data-screen="language"] .contentInner { max-width: 343px; }
  @media (min-width: 425px) {
    .screen[data-screen="language"] .contentInner { max-width: 393px; }
  }
  @media (min-width: 768px) {
    .screen[data-screen="language"] .contentInner { max-width: 720px; }
  }

  /* ─── Action container + CTA ────────────────────────────────── */
  /*
   * Fixed bottom overlay:
   *   - the white background spans the full browser width
   *     (left: 0; right: 0; inset outside the viewport box);
   *   - the CTA itself stays inside the 375 baseline container
   *     (max-width: 343) and is centered horizontally;
   *   - sits above the scrolling content via z-index;
   *   - the content reserves padding-bottom so the last element is
   *     never fully hidden underneath.
   */
  .actionContainer {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 10;
    background: var(--color-background-primary);
    /* Bottom padding folds in `safe-area-inset-bottom` so the
       action surface sits above the iPhone home-indicator without
       eating into the visual padding. */
    padding: var(--space-24) var(--space-16) calc(var(--space-24) + env(safe-area-inset-bottom, 0px));
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    /*
     * Background "skirt" that extends 200 px below the action bar.
     * On iOS 26 Chrome (and other browsers with a dynamic bottom URL
     * bar) the bar collapses on scroll — exposing extra viewport
     * height below `position: fixed; bottom: 0`.  Page content
     * scrolling underneath then peeks through that gap because the
     * action surface ends at the bar's bottom edge.
     *
     * `box-shadow: 0 200px 0 0 <bg>` paints a no-blur, no-spread
     * duplicate of the bar 200 px below it, in the same color, with
     * zero layout impact.  On viewports where the bar truly reaches
     * the bottom edge, the skirt is clipped off-screen (invisible).
     * On viewports where a gap appears, the skirt fills it.
     */
    box-shadow: 0 200px 0 0 var(--color-background-primary);
  }
  /*
   * Soft-keyboard handling on `is-fixed-screen` (non-scroll) screens.
   *
   * Default `position: fixed; bottom: 0` anchors the action bar to
   * the *layout* viewport.  On Mobile Safari, when the soft keyboard
   * opens, the layout viewport is shifted upward to keep the focused
   * input above the keyboard — the fixed bar travels with it and
   * visually "scrolls up" alongside the page (Name / Auth bug).
   *
   * Switching to `position: absolute` while `.app` is locked to
   * `100dvh` (see the `is-fixed-screen` block above) anchors the
   * bar to the *visual* viewport instead — `100dvh` dynamically
   * tracks the on-screen window above the keyboard, so the bar
   * stays glued to the bottom of the visible area regardless of
   * keyboard state.
   *
   * Anchor element is `.app` (not `.screen.active` or `.viewport`):
   * `.app` spans the entire browser width while the latter two are
   * constrained by `.viewport { max-width: 375px }`.  Anchoring to
   * `.app` lets the action bar cover the full browser width on
   * desktop emulation while preserving the keyboard fix.
   */
  html.is-fixed-screen .actionContainer {
    position: absolute;
  }
  /*
   * Full-viewport breakout for screens where the section itself is
   * `position: relative` (Ratings — absolute-pinned stat module;
   * Auth — confetti overlay).  In those cases the absolute action
   * bar anchors to the 375-px screen rather than to the full-width
   * `.app`, so the white background visually clips at the viewport
   * boundary on desktop.  Span 100vw via left:50% + negative
   * 50vw margin to escape the narrower containing block.  The CTA
   * inside still keeps its `max-width: 343px` cap, so only the
   * white surface widens — the button geometry is unchanged.
   */
  html.is-fixed-screen .screen[data-screen="ratings"] .actionContainer,
  html.is-fixed-screen .screen[data-screen="auth"]    .actionContainer {
    width: 100vw;
    left: 50%;
    right: auto;
    margin-left: -50vw;
  }
  .cta {
    width: 100%;
    max-width: 343px;
    height: 56px;
    padding: var(--space-16) var(--space-24);
    border: none;
    border-radius: var(--radius-medium);
    background: var(--color-background-brand);
    color: var(--color-text-on-accent);
    font-family: inherit;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    cursor: pointer;
    transition:
      background-color 250ms ease,
      color            250ms ease,
      var(--press-transition-cta);
    transform-origin: center center;
  }
  .cta:hover:not(:disabled) { background: #4f4dc7; }
  /*
   * Pressed feedback — brief scale-down, slightly more visible than
   * before but still quick. Native-feeling, no elastic / overshoot.
   */
  .cta:active:not(:disabled) { transform: scale(var(--press-scale-cta)); }
  /*
   * Disabled — Figma design system:
   *   background / fill : Ring/primary    (rgba(58, 67, 82, 0.15))
   *   label color       : Text/tertiary   (rgba(134, 149, 170, 0.6))
   * Both tokens already carry alpha, so we use them directly rather
   * than layering opacity on top of brand.
   */
  .cta:disabled {
    background: var(--color-ring-primary);
    color: var(--color-text-tertiary);
    cursor: not-allowed;
    pointer-events: none;
  }

  /*
   * Loading state — used while an in-flight network call is pending
   * (e.g. the auth screen's bootstrap call before advancing to
   * finalTouches).  Keeps the brand fill (so the user can see the
   * button is still active), hides the label, and overlays a
   * centered spinner so it's obvious something is happening.
   */
  .cta--loading,
  .cta--loading:disabled {
    position: relative;
    background: var(--color-background-brand);
    color: transparent;
    cursor: wait;
    pointer-events: none;
  }
  .cta--loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 22px;
    height: 22px;
    margin: -11px 0 0 -11px;
    border-radius: 50%;
    border: 3px solid rgba(255, 255, 255, 0.4);
    border-top-color: var(--color-text-on-accent);
    animation: cta-spin 0.8s linear infinite;
  }
  @keyframes cta-spin {
    to { transform: rotate(360deg); }
  }

  /* ─── Screens — only one visible at a time ──────────────────── */
  .screen { display: none; width: 100%; flex: 1 0 auto; flex-direction: column; }
  .screen.active { display: flex; }
  /*
   * Belt-and-suspenders rule: an `.actionContainer` is fixed-positioned
   * (relative to the viewport) and always has `display: flex` on its
   * own rule.  In CSS, `display: none` on an ancestor still removes
   * fixed descendants from the rendering tree — but only if no later
   * rule re-asserts a visible display value on a more-specific selector.
   * Earlier iterations of the Payment screen accidentally set
   * `display: flex` on `.screen[data-screen="payment"]` directly, which
   * (because (0,2,0) ≥ (0,1,0)) overrode `.screen { display: none }`
   * and left the section permanently visible — the Confirm Payment
   * footer then hovered at the bottom of every other screen.
   *
   * The override has been removed, but we lock the contract here so
   * any future single-screen styling that re-introduces a similar
   * mistake cannot leak its action footer across screens.  Specificity
   * (0,3,0) beats `.actionContainer` and `.actionContainer__row`
   * defaults (0,1,0) without needing !important.
   */
  .screen:not(.active) .actionContainer,
  .screen:not(.active) .personaActions {
    display: none;
  }


  /* ─── v3 · Welcome social proof ──────────────────────────────
     Both stats share ONE row inside a single pair of laurel
     branches; the ratings disclaimer sits beneath (same caption
     treatment as the auth screen). */
  .welcomeStats {
    margin: 24px auto 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
  }
  .welcomeStats__olive {
    width: 24px;
    height: 54px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
    opacity: 0.65;
  }
  .welcomeStats__items {
    display: flex;
    /* Top-align so the two values stay on the same line even when one
       label wraps to a second line (e.g. long localized labels). */
    align-items: flex-start;
    gap: 28px;
  }
  .welcomeStats__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .welcomeStats__value {
    margin: 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 20px;
    /* Muted on purpose — supporting evidence, not the screen's
       message. Primary-dark 18px pulled focus from the chart + CTA. */
    color: var(--color-text-secondary);
    white-space: nowrap;
  }
  .welcomeStats__label {
    margin: 0;
    font-weight: 600;
    font-size: 13px;
    line-height: 16px;
    color: var(--color-text-secondary);
    /* Allow long localized labels (e.g. German "durchschnittliche
       Bewertung") to wrap to a second line rather than forcing the row
       wide and crowding the laurel branches. Short labels stay 1 line. */
    max-width: 130px;
  }
  .welcomeStats__disclaimer {
    margin: 10px auto 0;
    text-align: center;
    font-weight: 500;
    font-size: 11px;
    line-height: 14px;
    color: var(--color-text-ghost, #9aa3b5);
  }
/*
 * preview/css/03-intro-screens.css
 * Welcome + Language pick + Name input screens.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L650–L1425.
 */

  /* ─── Welcome ───────────────────────────────────────────────── */
  .titleBlock {
    display: flex;
    flex-direction: column;
    gap: var(--space-20);
    padding: 0 var(--space-12);
    width: 100%;
  }
  .heading {
    margin: 0;
    text-align: center;
    color: var(--color-text-primary);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
  }
  .subtitle {
    /* Same dark token as the heading — Welcome's two text lines must
       read as one continuous block by colour, not split into a
       primary + faded subtitle. */
    margin: 0;
    text-align: center;
    color: var(--color-text-primary);
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
  }
  .badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-8);
    padding: var(--space-8) var(--space-16) var(--space-8) var(--space-8);
    border-radius: var(--radius-medium);
    background: var(--color-background-secondary);
    border: 1px solid var(--color-border-primary);
  }
  .badge svg { width: 24px; height: 24px; display: block; }
  .badge span {
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    color: var(--color-text-info);
    opacity: 0.8;
  }

  /* Chart */
  .chartWrap { width: 214px; height: 214px; }
  @media (max-width: 768px) {
    .screen[data-screen="welcome"] .chartWrap { width: 180px; height: 180px; }
  }
  .chart { width: 100%; height: 100%; display: block; }
  .arc {
    transform-box: fill-box;
    transform-origin: center;
    transform: rotate(-90deg);
    stroke-dasharray: 0.001 100;
    transition: stroke-dasharray 1100ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  /* Legend */
  .legend {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-12);
    width: 100%;
  }
  .legendItem {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-8);
    opacity: 0;
    transform: translateX(-12px);
    animation: legendFadeInLeft 500ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
  }
  .legendItem:nth-child(1) { animation-delay: 200ms; }
  .legendItem:nth-child(2) { animation-delay: 350ms; }
  .legendItem:nth-child(3) { animation-delay: 500ms; }
  .legendItem:nth-child(4) { animation-delay: 650ms; }
  .dot { width: 12px; height: 12px; border-radius: 50%; }
  .legendLabel {
    font-weight: 600; font-size: 14px; line-height: 16px;
    color: var(--color-text-primary);
  }
  @keyframes legendFadeInLeft {
    to { opacity: 1; transform: translateX(0); }
  }

  /* ─── Language select ───────────────────────────────────────── */
  .langTitle {
    margin: 0;
    text-align: center;
    color: var(--color-text-primary);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    width: 100%;
  }
  .langGrid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-16);
    width: 100%;
  }
  @media (min-width: 768px) {
    .langGrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }

  /*
   * Card — flag above label. The selected state gets a 2px brand
   * border + translucent brand bg + a check_circle badge pinned
   * to the top-right corner.
   */
  .langCard {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-8);
    padding: var(--space-16) var(--space-12);
    border-radius: var(--radius-medium);
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    font-family: inherit;
    cursor: pointer;
    min-height: 96px;
    transform-origin: center center;
    transition:
      background-color 150ms ease,
      border-color 150ms ease,
      var(--press-transition);
    outline: none;
  }
  /*
   * Hover — uses a token-driven 8 % brand tint over the surface so it
   * works in BOTH light and dark themes.  Previously hard-coded to
   * #f7f8ff which collapsed onto white in light mode and was nearly
   * invisible (and blindingly bright) in dark mode.
   */
  /* Hover gated to real pointer devices — see `.option:hover` for
     the touch-tap-sticks-hover rationale. */
  @media (hover: hover) {
    .langCard:hover:not(.langCardSelected) {
      background: color-mix(in srgb, var(--color-ring-inverse) 92%, var(--color-background-brand));
      border-color: var(--color-border-tertiary);
    }
  }
  /* Pressed — tinted brand fill + project-wide press scale. */
  .langCard:active {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
    transform: scale(var(--press-scale));
  }
  .langCardSelected {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    /* Shave 1px so the 2px border doesn't resize the card. */
    padding: calc(var(--space-16) - 1px) calc(var(--space-12) - 1px);
  }

  /*
   * Flag tile — 32×24 with 1px border-primary and 4px radius
   * per the design spec.
   */
  .flag {
    width: 32px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background: var(--color-background-primary);
    border: 1px solid var(--color-border-primary);
    border-radius: var(--radius-xxs);
    flex-shrink: 0;
  }
  /* "Other" (globe) doesn't sit inside a rectangular frame. */
  .flag.flag--noBorder {
    border: none;
    background: transparent;
  }
  .flag img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* Other (globe) ships as a square 512×512 PNG, so cover-cropping it
     into the 32×24 flag frame eats the poles.  Contain keeps it
     centered without the frame; the .flag--noBorder makes the
     surrounding 4 px column transparent. */
  .flag.flag--noBorder img { object-fit: contain; }
  .langLabel {
    font-weight: 600; font-size: 16px; line-height: 24px;
    color: var(--color-text-primary);
    text-align: center;
    white-space: nowrap;
  }

  /* Selected check_circle badge — material-style, pinned top-right. */
  .check {
    position: absolute;
    top: var(--space-8);
    right: var(--space-8);
    width: 24px;
    height: 24px;
    pointer-events: none;
    display: block;
  }

  /* ─── Name screen ───────────────────────────────────────────── */
  /*
   * Progress bar — 4 chapter knobs connected by 4 thin bars.
   * Final state (matches Figma 6003:8214):
   *   - bar 1: fully filled brand
   *   - knob 1: brand circle with a white check (completed)
   *   - bar 2: partially filled (15 px brand inside the secondary track)
   *   - knob 2 / bar 3 / knob 3 / bar 4 / knob 4: pending (secondary)
   *
   * The bars' brand fill lives in a ::after layer so we can animate
   * a transform:scaleX(0) → scaleX(1) without repainting the base
   * color. Base color always stays secondary (the "track").
   */
  .progress {
    display: flex;
    align-items: center;
    width: 100%;
    gap: 0;
  }
  .progress__bar {
    flex: 1 0 0;
    min-width: 0;
    height: 4px;
    background: var(--color-background-secondary);
    position: relative;
    overflow: hidden;
  }
  /* Brand fill layer — scaleX 1 in final state. */
  .progress__bar--filled::after,
  .progress__bar--partial::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    background: var(--color-background-brand);
    transform-origin: left center;
    transform: scaleX(1);
  }
  .progress__bar--filled::after  { right: 0; }
  /*
   * Partial-bar width is driven by a CSS custom property so each
   * screen can pin its own final value from Figma, and the
   * progression animation can interpolate between the previous and
   * current values (see --partial-from).
   *
   * Per-screen widths (measured in Figma):
   *   name                 → 15
   *   didYouKnow           → 23
   *   difficultyAdaptation → 23
   *   updatedWriting       → 25
   *   newSpeaking          → 28
   *   shorterTest          → 30
   *   evaluate             → 34
   *   howWellEnglish       → 43
   */
  .progress__bar--partial::after {
    width: var(--partial-width, 15px);
    /* Soft transition so a per-screen update of the partial-width
       custom property animates the fill instead of snapping — used
       by the dynamic struggle queue to advance the line smoothly
       across the chosen screens without changing the bar's
       geometry. */
    transition: width 600ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  /* Rounded outer ends on the first/last segments, matching Figma. */
  .progress__bar:first-child { border-radius: 2px 0 0 2px; }
  .progress__bar:last-child  { border-radius: 0 2px 2px 0; }

  .progress__knob {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    border-radius: 8px;
    background: var(--color-background-secondary);
    border: 1px solid var(--color-background-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* Completed knob — brand circle with a white check. */
  .progress__knob--done {
    background: var(--color-background-brand);
    border-color: var(--color-background-brand);
    width: 17px;
    height: 17px;
    border-radius: 50%;
  }
  .progress__knob--done svg {
    width: 12px;
    height: 12px;
    display: block;
  }

  /*
   * Entrance animation — one-shot, triggered by adding
   * `.progress--animate` to the root when the user lands on the
   * name screen. Keyframes step from the "all empty" initial state
   * to the final state above; `animation-fill-mode: both` makes
   * sure we sit at the initial state until the delay fires and
   * stay at the final state forever after it ends.
   *
   * Timeline (total ≈ 1.5 s):
   *    150 ms  start filling bar 1   (600 ms)
   *    750 ms  knob 1 flips to brand (250 ms)
   *    900 ms  white check fades in  (200 ms)
   *   1100 ms  start filling bar 2   (400 ms, stops at 15 px)
   */
  .progress--animate .progress__bar--filled::after {
    animation: barFill 600ms cubic-bezier(0.22, 0.61, 0.36, 1) 150ms both;
  }
  .progress--animate .progress__bar--partial::after {
    animation: barFill 400ms cubic-bezier(0.22, 0.61, 0.36, 1) 1100ms both;
  }
  .progress--animate .progress__knob--done {
    animation: knobDone 250ms ease 750ms both;
  }
  .progress--animate .progress__knob--done svg {
    animation: checkFadeIn 200ms ease 900ms both;
  }

  @keyframes barFill {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
  /*
   * "progress--progress" — one-shot progression animation: the partial
   * bar grows from --partial-from to --partial-width while the rest
   * of the bar keeps its finished state carried over from the
   * previous screen. Both custom properties are set by JS before the
   * class is applied so the same rule covers every subsequent screen.
   */
  .progress--progress .progress__bar--partial::after {
    animation: partialProgress 600ms cubic-bezier(0.22, 0.61, 0.36, 1) 200ms both;
  }
  @keyframes partialProgress {
    from { width: var(--partial-from, 15px); }
    to   { width: var(--partial-width, 15px); }
  }
  @keyframes knobDone {
    from {
      background: var(--color-background-secondary);
      border-color: var(--color-background-secondary);
    }
    to {
      background: var(--color-background-brand);
      border-color: var(--color-background-brand);
    }
  }
  @keyframes checkFadeIn {
    from { opacity: 0; transform: scale(0.5); }
    to   { opacity: 1; transform: scale(1); }
  }

  @media (prefers-reduced-motion: reduce) {
    .progress--animate .progress__bar--filled::after,
    .progress--animate .progress__bar--partial::after,
    .progress--animate .progress__knob--done,
    .progress--animate .progress__knob--done svg,
    .progress--progress .progress__bar--partial::after {
      animation: none;
    }
  }

  /* Title on name screen */
  .nameTitle {
    margin: 0;
    text-align: center;
    color: var(--color-text-primary);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    width: 100%;
  }

  /*
   * Input card — 343×72, radius 20, always 16 padding on all sides.
   *
   * States (per design spec):
   *   - default / inactive (empty, blurred):
   *       1px border-primary, white background, icon in secondary color
   *   - focus / active:
   *       2px border-brand, tinted brand background,
   *       icon in primary color; padding shaved by 1px to keep the
   *       card height at 72 and prevent the placeholder from jumping
   *   - filled / blurred:
   *       same as default (border-primary) but icon upgrades to the
   *       primary color because the field now carries real content
   *
   * Native <input> sits inside a <label>, so clicking anywhere on the
   * card focuses the field and the browser handles caret/IME/selection.
   */
  .nameInputCard {
    width: 100%;
    max-width: 343px;
    height: 72px;
    display: flex;
    align-items: center;
    gap: var(--space-16);
    padding: var(--space-16);
    border-radius: var(--radius-medium);
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    cursor: text;
    transition:
      background-color 150ms ease,
      border-color 150ms ease;
  }
  /*
   * The <form> wrappers around the input on the name and auth
   * screens are flex items inside parents with `align-items:
   * center` (`.contentInner` / `.authBody`).  Without an explicit
   * width they would shrink to their child's intrinsic size,
   * narrowing the input card on the screen.  `width: 100%` lets
   * each form fill its column so the input card keeps the same
   * width it had before the form was introduced.
   *
   * The form itself only exists so the mobile soft-keyboard's
   * action button reliably fires `submit` (see the JS submit
   * handlers on the name + auth forms).
   */
  #nameForm,
  #authForm {
    width: 100%;
    display: block;
    margin: 0;
  }

  /* Person icon — always rendered in `text-brand` regardless of
     state (resting / focused / filled).  Mirrors the email-icon
     treatment on the auth screen so the two input cards feel like
     the same component. */
  .nameInputCard svg {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    display: block;
    color: var(--color-text-brand);
  }

  /* Focused → brand border + tinted bg.  Icon colour is already
     `text-brand` from the base rule above, so no override needed. */
  .nameInputCard:focus-within {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    /* 15 = 16 − 1 to offset the +1px added by the 2px border. */
    padding: calc(var(--space-16) - 1px);
  }

  .nameInput {
    flex: 1;
    min-width: 0;
    /* Match line-height exactly so the text baseline sits on the card
       midline and doesn't shift when focus toggles. */
    height: 24px;
    line-height: 24px;
    border: none;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-size: 16px;
    font-weight: 400;
    color: var(--color-text-primary);
    padding: 0;
    margin: 0;
    caret-color: var(--color-background-brand);
  }
  .nameInput::placeholder {
    color: var(--color-text-secondary);
    opacity: 1;
  }
  /* Autofill override — browsers paint yellow/white bg by default. */
  .nameInput:-webkit-autofill {
    -webkit-text-fill-color: var(--color-text-primary);
    -webkit-box-shadow: 0 0 0 1000px transparent inset;
    transition: background-color 5000s ease-in-out 0s;
  }

  /*
   * Name screen uses a flex-grown inner column so Skip can drift
   * down toward the fixed CTA (mirroring Figma's justify-between).
   */
  .screen[data-screen="name"] .contentInner {
    flex: 1;
    min-height: 0;
  }

  /*
   * Spacing between progress bar and title (Figma 6003:8214).
   *
   * Figma measurements:
   *   Header Frame itemSpacing (Progress Frame → Card Container) = 20
   *   Progress Frame padding-bottom = 16
   * Total distance from progress-bar bottom to title top = 16 + 20 = 36.
   *
   * Header already has padding-bottom: 16 (via .header). The extra
   * 20 px is added here as the content's top margin so only the name
   * screen gets this vertical rhythm; other screens are untouched.
   */
  .screen[data-screen="name"] .content {
    margin-top: var(--space-20);
  }
  /*
   * Hero image slot — 160×160 transparent square sitting above the
   * Name title on desktop.  Hidden on mobile via the media query
   * below: a tall soft keyboard would otherwise eat half the screen
   * and push the title + input below the fold.  Skipping it on
   * narrow viewports keeps the form fully visible without sacrificing
   * the welcoming illustration on wider screens.
   */
  .nameHero {
    width: 160px;
    height: 160px;
    margin: 0 auto var(--space-24);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /*
   * Mobile (≤ 768 px viewport):
   *   • Hide the hand entirely — a tall soft keyboard would otherwise
   *     push the title + input pair off-screen.
   *   • Park Skip exactly 48 px below the input instead of floating
   *     it to the bottom of the column (`margin-top: auto`), so it
   *     stays comfortably reachable above the soft keyboard.  The
   *     48 px is split as parent's flex `gap: 32` + child's
   *     `margin-top: 16` → 32 + 16 = 48 px.
   */
  @media (max-width: 768px) {
    .nameHero { display: none; }
    .screen[data-screen="name"] .skip {
      margin-top: var(--space-16);
    }
  }
  .nameHero img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    /* Pivot at the wrist (bottom-center of the hand image) so the
       wave rotation feels anchored — like waving from the wrist,
       not pivoting from the centre of a card. The exact x/y matches
       the red anchor Alexey marked on the asset. */
    transform-origin: 50% 90%;
  }
  /*
   * Friendly hand wave on the Name screen.
   *
   * Single continuous animation (one iteration) packing all three
   * left-right cycles into one timeline.  An earlier version used
   * `iteration-count: 3` over a shorter keyframe set, but at every
   * iteration boundary the hand briefly held at 0° before starting
   * the next cycle — that micro-pause read as a small jitter.
   * Folding everything into one set of keyframes removes the seam
   * entirely, so the hand sweeps continuously through six peaks
   * before settling.
   *
   * Six peaks at ±14° = three full waves; the start (0°→peak left)
   * and end (peak right→0°) get extra padding so the motion eases
   * into and out of rest naturally.
   *
   * Easing: cubic-bezier(0.4, 0, 0.6, 1) — a soft pendulum-like
   * ease-in-out applied per segment, so the hand is slowest at each
   * peak (where it changes direction) and fastest crossing through
   * the centre.  Total duration trimmed from 2400 ms to 1600 ms so
   * the gesture feels livelier without becoming nervous.
   *
   * Kicks off automatically whenever the user lands on the Name
   * screen — `display: none` on inactive screens cancels the
   * animation, and the rule is re-applied on re-entry.
   */
  @keyframes nameHero-wave {
    0%   { transform: rotate(0deg); }
    8%   { transform: rotate(-14deg); }   /* peak left  #1 */
    23%  { transform: rotate(14deg); }    /* peak right #1 */
    38%  { transform: rotate(-14deg); }   /* peak left  #2 */
    53%  { transform: rotate(14deg); }    /* peak right #2 */
    68%  { transform: rotate(-14deg); }   /* peak left  #3 */
    82%  { transform: rotate(14deg); }    /* peak right #3 */
    100% { transform: rotate(0deg); }     /* settle */
  }
  .screen[data-screen="name"].active .nameHero img {
    animation: nameHero-wave 1600ms cubic-bezier(0.4, 0, 0.6, 1) 1;
    animation-fill-mode: both;
  }
  @media (prefers-reduced-motion: reduce) {
    .screen[data-screen="name"].active .nameHero img {
      animation: none;
    }
  }

  /* Skip link — centered, brand color, sits above the fixed CTA. */
  .skip {
    background: transparent;
    border: none;
    padding: var(--space-12) var(--space-16);
    font-family: inherit;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-background-brand);
    cursor: pointer;
    text-align: center;
    align-self: center;
    /* Push Skip down so it floats roughly midway between input
       and the fixed CTA zone, mirroring the Figma layout. */
    margin-top: auto;
  }
  .skip:hover { opacity: 0.85; }

  /*
   * ─── Did You Know screen — Figma 6038:69879 ────────────────
   *
   * Pure vertical stack:
   *   1. logo (in shared .header)
   *   2. funnel progress bar (in shared .header)
   *   3. headline "Did you know TOEFL is changing in 2026?"
   *   4. big rounded image card with exam.webp (225×226, radius 36)
   *   5. ROW of two equal option cards (163.5×180, padding 24,
   *      radius 20, white bg, 1px primary border, gap 16)
   *      - image 84×84 + label "I already know" / "Learn more"
   *
   * The Content frame sits inside the standard 343-wide column
   * (375 minus 16 px of horizontal padding) with gap 48 between the
   * three sections (heading → image → cards), exactly per Figma.
   * Header → heading gap is the standard 20 used elsewhere in the
   * funnel so the logo + bar block doesn't visually crowd the title.
   */
  .screen[data-screen="didYouKnow"] .content {
    margin-top: var(--space-20);
  }
  .dyk {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 48px;
  }
  .dyk__title {
    margin: 0;
    width: 100%;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  /*
   * Hero image card — 180×180 with 36 px corner radius, centred.
   * Soft secondary-bg surface holds the exam.webp asset which is
   * sized via object-fit: contain so the full 3D scene shows.
   *
   * Was 225×226 originally — Alexey reduced to 180×180 to lift the
   * I already know / Learn more cards higher on the screen. The
   * image inside scales via object-fit: contain so the asset stays
   * undistorted at the smaller size, and the surrounding gap-48
   * column rhythm pulls the lower cards up automatically.
   */
  .dyk__hero {
    width: 180px;
    height: 180px;
    border-radius: 36px;
    background: var(--color-background-secondary);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .dyk__hero img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  /* Row of two equal-size option cards, 343 wide total. */
  .dyk__cards {
    width: 100%;
    display: grid;
    /* `minmax(0, 1fr)` clamps each track to the available column —
       without the 0-min, a long unwrappable word would push the
       column wider than its half. With it, content stays inside
       the track and wraps onto a second line instead. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* `auto-rows: 1fr` + `align-items: stretch` (default) keeps both
       cards exactly the same height — if one wraps onto two lines
       its sibling grows to match. */
    grid-auto-rows: 1fr;
    gap: 16px;
  }
  .dykCard {
    /* `min-height` (not `height`) — cards can grow when the label
       wraps to two lines; the auto-row stretch above keeps both at
       the same height regardless of which one is taller. */
    min-height: 180px;
    padding: 24px;
    box-sizing: border-box;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font-family: inherit;
    cursor: pointer;
    transform-origin: center center;
    transition:
      background-color 150ms ease,
      border-color 150ms ease,
      var(--press-transition);
  }
  /* Hover gated to real pointer devices — see `.option:hover` for
     the touch-tap-sticks-hover rationale. */
  @media (hover: hover) {
    .dykCard:hover {
      background: color-mix(in srgb, var(--color-ring-inverse) 92%, var(--color-background-brand));
      border-color: var(--color-border-tertiary);
    }
  }
  .dykCard:active {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
    transform: scale(var(--press-scale));
  }
  /*
   * Card image slot — 84×84 transparent square holding the supplied
   * 3D asset (check.webp / learn-more.webp). object-fit: contain
   * preserves proportions; flex-shrink: 0 keeps the slot from
   * collapsing on narrow viewports.
   */
  .dykCard__icon {
    width: 84px;
    height: 84px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .dykCard__icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  /* Card label — Semibold 16/24, primary text, centred.
     `white-space: nowrap` removed: long translations (e.g. "Узнать
     подробнее") wrap onto a second line instead of pushing the
     card wider than its grid column. `overflow-wrap: anywhere`
     handles the rare unbreakable-token case. */
  .dykCard__label {
    margin: 0;
    flex: 0 0 auto;
    max-width: 100%;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-align: center;
    white-space: normal;
    overflow-wrap: anywhere;
    word-break: break-word;
  }
  /*
   * Per Figma 6038:69879 both option cards share identical chrome —
   * white surface, primary border, primary-text label, no per-card
   * accent colour. The previous .dykCard--success / --brand variants
   * are intentionally not used here to match the design 1:1.
   */

/*
 * preview/css/04-info-screens.css
 * Evaluate graph, How-well options, Lex flow, Word-section complete, Meaning, Game-like format.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L1426–L2840.
 */

  /* ─── Info & evaluate & How-Well — rebuilt from scratch ─────
   *
   * Intentionally use plain block-flow inside a dedicated body
   * element (.sbody). No flex on the container, no gap tricks.
   * Vertical spacing is done with ordinary margin-top on block
   * children, which every browser lays out identically.
   *
   * The body sits between the shared header (logo + progress bar,
   * injected by JS) and the fixed bottom CTA overlay. Padding-top
   * 20 on .sbody gives the 20 px gap from the progress bar; padding
   * bottom 128 reserves room so content doesn't slide under the CTA.
   */
  .sbody {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    padding: 20px 16px 128px;
    box-sizing: border-box;
    flex: 1 0 auto;
    text-align: center;
  }

  /*
   * Image block — 280×280 transparent slot centered horizontally.
   * The repo assets are 3D-rendered PNGs/WebPs with alpha channels so
   * we let them sit directly on the canvas (no card frame), and use
   * `object-fit: contain` to keep the full subject visible without
   * cropping. The slot keeps the same 280×280 footprint Figma uses.
   */
  .sbody__image {
    width: 280px;
    height: 280px;
    margin: 0 auto;
    background: transparent;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* ─── Evaluate-screen graph (Figma 6489:8512) ──────────────────
     Inline SVG rebuild of the curved progression chart. A faint
     dashed grid backs an S-curve where:
       • the brand-coloured `.__trace` path is animated via
         stroke-dashoffset (Trim Paths look),
       • a marker rides the trace's leading edge in sync,
       • the unreached tail is drawn as a faded `.__ghost` path so
         the final composition matches the Figma reference.
     Width 343 px (full content area) — overrides the 280 px default
     of `.sbody__image` because this screen needs the wider artboard.
     ────────────────────────────────────────────────────────────── */
  .evaluateGraph {
    width: 343px;
    height: 280px;
    max-width: 100%;
    margin: 44px auto 0;  /* matches former `style="margin-top: 44px"` on the image */
    position: relative;
  }
  .evaluateGraph__svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
  }
  /* Gradient stops driven from the design-system token. The 3-stop
     gradient gives a smooth falloff from brand-coloured (bottom-left)
     to transparent (top-right) — the same fade present in Figma. */
  .evaluateGraph__stop {
    stop-color: var(--color-text-brand);
  }
  .evaluateGraph__stop--1 { stop-opacity: 0.45; }
  .evaluateGraph__stop--2 { stop-opacity: 0.28; }
  .evaluateGraph__stop--3 { stop-opacity: 0;    }
  .evaluateGraph__grid line {
    stroke: url(#evalDashGradient);
    stroke-width: 1;
    stroke-dasharray: 3 5;
  }
  .evaluateGraph__ghost {
    stroke: var(--color-text-brand);
    stroke-width: 6;
    opacity: 0.18;
  }
  .evaluateGraph__trace {
    stroke: var(--color-text-brand);
    stroke-width: 6;
    /* JS will set stroke-dasharray + stroke-dashoffset on screen entry. */
  }
  .evaluateGraph__halo {
    fill: var(--color-text-brand);
    opacity: 0.18;
  }
  .evaluateGraph__core {
    fill: var(--color-text-brand);
  }
  /* ─── Marker waves ──────────────────────────────────────────────
     3 stroked rings emanating from the marker. Each scales 0.4 → 5.7
     (effective radius 14·5.7 ≈ 80 px) while opacity rides 0 → 0.5 → 0,
     giving a soft ripple. Cycle = 3000 ms; rings start 1000 ms apart
     so by the third onset the trio overlaps continuously — one is
     fading out as the next is mid-phase and a third is ramping in.
     `vector-effect: non-scaling-stroke` keeps the line thickness
     constant as the ring grows.
     Animations only run while the screen is active and start AFTER
     the trace finishes (≈2200 ms) so the marker has settled in.
     ─────────────────────────────────────────────────────────────── */
  .evaluateGraph__wave {
    fill: none;
    stroke: var(--color-text-brand);
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
    transform-box: fill-box;
    transform-origin: center;
    opacity: 0;
  }
  @keyframes evalWaveOut {
    0%   { transform: scale(0.4); opacity: 0;   }
    18%  {                        opacity: 0.5; }
    100% { transform: scale(5.7); opacity: 0;   }
  }
  #app[data-active-step="evaluate"] .evaluateGraph__wave {
    animation: evalWaveOut 3000ms cubic-bezier(0.22, 1, 0.36, 1) infinite;
  }
  #app[data-active-step="evaluate"] .evaluateGraph__wave--1 { animation-delay: 2400ms; }
  #app[data-active-step="evaluate"] .evaluateGraph__wave--2 { animation-delay: 3400ms; }
  #app[data-active-step="evaluate"] .evaluateGraph__wave--3 { animation-delay: 4400ms; }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="evaluate"] .evaluateGraph__wave {
      animation: none;
      opacity: 0;
    }
  }
  /* Scoped 200×200 image override for the marketing/struggle group
     (Difficulty Adaptation, Updated Writing Tasks, New Speaking
     Section, Shorter Test Time, Lack of Practice, Pronunciation,
     Fear of Speaking). The shared default stays 280×280 for all
     other screens (evaluate, goodHands, planEnglishPractice, etc.)
     so we don't accidentally shrink screens that weren't asked
     about. img inside still uses object-fit: contain so the asset
     isn't distorted at the smaller size. */
  .screen[data-screen="difficultyAdaptation"] .sbody__image,
  .screen[data-screen="updatedWriting"]       .sbody__image,
  .screen[data-screen="newSpeaking"]          .sbody__image,
  .screen[data-screen="shorterTest"]          .sbody__image,
  .screen[data-screen="lackOfPractice"]       .sbody__image,
  .screen[data-screen="pronunciation"]        .sbody__image,
  .screen[data-screen="fearOfSpeaking"]       .sbody__image,
  .screen[data-screen="grammarStr"]           .sbody__image,
  .screen[data-screen="listeningStr"]         .sbody__image,
  .screen[data-screen="vocabularyStr"]        .sbody__image {
    width: 200px;
    height: 200px;
  }
  .sbody__image > img,
  .sbody__image > svg {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  /*
   * Title block — fixed 48 px below the image. Horizontal padding
   * 12 inside the 343-wide body (343 − 24 = 319), matching Figma's
   * Title x=28, w=319.
   */
  .sbody__title {
    margin: 48px 0 0;
    padding: 0 12px;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  /* Subtitle — fixed 20 px under the title. */
  .sbody__desc {
    margin: 20px 0 0;
    padding: 0 12px;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
  }

  /* ─── How well do you know English? options list ────────────── */
  /*
   * Heading on How-Well uses the full 343 width (no inner padding 12).
   * 48 px below it comes the options list; cards are 343 × 86.
   */
  .sbody--question .sbody__title {
    padding: 0;
  }
  .options {
    margin-top: 48px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  /*
   * Struggles screen — 24 px gap between the title and the start
   * of the option block.  The shared `.options { margin-top: 48px }`
   * rule above is overridden here only for this screen so the
   * other question screens keep their 48-px rhythm.
   */
  .screen[data-screen="struggles"] .options {
    margin-top: 24px;
  }
  .option {
    width: 100%;
    height: 86px;
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 16px;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    font-family: inherit;
    cursor: pointer;
    transform-origin: center center;
    transition:
      background-color 150ms ease,
      border-color 150ms ease,
      var(--press-transition);
  }
  /*
   * Hover wrapped in `@media (hover: hover)` so the tint only
   * appears for real pointer devices (mouse / trackpad).  Without
   * this guard, mobile browsers (iOS Safari, Android Chrome,
   * Samsung Internet) treat a tap as a hover and the highlight
   * persists after the finger lifts — the next card the user picks
   * then visually looks "also selected" alongside the real
   * selection (reported by a tester on the Struggles screen).
   * Touch UAs skip this rule and rely on `:active` (instant) +
   * `.option--selected` (sticky) for feedback.
   */
  @media (hover: hover) {
    .option:hover:not(.option--selected) {
      background: color-mix(in srgb, var(--color-ring-inverse) 92%, var(--color-background-brand));
      border-color: var(--color-border-tertiary);
    }
  }
  .option:active {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
    transform: scale(var(--press-scale));
  }
  .option--selected {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    padding: 15px;
  }
  /*
   * Mobile: shrink the answer cells on the option-list question
   * screens (whyToefl / struggles / lackOfPractice / pronunciation /
   * fearOfSpeaking / howYourBrainLearns / howWellEnglish /
   * ready3MinTest).  Card height 86 → 72, padding 16 → 12 (selected
   * 15 → 11 to absorb the extra 1 px border), inner image slot
   * 54 → 48 to keep the visual balance.
   */
  @media (max-width: 768px) {
    .option {
      height: 72px;
      padding: 12px;
    }
    .option--selected {
      padding: 11px;
    }
    .option__image {
      width: 48px;
      height: 48px;
    }
  }
  /*
   * Option image slot — 54×54 transparent square holding a 3D PNG /
   * WebP asset.  Background is transparent (no coloured plate) so the
   * 3D asset sits directly on the card's white surface, matching the
   * Figma spec.  Empty when an asset is missing — never substituted.
   */
  .option__image {
    width: 54px;
    height: 54px;
    flex-shrink: 0;
    background: transparent;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .option__image img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  .option__label {
    flex: 1;
    min-width: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-align: left;
  }

  /*
   * Multi-select checkbox — appears on the right edge of every
   * `.option--multi` card. Figma spec:
   *   outer container : 24 × 24
   *   icon area       : 20 × 20
   *   unchecked       : 1 px text-tertiary outline, no fill
   *   checked         : filled brand circle with a white tick
   *
   * Implemented as ::after (24 × 24 flex-container) that box-draws
   * a 20 × 20 circle via a centered inset via box-shadow trick:
   * keep outer width 24 and draw the inner 20 using `background-clip`
   * + `border` layering is fragile, so we use a real pseudo sized
   * 20 × 20 and add a 2 px transparent frame via `border: 2px solid
   * transparent` — this keeps overall 24 × 24 while the visible icon
   * is 20 × 20 inside.
   *
   * Figma note: only the CHECKED state ships as an asset
   * (material check_circle); the unchecked outline is not exported
   * separately, so it's reconstructed in the same material style
   * using tokens the Figma spec calls out (border text-tertiary 1 px).
   */
  .option--multi::after {
    content: "";
    flex-shrink: 0;
    box-sizing: border-box;
    /* 24 × 24 outer container with 2 px transparent border = 20 × 20
       inner icon area, exactly per Figma spec. */
    width: 24px;
    height: 24px;
    border: 2px solid transparent;
    border-radius: 50%;
    background-clip: content-box;
    background-color: transparent;
    /* the visible 20 × 20 ring is drawn with an inset box-shadow
       that hugs the content-box edge */
    box-shadow: inset 0 0 0 1px var(--color-text-tertiary);
    transition:
      background-color 150ms ease,
      box-shadow 150ms ease;
  }
  .option--multi.option--selected::after {
    background-color: var(--color-background-brand);
    box-shadow: inset 0 0 0 2px var(--color-background-brand);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
  }


  /* ─── Lexical Practice flow ─────────────────────────────────────
     Shared visual layer for the 12-screen Lexical-Practice mini-test
     (Plural → Combine 1/2 → NUN 1/2 → Option 1/2/3 → Meaning 1/2 →
      Find Synonym → Lexical Access Score). All screens share:
       • Header: LangArena logo at top, single shared timer below it
       • Title: 24/32 Bold
       • Body: per-screen layout (image grid, slot+plus, list, etc.)
       • Footer: full-width white action bar with Skip / Next
     Timer is driven from JS — starts on first entry to `pluralNoun`
     and runs until the user lands on `lexicalScore`. -- */


  /* HTML overlay labels — positioned at Figma coordinates relative
     to the 343×280 graph box. Each rule sets the precise top/left as
     read from the Figma source. */
  /* Header block (column titles) */
  /* Big numbers (500 / 600) */
  /* "words" labels under the numbers */
  /* "Today" pin under the left marker */
  /* "30 / may" date under the right marker */
  /* `30` sits directly above `may`. `may` shares its baseline with
     the left-column `Today` pin (both at top:243) so the two labels
     read on the same horizontal line under their markers. */
  /* Progress bar — second segment is partial (≈34 px of brand inside
     a wider light track), per Figma. The base `.progress` already has
     all the segment-by-segment treatment; we just hardcode the
     partial-width here so the static screen reads correctly without
     needing the JS-driven `--partial-width` to fire. */

  /* ─── Word section complete (fresh rebuild) ───────────────────
     Single self-contained CSS block.  All rules use the `wsc-`
     prefix so nothing here can collide with anything else. */
  .screen.wsc {
    background: var(--color-background-canvas);
    align-items: center;
    padding: 0;
  }
  .wsc-title {
    margin: 0;
    padding: 0 16px;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
    text-align: center;
  }
  .wsc-stage {
    position: relative;
    width: 343px;
    max-width: 100%;
    height: 280px;
    margin: 48px auto 0;
    flex-shrink: 0;
  }
  .wsc-anim {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  .wsc-anim svg {
    width: 100% !important;
    height: 100% !important;
    display: block;
  }
  .wsc-lbl,
  .wsc-num,
  .wsc-pin,
  .wsc-day,
  .wsc-month {
    position: absolute;
    margin: 0;
    transform: translateX(-50%);
    text-align: center;
    pointer-events: none;
    font-family: var(--font-family-base);
  }
  .wsc-lbl--leftHead,
  .wsc-lbl--rightHead {
    top: 12px;
    width: 146px;
    font-weight: 600;
    font-size: 16px;
    line-height: 20px;
    color: var(--color-text-secondary);
  }
  .wsc-lbl--leftHead  { left: 99px;  }
  .wsc-lbl--rightHead { left: 245px; }
  .wsc-num {
    top: 58px;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-primary);
    white-space: nowrap;
  }
  .wsc-num--left  { left: 99px;   }
  .wsc-num--right { left: 243.5px;}
  .wsc-lbl--leftWords,
  .wsc-lbl--rightWords {
    top: 89px;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
    white-space: nowrap;
  }
  .wsc-lbl--leftWords  { left: 99px;  }
  .wsc-lbl--rightWords { left: 243px; }
  .wsc-pin {
    top: 243px;
    left: 99.5px;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    white-space: nowrap;
  }
  .wsc-day {
    top: 203px;
    left: 243px;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-primary);
    white-space: nowrap;
  }
  .wsc-month {
    top: 243px;
    left: 243.5px;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    white-space: nowrap;
  }

  /*
   * Text-block reveal — `wordSectionComplete` and `grammarTestComplete`.
   *
   * Behaviour (identical on both screens):
   *   • Group A (Today / left side) — `Your estimated vocabulary
   *     | Your estimated level`, the left number, the optional
   *     `words` sub-label, and the `Today` pin — fades in at 1 s.
   *   • Group B (next milestones / right side) — `Next milestones`,
   *     the right number, the optional `words` sub-label, and the
   *     date pair (day + month) — fades in at 3 s.
   *
   * Pure opacity fade — no scale, no slide, no bounce.  Same easing
   * (`ease-out`) and duration (600 ms) for every element so the
   * groups read as one block coming in, not as a sequence.
   *
   * Both `.wsc` screens share the `wsc-*` classes, so a single
   * selector covers both `wordSectionComplete` and
   * `grammarTestComplete`.  Title (`.wsc-title`) and the Lottie
   * graph (`.wsc-anim`) are intentionally NOT in either group —
   * they stay visible from the moment the screen mounts.
   *
   * `animation-fill-mode: both` keeps opacity at 0 throughout the
   * delay (so the reveal feels deliberate) and locks at 1 after.
   * The animation re-runs on every screen entry because removing
   * `.active` and re-adding it re-evaluates the selector match.
   */
  .screen.wsc .wsc-lbl--leftHead,
  .screen.wsc .wsc-lbl--leftWords,
  .screen.wsc .wsc-num--left,
  .screen.wsc .wsc-pin,
  .screen.wsc .wsc-lbl--rightHead,
  .screen.wsc .wsc-lbl--rightWords,
  .screen.wsc .wsc-num--right,
  .screen.wsc .wsc-day,
  .screen.wsc .wsc-month {
    opacity: 0;
  }
  .screen.wsc.active .wsc-lbl--leftHead,
  .screen.wsc.active .wsc-lbl--leftWords,
  .screen.wsc.active .wsc-num--left,
  .screen.wsc.active .wsc-pin {
    animation: wsc-fade-in 600ms ease-out 1000ms both;
  }
  .screen.wsc.active .wsc-lbl--rightHead,
  .screen.wsc.active .wsc-lbl--rightWords,
  .screen.wsc.active .wsc-num--right,
  .screen.wsc.active .wsc-day,
  .screen.wsc.active .wsc-month {
    animation: wsc-fade-in 600ms ease-out 3000ms both;
  }
  @keyframes wsc-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    .screen.wsc.active .wsc-lbl--leftHead,
    .screen.wsc.active .wsc-lbl--leftWords,
    .screen.wsc.active .wsc-num--left,
    .screen.wsc.active .wsc-pin,
    .screen.wsc.active .wsc-lbl--rightHead,
    .screen.wsc.active .wsc-lbl--rightWords,
    .screen.wsc.active .wsc-num--right,
    .screen.wsc.active .wsc-day,
    .screen.wsc.active .wsc-month {
      animation: none;
      opacity: 1;
    }
  }
  .screen.wsc .wsc-partial {
    --partial-width: 34px;
    --partial-from: 34px;
  }

  /* Scoped under `.screen.lex` (specificity 0,2,0) so the base
     `.screen { display: none }` rule keeps inactive lex screens
     hidden — without this, all 11 lex sections render stacked under
     the active screen. `.screen.active { display: flex }` then takes
     over once a step is the current one. flex-direction is inherited
     from `.screen { … flex-direction: column }`. */
  .screen.lex {
    background: var(--color-background-canvas);
    min-height: 100%;
    align-items: center;
    padding: 0;
  }
  .lex__top {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 24px 16px 0;
    box-sizing: border-box;
  }
  .lex__timer {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    font-variant-numeric: tabular-nums;
  }
  .lex__title {
    margin: 16px 0 0;
    width: 100%;
    max-width: 325px;
    text-align: center;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  /* Brand-coloured target word, e.g. `honeymoon` on combine screens
     or the centred lexeme on Meaning / Find Synonym. */
  .lex__targetWord {
    margin: 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-brand);
    text-align: center;
  }
  /*
   * Combine-style screens (`combine` + `combine2`) — typography
   * + spacing override per spec:
   *   - title shrinks from 24/32 to 20/40
   *   - timer → title gap = 0  (override `.lex__title { margin-top: 16 }`)
   *   - title → target word gap = 0  (override `.lex__targetWord { margin-top: 12 }`)
   *   - target word → first body block stays at 32 px (provided by
   *     the unscoped `.lex__body { padding: 32px 16px 24px }` rule)
   * Scoped to these two screens only; other screens that share
   * `.lex__title` / `.lex__targetWord` (nun, meaning, findSynonym)
   * keep their original 24/32 sizing and 16/12 margins. */
  .screen[data-screen="combine"] .lex__title,
  .screen[data-screen="combine2"] .lex__title {
    font-size: 20px;
    line-height: 24px;
    margin-top: 0;
  }
  .screen[data-screen="combine"] .lex__targetWord,
  .screen[data-screen="combine2"] .lex__targetWord {
    margin-top: 0;
  }
  .lex__body {
    width: 100%;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 32px 16px 24px;
    box-sizing: border-box;
    gap: 24px;
  }
  /*
   * Find Synonym screen — the four text-cards (4 × 72 px + 3 × 16 px
   * gap = 336 px) plus the hero image + target word + title can
   * exceed the viewport on shorter devices.  The shared `.lex__body
   * { padding-bottom: 24 }` reserves only 24 px at the bottom, which
   * isn't enough room to clear the fixed action overlay (≈ 104 px:
   * 24 + 56 + 24 + safe-area).  The fourth card was getting hidden
   * behind the Skip bar at maximum scroll.
   *
   * Bumping the bottom padding to the project-wide 128 px reserve
   * (same value `.content` uses) gives the scroll area enough room
   * so that, when the user scrolls all the way down, the fourth
   * card sits comfortably above the action bar instead of underneath
   * it.  Layout is otherwise unchanged — same gap, same grid, same
   * cards.  Scoped to findSynonym so other lex screens that fit in
   * the viewport keep the original 24-px tight bottom.
   */
  .screen[data-screen="findSynonym"] .lex__body {
    padding-bottom: 128px;
  }
  /* — Plural-style image grid (also shared with combine bottom set) — */
  .lex__imgGrid {
    width: 100%;
    max-width: 280px;
    display: grid;
    grid-template-columns: repeat(2, 132px);
    grid-auto-rows: 132px;
    gap: 16px;
    justify-content: center;
  }
  .lex__imgCard {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    background: var(--color-ring-inverse);
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
    padding: 16px;
    box-sizing: border-box;
  }
  .lex__imgCard img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    display: block;
    pointer-events: none;
  }
  /* Cards that show a word PART (no semantic image in repo) — render
     the part as bold brand text on a neutral card. */
  .lex__imgCard--text { font-weight: 700; font-size: 20px; line-height: 24px; color: var(--color-text-primary); }
  .lex__imgCard.is-selected {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
  }
  .lex__check {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 24px;
    height: 24px;
    background-color: var(--color-background-brand);
    border-radius: 50%;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
    display: none;
  }
  .lex__imgCard.is-selected .lex__check { display: block; }

  /* — Combine: 2 slots + plus + 2x2 image cards — */
  .lex__slotRow {
    display: flex;
    align-items: center;
    gap: 16px;
  }
  .lex__slot {
    width: 100px;
    height: 100px;
    border-radius: 20px;
    border: 1px solid var(--color-border-primary);
    background: var(--color-ring-inverse);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 36px;
    line-height: 48px;
    color: var(--color-text-brand);
    overflow: hidden;
  }
  .lex__slot.is-filled {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
    color: var(--color-text-primary);
    /* No padding here — the image keeps its 8-px visual inset via
       its own width/height calc, while .lex__slot__close
       (position: absolute) is anchored to the SLOT's outer corner
       so its top/right coordinates match Figma 1:1 (top: 7 px /
       right: 7 px from the outer edge, not from the padding box).
       Earlier `padding: 8px` here pushed the close inward by 8 px. */
    position: relative;
  }
  /* "Choose the compound word" (nun1 / nun2) ships its slots
     pre-filled at page load — they're informational displays, not
     interactive picks — so they read as a calm white-card pair
     rather than the brand-tinted accent the Combine slots use to
     show selection state.  Fill = ring-inverse, border = border-
     primary, no padding override. */
  .screen[data-screen="nun1"] .lex__slot.is-filled,
  .screen[data-screen="nun2"] .lex__slot.is-filled {
    background: var(--color-ring-inverse);
    border-color: var(--color-border-primary);
  }
  /* Image inset of 8 px on every side simulates the previous
     padding without affecting the close-X containing block.
     `:not(.lex__slot__close)` excludes the close icon — without
     this guard the close <img> would also match and stretch to
     fill the slot (the inset rule has higher specificity than
     the close rule). */
  .lex__slot.is-filled img:not(.lex__slot__close) {
    width: calc(100% - 16px);
    height: calc(100% - 16px);
    object-fit: contain;
    display: block;
    pointer-events: none;
  }
  /* × glyph on a filled slot — uses the repository asset
     `images/close.svg` (24×24 brand-purple X) directly.  The whole
     filled slot is clickable to remove (see slot-level click
     handler in wireCombine); the icon itself is purely visual
     so pointer-events are off. */
  .lex__slot.is-filled { cursor: pointer; }
  .lex__slot__close {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 24px;
    height: 24px;
    display: block;
    pointer-events: none;
  }
  .lex__plus {
    width: 24px;
    height: 24px;
    background-color: var(--color-text-brand);
    -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 4h2v7h7v2h-7v7h-2v-7H4v-2h7z' fill='black'/></svg>") no-repeat center / contain;
            mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 4h2v7h7v2h-7v7h-2v-7H4v-2h7z' fill='black'/></svg>") no-repeat center / contain;
  }

  /* — NUN style: 2 fixed image cards on top + 1-col text-option list — */
  .lex__textList {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0;
  }
  .lex__textCard {
    position: relative;
    width: 100%;
    height: 72px;
    padding: 10px 12px;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    background: var(--color-ring-inverse);
    color: var(--color-text-primary);
    font-family: inherit;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    text-align: center;
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .lex__textCard.is-selected {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
  }
  .lex__textCard__dot {
    position: absolute;
    top: 50%;
    right: 16px;
    transform: translateY(-50%);
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1.5px solid var(--color-border-primary);
    background: transparent;
  }
  .lex__textCard.is-selected .lex__textCard__dot {
    border-color: var(--color-background-brand);
    background-color: var(--color-background-brand);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
  }

  /* — Meaning: thumb-up / thumb-down icon inside the card — */
  .lex__textCard__icon {
    position: absolute;
    top: 50%;
    left: 16px;
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
    border-radius: 50%;
    overflow: hidden;
  }
  .lex__textCard__icon img { width: 100%; height: 100%; object-fit: cover; display: block; }

  /* ─── Meaning 1 / Meaning 2 — taller cards + 72×72 WP icons ──────
   * Scoped strictly to data-screen="meaning1" / "meaning2" so the
   * other lexical screens (combine / nun / option / findSynonym)
   * keep the default 72-px card height. Positive / negative buttons
   * use the dedicated success / danger fill + border tokens so the
   * states read as semantic affirm / reject choices. */
  .screen[data-screen="meaning1"] .lex__textCard,
  .screen[data-screen="meaning2"] .lex__textCard {
    height: 120px;
    /* Symmetric horizontal padding so the label sits in the visual
       centre of the button. The 72-px WP icon and the 24-px right-side
       dot are both absolutely positioned and overlay the padded area
       — short labels ("positive" / "negative") fit comfortably between
       them without colliding. */
    padding: 16px 56px;
  }
  .screen[data-screen="meaning1"] .lex__textCard__icon,
  .screen[data-screen="meaning2"] .lex__textCard__icon {
    width: 72px;
    height: 72px;
    left: 24px;
    border-radius: 0;            /* WP renders are full-bleed shapes */
    overflow: visible;           /* don't clip the artwork */
  }
  .screen[data-screen="meaning1"] .lex__textCard__icon img,
  .screen[data-screen="meaning2"] .lex__textCard__icon img {
    width: 72px;
    height: 72px;
    object-fit: contain;         /* preserve WP proportions */
  }
  /* meaning1 / meaning2 inherit the neutral base `.lex__textCard`
     look — white surface, primary text, neutral grey border — and
     fall back on the brand-purple `.is-selected` rule above for
     the picked state.  Earlier the positive option was tinted
     green and negative was tinted red, but users reported the
     red read as "wrong answer" before they'd even tapped it
     (testers flagged this in mobile QA), so the semantic colour
     pair was removed.  The positive.webp / negative.webp icons
     stay because they read symbolically without leaning on
     correctness-coded colour. */
  /* On phones the meaning cards shrink in height and the inline
     positive / negative icon comes down a notch so the pair sits
     closer to the question. Width stays at the standard 100 % of
     the parent list — these are full-bleed cards. */
  @media (max-width: 768px) {
    .screen[data-screen="meaning1"] .lex__textCard,
    .screen[data-screen="meaning2"] .lex__textCard {
      height: 88px;
      padding: 12px 48px;
    }
    .screen[data-screen="meaning1"] .lex__textCard__icon,
    .screen[data-screen="meaning2"] .lex__textCard__icon {
      width: 56px;
      height: 56px;
      left: 16px;
    }
    .screen[data-screen="meaning1"] .lex__textCard__icon img,
    .screen[data-screen="meaning2"] .lex__textCard__icon img {
      width: 56px;
      height: 56px;
    }
  }

  /* — Option / Synonym hero image (centered, ~140px) — */
  .lex__hero {
    width: 140px;
    height: 140px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .lex__hero img {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  /* — Lexical Access Score donuts — */
  .lex__results {
    display: flex;
    flex-direction: column;
    gap: 32px;
    align-items: center;
    margin-top: 16px;
  }
  .lex__donut {
    position: relative;
    width: 140px;
    height: 140px;
  }
  .lex__donut svg {
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
  }
  .lex__donut__track {
    fill: none;
    stroke: var(--color-background-secondary);
    stroke-width: 12;
  }
  .lex__donut__bar {
    fill: none;
    stroke: var(--color-text-brand);
    stroke-width: 12;
    stroke-linecap: round;
    transition: stroke-dashoffset 800ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .lex__donut__center {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    line-height: 1;
    text-align: center;
  }
  .lex__donut__value {
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-primary);
  }
  .lex__donut__unit {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    margin-top: 2px;
  }

  /* The bottom action overlay reuses the project-wide `.actionContainer`. */

  /* ─── Game-Like Format mini-practice ──────────────────────────
     New branch off ready3MinTest's "Game-like format" button:
       1. selectWords — 2-col grid of words (multi-select)
       2. pluralNoun  — 2×2 image grid (single-select, auto-advance)
       3. fillBlank   — sentence with stable-width blank pill +
                        4 stacked options (single-select, auto-advance)
     Visual treatment follows Figma 6497:8866 / 6497:8554 / 6497:10147
     and reuses the existing token system (ring/brand, border/brand,
     border/primary, etc.).
     ───────────────────────────────────────────────────────────── */

  /* — selectWords — */
  .gameSubtitle {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
    text-align: center;
  }
  .gameWordGrid {
    width: 100%;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    padding: 0 16px;
    box-sizing: border-box;
    margin-top: 32px;
  }
  .gameWord {
    /* Card — 72 px tall; toggleable selected state. */
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    height: 72px;
    padding: 10px 12px;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    background: var(--color-background-primary);
    color: var(--color-text-primary);
    font-family: inherit;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    text-align: left;
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
  }
  /* Right-side check indicator — visually identical to the Struggles
     `.option--multi::after` checkbox. Default = 20×20 thin ring;
     selected = filled brand disc with white check. The element is
     always rendered (no display swap) so the layout never shifts. */
  .gameWord__check {
    flex-shrink: 0;
    box-sizing: border-box;
    width: 24px;
    height: 24px;
    border: 2px solid transparent;
    border-radius: 50%;
    background-clip: content-box;
    background-color: transparent;
    box-shadow: inset 0 0 0 1px var(--color-text-tertiary);
    transition:
      background-color 150ms ease,
      box-shadow 150ms ease;
  }
  .gameWord.is-selected {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
  }
  .gameWord.is-selected .gameWord__check {
    background-color: var(--color-background-brand);
    box-shadow: inset 0 0 0 2px var(--color-background-brand);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
  }

  /* — pluralNoun — */
  .gameNounGrid {
    width: 100%;
    max-width: 280px;
    margin: 32px auto 0;
    display: grid;
    grid-template-columns: repeat(2, 132px);
    grid-auto-rows: 132px;
    gap: 16px;
    justify-content: center;
  }
  .gameNoun {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    background: var(--color-ring-inverse);
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
    padding: 16px;
    box-sizing: border-box;
  }
  .gameNoun img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    display: block;
    pointer-events: none;
  }
  /* Selected-state check badge — Figma 6497:8554 places this 24×24
     icon at left:91.5/top:15 inside the 132×132 card, i.e. ≈14 px
     from the top-right corner. Filled brand-purple disc with white
     check matches the imported imgCheckCircle asset. */
  .gameNoun__check {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 24px;
    height: 24px;
    background-color: var(--color-background-brand);
    border-radius: 50%;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
    display: none;
    box-shadow: 0 0 0 2px var(--color-ring-brand);  /* subtle halo so the badge reads against the card's tinted bg */
  }
  .gameNoun.is-selected {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
  }
  .gameNoun.is-selected .gameNoun__check {
    display: block;
  }

  /* — fillBlank — */
  .gameSentence {
    width: 100%;
    padding: 0 24px;
    box-sizing: border-box;
    text-align: center;
    font-weight: 600;
    font-size: 20px;
    line-height: 40px;
    color: var(--color-text-primary);
  }
  /* Inline pill that stands in for the blank. Width is set by JS at
     render time to match the LONGEST option label so the geometry
     never hints at the right answer. */
  .gameBlank {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 40px;
    padding: 0 16px;
    margin: 0 4px;
    border: 1px solid var(--color-border-brand);
    border-radius: 16px;
    background: var(--color-ring-brand);
    color: var(--color-text-primary);
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    box-sizing: border-box;
    vertical-align: middle;
    /* `--blank-w` is set by JS once we know the longest answer's
       rendered width; default keeps it visible while JS catches up. */
    min-width: var(--blank-w, 140px);
  }
  .gameFillList {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 16px;
    box-sizing: border-box;
    margin-top: 32px;
  }
  .gameFill {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 72px;
    padding: 10px 12px;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    background: var(--color-ring-inverse);
    color: var(--color-text-primary);
    font-family: inherit;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
  }
  /* Twin check-circle markers on each end of the row, per Figma. The
     left marker mirrors the right (decorative); both share styling. */
  .gameFill__dot {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1.5px solid var(--color-border-primary);
    background: transparent;
  }
  .gameFill__dot--left  { left: 16px;  }
  .gameFill__dot--right { right: 16px; }
  .gameFill.is-selected {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
  }
  .gameFill.is-selected .gameFill__dot {
    border-color: var(--color-background-brand);
    background-color: var(--color-background-brand);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M7 12.5 L10.3 15.5 L17 8.5' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-size: 14px 14px;
    background-position: center;
    background-repeat: no-repeat;
  }

  /* — shared screen padding for the three game screens — */
  .screen[data-screen="selectWords"],
  .screen[data-screen="pluralNoun"],
  .screen[data-screen="fillBlank"] {
    padding-bottom: 0;
  }
  /*
   * Mobile gap tightening — the vocab word grid (selectWords*) and
   * the stacked fillBlank* answer list both ship with 16 px gaps
   * between cards, which pushes the last row below the visible
   * viewport on common phone heights (375×667, 360×640).  Halve the
   * gap on screens narrower than the tablet breakpoint so the full
   * set of options stays visible without scroll, while desktop and
   * iPad widths keep the original breathing room.
   */
  @media (max-width: 768px) {
    /* (.gameWordGrid gap is now 8 px on every viewport — the default
       above matches what mobile used to override here.) */
    /* `.gameFillList` carries a 32 px `margin-top` on top of the
       parent `.gameBody`'s 16 px flex gap (≈48 px total between the
       sentence and the first answer pill).  Halve the margin on
       phones so the answers sit closer to the question and the
       fourth pill stays above the fold on short viewports. */
    .gameFillList { margin-top: 16px; }
    /* Image-grid screens (pluralAn / pluralMuch grammar Qs and the
       pluralNoun lex Q) — `.gameNounGrid` carries a 32 px top
       margin.  Halve it so the 2×2 image grid sits closer to the
       title and the fourth card stays above the fold. */
    .gameNounGrid { margin-top: 16px; gap: 8px; }
    /* Lex test screens (combine, nun1/nun2, option1-4) — `.lex__body`
       has 32 px top padding + 24 px internal gap between the hero
       image and the answer list.  Tighten both on phones so the
       answer cards sit higher and the last card stays visible
       without scrolling on short viewports. */
    .lex__body { padding-top: 16px; gap: 16px; }
    /* Inner answer-grid / answer-list gaps on the lex screens —
       `.lex__imgGrid` (combine and its variants) and `.lex__textList`
       (option1-4, nun, meaning, findSynonym) both ship with 16 px
       gaps between cards.  Halve them on phones to recover ~24 px
       across a 4-card list and a comparable amount across the 2×2
       image grid. */
    .lex__imgGrid { gap: 8px; }
    /* (.lex__textList gap is now 8 px on every viewport — the default
       above matches what mobile used to override here.) */
    /* Shrink image cells on the picture-heavy test screens so two
       rows fit comfortably above the fixed action bar on short
       viewports. Cards go from 132 → 110, inner image from 100 → 80.
       Affects pluralAn / pluralMuch / pluralNoun (`.gameNoun*`) and
       combine (`.lex__imgGrid` / `.lex__imgCard`). The 2×2 grid's
       horizontal max-width still allows this since columns are
       explicit, not auto-fit. */
    .gameNounGrid {
      grid-template-columns: repeat(2, 110px);
      grid-auto-rows: 110px;
    }
    .gameNoun img { width: 80px; height: 80px; }
    .lex__imgGrid {
      grid-template-columns: repeat(2, 110px);
      grid-auto-rows: 110px;
    }
    .lex__imgCard img { width: 80px; height: 80px; }
    /* Combine / nun puzzle slots at the top — same proportional
       trim (100 → 84). The filled-slot image uses
       `calc(100% - 16px)` so it auto-scales with the new size; the
       close X stays in the slot's top-right corner via its absolute
       offsets. */
    .lex__slot { width: 84px; height: 84px; font-size: 28px; line-height: 36px; }
    /* Centred hero image on option1-4, findSynonym, meaning1/2 —
       140 → 112 keeps the same proportional trim as the noun cards
       above and clears another ~28 px above the answer list on
       short viewports. Inner <img> uses width/height: 100%, so it
       scales with the container. */
    .lex__hero { width: 112px; height: 112px; }
    /* On findSynonym specifically, the target word ("happy") sits
       directly under the hero image.  Pull it tight against the
       image via a negative margin that cancels half of the parent's
       16 px flex gap — the word now reads as a caption, not a
       separate row, and recovers another ~8 px above the answer
       list. */
    .screen[data-screen="findSynonym"] .lex__targetWord { margin-top: -8px; }
    /* Language-pick screen — the 2-column grid of flag cards uses a
       16 px gap, which adds up to ~80 px of dead space across the
       10+ rows of options. Halve it on phones so more of the list
       sits above the fold without scrolling. */
    .langGrid { gap: 8px; }
    /* Ratings screen — three olive-branch stat rows (4.9★, 60M+,
       etc.) stack vertically and overflow the viewport on shorter
       phones, pushing the disclaimer behind the fixed action bar.
       Shrink the decorative olives and step the value / label
       typography down a tier so the full composition fits without
       scrolling. ~27 px saved per row × 3 rows ≈ 80 px reclaimed. */
    .screen .ratings__olive { width: 44px; height: 102px; }
    .screen .ratings__statValue { font-size: 26px; line-height: 32px; }
    .screen .ratings__statLabel { font-size: 20px; line-height: 26px; }
    /* Did-you-know screen — title + 180-px hero + two 180-px-tall
       cards stack vertically with a 48-px gap between sections.
       Trim the gap, hero box and card icons / min-height on phones
       so the entire question-card composition sits above the fixed
       action bar without scrolling. */
    .dyk { gap: 24px; }
    .dyk__hero { width: 140px; height: 140px; border-radius: 28px; }
    .dykCard { min-height: 140px; padding: 16px; }
    .dykCard__icon { width: 64px; height: 64px; }
    /* Hero illustration on the structurally-identical reveal /
       struggle screens that desktop already steps down to 200×200
       (see the unscoped rule above). Phones get one more notch
       (160×160) so the surrounding title + body copy fits without
       scrolling.  Covers all 10 screens that share the 200-px
       desktop sizing — the four "what's new" reveals plus the six
       struggle / sub-skill explainers. */
    .screen[data-screen="difficultyAdaptation"] .sbody__image,
    .screen[data-screen="updatedWriting"]       .sbody__image,
    .screen[data-screen="newSpeaking"]          .sbody__image,
    .screen[data-screen="shorterTest"]          .sbody__image,
    .screen[data-screen="lackOfPractice"]       .sbody__image,
    .screen[data-screen="pronunciation"]        .sbody__image,
    .screen[data-screen="fearOfSpeaking"]       .sbody__image,
    .screen[data-screen="grammarStr"]           .sbody__image,
    .screen[data-screen="listeningStr"]         .sbody__image,
    .screen[data-screen="vocabularyStr"]        .sbody__image {
      width: 160px;
      height: 160px;
    }
    /* `.options` is the shared column container for the funnel's
       single/multi-select screens — howWellEnglish, whyToefl,
       struggles, planEnglishPractice, ready3MinTest.  Two axes
       tightened on phones:
         - `gap` (between option cards) 16 → 8 so 3-6 stacked rows
           fit comfortably above the fold.
         - `margin-top` (gap from the title above) 48 → 16 so the
           question and its first answer sit closer together.
       Applied uniformly so the rhythm matches across screens. */
    .options { gap: 8px; margin-top: 16px; }
    /* Image slot on the "How your brain learns" screen — desktop
       reserves 180 px tall, which along with the overlapping label
       row and the big "2x" stat pushes the spaced-repetition
       caption below the fold on shorter phones. Trim to 140 px;
       the `img` inside uses `object-fit: contain` so the chart
       scales without distortion. */
    .screen .brain__image { height: 160px; }
    /* Sibling layout on languageExercises — image-on-the-left
       (`.lang2__image`) sized 156×232 on desktop pairs with stats
       on the right. Shrink the image and the row's top margin so
       the trailing stats row stays visible. */
    .screen .lang2__image { width: 140px; height: 208px; }
    .screen .lang2 { margin-top: 24px; }
    /* Offer screen — gift-box image (`.offerVisual`) is 200×200 on
       desktop; the surrounding confetti animation (`.offerVisual__anim`)
       overflows it at 285×285. Shrink both proportionally on phones
       so the title, image, "50% off" unlock and badge fit above the
       fixed CTA without scrolling. */
    .screen .offerVisual { width: 160px; height: 160px; }
    .screen .offerVisual__anim { width: 228px; height: 228px; }
    /* (`.tbody__options` gap is now 8 px on every viewport — the
       desktop default in 05-toefl-tests.css matches what mobile used
       to override here, so no per-viewport rule is needed.) */
    /* You-are-in-good-hands screen — the composition is built from
       absolutely-positioned children (experts photo, caption, sign,
       cards, decorative venn circles) inside a fixed 480-px-tall
       `.goodHands` stage.  Touching individual top/width values
       would unravel the choreography, so scale the whole stage on
       phones via a single transform and claw back the visual
       overflow with a negative bottom margin — title above and the
       fixed action bar below stay in place. */
    .screen .goodHands {
      transform: scale(0.85);
      transform-origin: top center;
      margin-bottom: -72px;
    }
  }
  .gameTitle {
    margin: 0;
    text-align: center;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
    padding: 0 24px;
  }
  .gameBody {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding-bottom: 120px;     /* breathing room above the fixed action overlay */
  }
  /* Plural-screen-only override — vertical rhythm per spec:
     timer (above) → title = 16 px,
     title → noun grid       = 24 px.
     `padding-top` of `gameBody` is the gap from the timer below
     `.lex__top` to the title; `gap` is the spacing between title
     and the grid below it.  Other game-format screens are not
     affected (they use the unscoped `.gameBody` defaults). */
  .screen[data-screen="pluralNoun"] .gameBody {
    padding-top: 16px;
    gap: 24px;
  }

/*
 * preview/css/05-toefl-tests.css
 * Language-Exercises bridge + TOEFL test screens (Reading/Listening/Speaking/Writing).
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L2841–L4019.
 */

  /* ─── Language learning exercises your brain (6003:9557) ────── */
  /*
   * Title at top, below it — a horizontal 2-column block (Image |
   * Stats) with gap 32, below that — a small "Source" footer.
   * CTA lives in the fixed action container like every other screen.
   */
  .lang2 {
    display: flex;
    gap: 32px;
    align-items: stretch;
    margin-top: 48px;
    width: 100%;
  }
  /*
   * Image slot.  Transparent background so a PNG with alpha (e.g.
   * brain-bulb.png) sits cleanly on the canvas instead of inside a
   * grey card.  `object-fit: contain` keeps the bulb fully visible
   * without clipping.
   */
  .lang2__image {
    width: 156px;
    height: 232px;
    background: transparent;
    flex-shrink: 0;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .lang2__image > img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .lang2__stats {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 24px;
    justify-content: center;
    text-align: left;
  }
  .lang2__stat-label {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
  }
  .lang2__stat-value {
    margin: 4px 0 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-primary);
    letter-spacing: 0;
  }

  /* ─── TOEFL skill-test screens (Reading / Listening / Speaking)
   *
   * These screens use a DIFFERENT progress bar than the funnel — per
   * Figma 6003:13593. Row layout:
   *   [ "N of 5" label ][ progress bar ][ Difficulty badge ]
   *
   * The progress bar itself (218 × 16) is a knob + 4 segments:
   *   knob (16 × 16, brand)   — current question marker
   *   4 bars (47 × 4 each)    — remaining questions
   * Knob sits at the filled end; each completed question shifts the
   * knob one segment to the right.
   */
  .header--test {
    padding-bottom: 16px;
    flex-direction: column;
    gap: var(--space-20);
  }
  /*
   * Top-row layout — flexible bar, fixed 20-px gaps on both sides:
   *
   *      [ x of 6 ]──20px──[ ──── progress bar ──── ]──20px──[ EASY ]
   *
   *   - `.testProgress` is a flex row with `gap: 20px`, so the
   *     gap is identical between step↔bar and bar↔badge.
   *   - `.testProgress__step` has no `min-width`; its element
   *     width equals the text width, so the 20-px gap measures
   *     from the END of the counter text.
   *   - `.testProgress__track` is `flex: 1` and grows to fill the
   *     remaining space.  Its width can change slightly when the
   *     badge's text length changes (EASY ↔ MEDIUM ↔ HARD), but
   *     visually that change is hidden by the solid-line style
   *     of the bar (see `.testProgress__track` rule below).
   *   - `.testProgress__badge` lives in flex flow — its natural
   *     pill width determines the right-side anchor of the bar.
   */
  .testProgress {
    display: flex;
    align-items: center;
    gap: 20px;
    width: 100%;
  }
  .testProgress__step {
    flex-shrink: 0;
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    /* Figma: Progress Step uses text-brand on test screens. */
    color: var(--color-text-brand);
  }
  /*
   * Bar visual: SINGLE solid rounded rectangle, not a row of pills.
   * Track itself carries the unfilled secondary tint and rounds
   * the outer corners (`border-radius: 2px` + `overflow: hidden`).
   * Each `.testProgress__seg` is an equal-width zero-radius strip
   * inside the track — adjacent strips butt against each other
   * with no gap, so the bar reads as a continuous line that fills
   * smoothly as segments transition from secondary → brand.
   * Hides the cosmetic effect of bar-width drift when the badge
   * text length changes.
   */
  .testProgress__track {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 0;
    min-width: 0;
    border-radius: 2px;
    overflow: hidden;
  }
  .testProgress__seg {
    flex: 1;                /* equal-width segments */
    height: 4px;
    /* No per-segment rounding — adjacent strips butt against each
       other inside the track and the track itself rounds the
       outer corners (`border-radius: 2px` + `overflow: hidden`),
       so the bar reads as a single continuous line. */
    border-radius: 0;
    background: var(--color-background-secondary);
    /* Smooth fill: each segment animates its background colour
       from secondary → brand when wireToeflProgressAnim adds the
       --filled class on screen entry.  Same easing token used by
       the rest of the project for state transitions. */
    transition: background-color 600ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  .testProgress__seg--filled {
    background: var(--color-background-brand);
  }
  .testProgress__badge {
    /* In flex flow at the row's right edge — natural pill width
       sets where the bar's right edge falls (with a fixed 20 px
       gap from the parent flex `gap`). */
    flex-shrink: 0;
    box-sizing: border-box;
    padding: 4px 8px;
    border-radius: 12px;
    /* Body / small / emphasised — 12/14, 600 weight (per spec). */
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    text-align: center;
    text-transform: uppercase;
    /* Default = EASY pill (success/green @ 20 %).  EASY/MEDIUM/HARD
       variants below override the surface + text colour.  All three
       use Figma's literal palette tints scaled to 20 % alpha so the
       badge reads as a pill, not a saturated chip. */
    background: rgba(52, 199, 89, 0.2);
    color: #248a3d;
  }
  .testProgress__badge--easy {
    background: rgba(52, 199, 89, 0.2);   /* success @ 20 % */
    color: #248a3d;                       /* text-success light */
  }
  .testProgress__badge--medium {
    background: rgba(255, 174, 0, 0.2);   /* orange @ 20 % */
    color: #b07700;                       /* readable amber on the tint */
  }
  .testProgress__badge--hard {
    background: rgba(255, 77, 79, 0.2);   /* danger @ 20 % */
    color: #c83a3c;                       /* darker rose for contrast */
  }

  /* Test body — plain block flow, same padding rules as .sbody. */
  .tbody {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    padding: 20px 16px 128px;
    box-sizing: border-box;
    flex: 1 0 auto;
  }
  /*
   * TOEFL test screens (reading, insertSentence, listening,
   * readingHard, speaking, writing) — Skip CTA now flows inline as
   * the last child of `.tbody` / `.writing` instead of riding a
   * fixed `.actionContainer` overlay (mirrors the Name screen's
   * Skip pattern: brand-coloured text on transparent surface, no
   * pinned background bar).  Drop the 128 px bottom padding the
   * containers reserved for that overlay — the surface below the
   * Skip would otherwise stretch into a blank strip — and make the
   * Skip render as a centred block in both containers (`.tbody` is
   * block, `.writing` is flex column — `display: block; margin auto`
   * works in both since `align-items: center` on the flex parent
   * doesn't fight a block child that has `margin: auto`).
   */
  .screen[data-screen="reading"] .tbody,
  .screen[data-screen="insertSentence"] .tbody,
  .screen[data-screen="listening"] .tbody,
  .screen[data-screen="readingHard"] .tbody,
  .screen[data-screen="speaking"] .tbody,
  .screen[data-screen="writing"]  .writing,
  /* Grammar + vocab game screens — all use inline Skip now,
     including the three selectWords* word-pick screens whose
     "Next" CTA was replaced with the same .skip pattern so the
     vocab and grammar tests read as one consistent flow. */
  .screen[data-screen="selectWords"]  .gameBody,
  .screen[data-screen="selectWords2"] .gameBody,
  .screen[data-screen="selectWords3"] .gameBody,
  .screen[data-screen="fillBlank"]   .gameBody,
  .screen[data-screen="fillBlank2"]  .gameBody,
  .screen[data-screen="fillBlank3"]  .gameBody,
  .screen[data-screen="pluralAn"]    .gameBody,
  .screen[data-screen="pluralMuch"]  .gameBody,
  .screen[data-screen="pluralNoun"]  .gameBody,
  /* Lex test screens — all 11 use `.lex__body` and the same inline
     `[data-lex-skip]` Skip pattern. */
  .screen[data-screen="combine"]      .lex__body,
  .screen[data-screen="combine2"]     .lex__body,
  .screen[data-screen="nun1"]         .lex__body,
  .screen[data-screen="nun2"]         .lex__body,
  .screen[data-screen="option1"]      .lex__body,
  .screen[data-screen="option2"]      .lex__body,
  .screen[data-screen="option3"]      .lex__body,
  .screen[data-screen="option4"]      .lex__body,
  .screen[data-screen="meaning1"]     .lex__body,
  .screen[data-screen="meaning2"]     .lex__body,
  .screen[data-screen="findSynonym"]  .lex__body {
    padding-bottom: 32px;
  }
  .tbody > .skip,
  .writing > .skip,
  .gameBody > .skip,
  .lex__body > .skip {
    display: block;
    /* Pull the Skip button physically away from the answer card
       above and keep the tap target a comfortable 64 px tall.
       Android touch-snap was previously routing taps near Skip to
       the last answer card; the fix combo (this gap + min-height
       + the `if (!card.contains(e.target)) return` guard inside
       .topt's click handler) keeps the snap inside Skip's zone.
       z-index + relative ensures the button sits on top in cases
       where some sibling box-shadow / outline would otherwise
       extend over it. */
    margin: 32px auto 0;
    padding: 20px 24px;
    min-height: 64px;
    position: relative;
    z-index: 1;
  }
  /* Mobile: tighten the top gap from 56 → 20 px so Skip sits
     close to the last answer card on short phone viewports.
     The Android touch-snap hardening still relies on the bigger
     Skip tap area (min-height 64) + the `if (!card.contains
     (e.target)) return` guard inside the .topt click handler, so
     the smaller gap stays safe from the original misroute
     regression. */
  @media (max-width: 768px) {
    .tbody > .skip,
    .writing > .skip,
    .gameBody > .skip,
    .lex__body > .skip {
      margin-top: 20px;
    }
  }
  /*
   * Reading passage card (Figma 6003:13622):
   *   Card Wrapper 343 × 164
   *   - Card: 343 × 148 white, pad 28/20/20/20, radius 20 (y=16)
   *   - "Context" badge: 82 × 32 bg-secondary radius 20 pad 8/16,
   *     overlays top center of card (y=0 → 16 px of overlap)
   */
  .passage {
    position: relative;
    margin-bottom: 16px;
    padding-top: 16px; /* room for the overlapping badge */
  }
  .passage__card {
    background: var(--color-ring-inverse);
    border-radius: 20px;
    padding: 28px 20px 20px;
    min-height: 100px;
  }
  .passage__badge {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    padding: 8px 16px;
    border-radius: 20px;
    background: var(--color-background-secondary);
    color: var(--color-text-primary);
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
  }
  .passage__text {
    margin: 0;
    font-weight: 400;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-primary);
  }
  .passage__text--italic {
    font-style: italic;
  }

  /* — Insert-sentence paragraph block.  Figma 6571:12759 family.
       Single text-flow container: every word is its own
       inline-block <span> so the line-wrap algorithm can break
       between any two words even when an A/B/C/D marker sits
       directly after the previous sentence's closing word.  Line
       height is locked at 40 px so the 40×40 markers sit cleanly
       in the same vertical rhythm as the surrounding text. */
  .insertParagraph {
    margin-top: 16px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    padding: 16px 20px;
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 14px;
    line-height: 40px;
    color: var(--color-text-primary);
  }
  .insertWord {
    display: inline-block;
    vertical-align: middle;
  }
  .insertMark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin: 0 4px;
    vertical-align: middle;
    border-radius: 50%;
    border: 0;
    background: var(--color-ring-brand);
    color: var(--color-text-brand);
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 0;
    transition: var(--press-transition), background-color 150ms ease;
  }
  .insertMark:hover {
    background: color-mix(in srgb, var(--color-text-brand) 18%, transparent);
  }
  .insertMark:active { transform: scale(var(--press-scale-small)); }
  .insertMark.is-selected {
    background: var(--color-background-brand);
    color: var(--color-text-on-accent);
  }
  /* — Match the upper "Insert this sentence" card visually to the
       lower paragraph card (both = white surface + 1-px border-
       primary stroke + 20-px radius).  Scoped to insertSentence
       only so the Reading/Reading-Hard passage cards stay the
       borderless variant they already use.  `min-height: auto`
       collapses the card to fit the short italic prompt — the
       Reading/Reading-Hard variants keep the 100-px floor for
       longer passage text.  Spec says: text → bottom edge = 20 px. */
  .screen[data-screen="insertSentence"] .passage__card {
    border: 1px solid var(--color-border-primary);
    min-height: auto;
    padding-bottom: 20px;
  }
  /* Also tighten the title margin specifically on this screen so
     the upper / lower blocks read as one stack instead of being
     split by an oversized gap. */
  .screen[data-screen="insertSentence"] .tbody__title {
    margin-top: 8px;
  }

  /* Question title on test screens — Figma: Semibold 20/24. */
  .tbody__title {
    margin: 16px 0 0;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  .tbody__options {
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  /*
   * Answer option — Figma 6003:13633: 343 × 72, pad 16, radius 20.
   * Inside: Option Image 40 × 40 + label (gap 16).
   */
  .topt {
    width: 100%;
    min-height: 72px;
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 10px 12px;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    font-family: inherit;
    cursor: pointer;
    transform-origin: center center;
    transition: background-color 150ms ease, border-color 150ms ease, var(--press-transition);
  }
  /* Hover gated to real pointer devices — see `.option:hover` for
     the touch-tap-sticks-hover rationale. */
  @media (hover: hover) {
    .topt:hover:not(.topt--selected):not(.topt--wrong):not(.topt--correct) {
      background: color-mix(in srgb, var(--color-ring-inverse) 92%, var(--color-background-brand));
      border-color: var(--color-border-tertiary);
    }
  }
  .topt:active {
    background: var(--color-ring-brand);
    border-color: var(--color-border-brand);
    transform: scale(var(--press-scale));
  }
  .topt--selected {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    padding: 9px 11px;
  }
  /*
   * Option image placeholder — 40 × 40 per Figma. Ready for asset
   * replacement: pass a child <img> or <svg> and set src.
   */
  /*
   * TOEFL test option image slot — 40×40 transparent square holding
   * the supplied 3D PNG/WebP asset (e.g. a_letter.webp / b_letter.webp).
   * No coloured plate behind it, per the project rule.
   */
  .topt__image {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    background: transparent;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .topt__image > img,
  .topt__image > svg {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .topt__label {
    flex: 1;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-align: left;
  }

  /* ── Build-a-sentence — tap word chips into the answer row ───────── */
  .buildSentence__hint {
    margin: 4px 0 14px;
    font-weight: 500;
    font-size: 14px;
    line-height: 19px;
    color: var(--color-text-secondary);
  }
  /* Answer row — chips land here in order. Dashed when empty. */
  .buildSentence__answer {
    min-height: 60px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-content: flex-start;
    padding: 12px;
    border: 1.5px dashed var(--color-border-primary);
    border-radius: 16px;
    background: var(--color-background-primary);
  }
  .buildSentence__bank {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;
  }
  .buildChip {
    font-weight: 600;
    font-size: 16px;
    line-height: 22px;
    padding: 10px 16px;
    border-radius: 12px;
    border: 1.5px solid var(--color-border-primary);
    background: var(--color-background-primary);
    color: var(--color-text-primary);
    cursor: pointer;
    transition: transform 80ms ease;
  }
  .buildChip:active { transform: scale(0.96); }
  /* Chips that have been placed in the answer row read as "chosen". */
  .buildSentence__answer .buildChip {
    border-color: var(--color-border-brand);
    background: var(--color-background-secondary);
    color: var(--color-text-brand);
  }

  /* ── Wrong-answer flash ──────────────────────────────────────────
     When an answer is incorrect, the chosen option is briefly tinted
     red (danger token pair) before the insight callout appears. These
     rules follow their `--selected`/`is-selected` counterparts in the
     source so they win the cascade on the same element. */
  .topt--wrong {
    background: var(--color-fill-ring-danger);
    border: 2px solid var(--color-border-danger);
    padding: 9px 11px;
  }
  .topt--wrong .topt__label { color: var(--color-text-danger); }
  .insertMark.is-wrong {
    background: var(--color-border-danger);
    color: var(--color-text-on-accent);
  }
  .buildSentence__answer.is-wrong .buildChip {
    border-color: var(--color-border-danger);
    background: var(--color-fill-ring-danger);
    color: var(--color-text-danger);
  }

  /* ── Right-answer highlight ──────────────────────────────────────
     The correct option is tinted green (success token pair): on a
     correct answer the chosen option; on a wrong answer the actual
     correct option is also surfaced alongside the red one. There is no
     `--color-border-success` token, so the green text colour doubles as
     the border. Defined after the `--wrong`/`is-wrong` rules so they
     win the cascade on the same element. */
  .topt--correct {
    background: var(--color-fill-ring-success);
    border: 2px solid var(--color-text-success);
    padding: 9px 11px;
  }
  .topt--correct .topt__label { color: var(--color-text-success); }
  .insertMark.is-correct {
    background: var(--color-text-success);
    color: var(--color-text-on-accent);
  }
  .buildSentence__answer.is-correct .buildChip {
    border-color: var(--color-text-success);
    background: var(--color-fill-ring-success);
    color: var(--color-text-success);
  }
  .buildSentence__submit {
    margin-top: 20px;
  }
  .buildSentence__submit:disabled {
    opacity: 0.45;
    pointer-events: none;
  }

  /*
   * Listening audio player (Figma 6003:13808):
   *   Row   : 343 × 72 white card radius 20, pad 16
   *   Play  : 40 × 40 brand circle, material play_arrow icon inside
   *   Progress bar: thin 4px track, filled/empty with a draggable knob
   *                 (16 × 16 brand circle) at the current position
   *   Time  : "elapsed/total" like "0:00/0:11", text-primary 12
   */
  .audioRow {
    width: 100%;
    height: 72px;
    padding: 16px;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
  }
  /* The hidden `<audio>` source element inside .audioRow has no
     `controls`, but iOS Safari reserves ~25 px of vertical space
     for it anyway as part of its default UA stylesheet. That
     pushed the visible play-bar lower than the equivalent first
     element on the other test screens. Explicit `display: none`
     drops the reservation without affecting JS playback
     (audio.play() / .pause() still work on a detached-from-flow
     element). */
  .audioRow audio { display: none; }
  .audioRow__playBtn {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-background-brand);
    color: #fff;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: 0;
  }
  .audioRow__playBtn svg {
    width: 24px;
    height: 24px;
    display: block;
  }
  /* Progress bar with knob — matches funnel progress visual logic. */
  .audioRow__bar {
    flex: 1;
    position: relative;
    height: 16px;
    display: flex;
    align-items: center;
  }
  .audioRow__barFilled {
    height: 4px;
    background: var(--color-background-brand);
    border-radius: 2px 0 0 2px;
  }
  .audioRow__barEmpty {
    flex: 1;
    height: 4px;
    background: var(--color-background-secondary);
    border-radius: 0 2px 2px 0;
  }
  .audioRow__knob {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--color-background-brand);
    flex-shrink: 0;
    margin: 0 -8px; /* sit on the seam between filled / empty */
    position: relative;
    z-index: 1;
  }
  .audioRow__time {
    font-size: 12px;
    line-height: 14px;
    color: var(--color-text-primary);
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
  }

  /*
   * Accordion — Listening transcript.
   * Closed: 343 × 64 (header only). Open: 343 × 220 (header + body).
   * Animation: max-height + opacity, quick and clean.
   */
  .accordion {
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    padding: 20px;
    margin-bottom: 16px;
  }
  .accordion__head {
    background: transparent;
    border: none;
    padding: 0;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    cursor: pointer;
    font-family: inherit;
    color: var(--color-text-primary);
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
  }
  .accordion__chev {
    width: 24px;
    height: 24px;
    transition: transform 200ms ease;
  }
  .accordion[data-open="true"] .accordion__chev { transform: rotate(180deg); }
  .accordion__body {
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    transition: max-height 250ms ease, opacity 150ms ease, margin-top 250ms ease;
    margin-top: 0;
    font-weight: 400;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
    /* iOS Safari was still hit-testing the transcript paragraphs
       inside the visually-collapsed body (max-height: 0 + overflow:
       hidden), which stole taps from the .topt answer cards and
       the inline Skip button further below — only on the Listening
       screen, since it's the only TOEFL screen with an accordion.
       Disabling pointer-events on the body until it's expanded
       makes the closed state inert for touch routing. */
    pointer-events: none;
  }
  .accordion[data-open="true"] .accordion__body {
    max-height: 400px;
    opacity: 1;
    margin-top: 16px;
    pointer-events: auto;
  }

  /*
   * Dual-button variant of the standard action container.
   * Reproduces Figma node 6215:3782 / 6003:14157 ("Action Container"):
   *   - 375-wide centered container with 16 px horizontal padding
   *     (so inner content row is 343, matching every other screen);
   *   - 24 px top / bottom padding + iOS safe-area inset on bottom;
   *   - HORIZONTAL flex with 20 px gap between buttons;
   *   - both buttons are `flex: 1 0 0` so they share the row evenly
   *     ((343 − 20) / 2 = 161.5 each — exactly the Figma sizes).
   */
  .actionContainer--dual {
    padding: var(--space-24) var(--space-16) calc(var(--space-24) + env(safe-area-inset-bottom, 0px));
  }
  .actionContainer__row {
    width: 100%;
    max-width: 343px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-20);
  }
  .actionContainer--dual .cta {
    flex: 1 0 0;
    min-width: 0;
    max-width: none;
    width: auto;
    padding: var(--space-16) var(--space-24);
    font-size: 16px;
    line-height: 24px;
  }
  /*
   * Ghost button must always read on a single line (Figma never
   * wraps "I can't listen now" / "I can't speak now" / "Skip
   * question").  Content-sized with white-space:nowrap; padding
   * trimmed to 12 px so a long label still fits on a 343-px row
   * without pushing past the safe area.  The primary Next sibling
   * picks up the remaining width and may shrink — but keeps a
   * 96-px min so its label remains readable.
   */
  .actionContainer--dual .cta.cta--ghost {
    flex: 0 0 auto;
    white-space: nowrap;
    padding-left: var(--space-12);
    padding-right: var(--space-12);
  }
  .actionContainer--dual .cta:not(.cta--ghost) {
    flex: 1 1 0;
    min-width: 96px;
  }
  /*
   * Ghost button — no fill, no border, just text-brand label.
   * Used for the secondary "Skip Question" action across every TOEFL
   * test screen (Reading / Listening / Speaking / Writing). Sits next
   * to the primary Next CTA without competing visually.
   */
  .cta--ghost {
    background: transparent !important;
    color: var(--color-text-brand) !important;
    border: none !important;
    box-shadow: none !important;
    font-weight: 600;
  }
  .cta--ghost:hover:not(:disabled) { background: transparent !important; }

  /*
   * Speaking (Figma 6003:14003) composition:
   *   progress header (108)
   *   gap ≈ 102 (Figma y=210 - y=108)
   *   badge "Speak for up to 45 seconds"
   *   gap 40
   *   title
   *   gap 40
   *   mic button 100 × 100
   *   (remaining space)
   *   action container
   */
  .screen[data-screen="speaking"] .tbody {
    padding-top: 102px; /* from Figma: badge y=210, header end y=108 */
  }
  /*
   * Speaking mic (Figma 6003:14084): 100 × 100 brand circle,
   * material mic icon 40 × 40 inside, white.
   */
  .speakingMic {
    margin: 40px auto 0;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: var(--color-background-brand);
    color: #fff;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }
  .speakingMic svg {
    width: 40px;
    height: 40px;
    display: block;
  }
  /* Icon swap — show the mic glyph in the idle state, the square
     stop glyph while recording.  The `.speakingMic` prefix on each
     selector bumps specificity to (0,2,0) so these rules outrank
     the unscoped `.speakingMic svg { display: block }` declaration
     above (which is (0,1,1)). */
  .speakingMic .speakingMic__stop { display: none; }
  .speakingMic.is-recording .speakingMic__idle { display: none; }
  .speakingMic.is-recording .speakingMic__stop { display: block; }
  /* Active-recording state: brand purple → danger red so it reads
     clearly as "recording in progress, tap to stop".  The subtle
     pulsing halo via box-shadow keeps the button feeling alive
     without distracting the user from reading the prompt. */
  .speakingMic.is-recording {
    background: var(--color-text-danger);
    animation: speakingMicPulse 1.2s ease-in-out infinite;
  }
  @keyframes speakingMicPulse {
    0%   { box-shadow: 0 0 0 0   rgba(255, 77, 79, 0.45); }
    70%  { box-shadow: 0 0 0 16px rgba(255, 77, 79, 0);    }
    100% { box-shadow: 0 0 0 0   rgba(255, 77, 79, 0);    }
  }
  /* Big monospace timer under the mic. Tabular-nums so the
     digits don't jitter as the seconds tick. */
  .speakingTimer {
    margin: 16px auto 0;
    text-align: center;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    color: var(--color-text-primary);
    font-variant-numeric: tabular-nums;
  }
  .speakingHint {
    margin: 4px auto 0;
    text-align: center;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
  }
  /*
   * Mic-permission supporting text.  Hidden by default via the
   * `hidden` HTML attribute; `wireSpeakingMic` flips it off when
   * permission is denied.  Uses the design-system danger token so
   * it reads as an error message but stays visually calm — no
   * background, no border, just supporting copy under the mic.
   */
  .speakingError {
    margin: 16px auto 0;
    max-width: 320px;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 500;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-danger);
  }
  .speakingBadge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 8px 16px;
    border-radius: 20px;
    background: var(--color-background-secondary);
    color: var(--color-text-primary);
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    /* Figma gap badge → title = 40 */
    margin: 0 auto 40px;
  }
  .speakingBadgeRow { text-align: center; }
  .speakingTitle {
    margin: 0;
    text-align: center;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    color: var(--color-text-primary);
  }

  /*
   * Writing test screen — fully re-implemented from Figma node
   * 6003:14117 ("Writting") as the absolute source of truth.
   *
   * Frame 375 × 812.  Top container (Progress Container, 6003:14119)
   * is a vertical flex column with gap 20, padding-top 24, padding-x
   * 16, padding-bottom 16, items-center.  Children stack:
   *   1. logo (in test-header-tpl)
   *   2. progress info row (1 of 5 · bar · EASY badge)
   *   3. Context block (card 343 wide + Context badge floating above)
   *   4. Question heading (semibold 20/24, primary, centered, w 343)
   *   5. Response section
   *        a. Textarea card 342×183, brand-ring bg + 2px brand border
   *           when focused, white + 1px primary border at rest
   *        b. Word count meta row (0 words · 10 word minimum |
   *           Target: 30-60 words) — semibold 12/14, ls 0.12,
   *           padding-x 12
   *
   * The Action Container (6003:14157) sits at the bottom of the frame
   * with white bg, padding 24/16, gap 20, and two flex-1 buttons
   * 56 tall ("Skip Question" ghost · "Next" primary).
   */
  .writing {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    padding: 0 16px 128px;
    box-sizing: border-box;
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }

  /* Context block — card with floating "Context" badge above it. */
  .writing__context {
    position: relative;
    width: 343px;
    margin-top: 16px;
  }
  .writing__contextCard {
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    padding: 28px 20px 20px;
    box-sizing: border-box;
  }
  .writing__contextText {
    margin: 0;
    font-weight: 400;
    font-size: 14px;
    line-height: 20px;
    letter-spacing: 0.14px;
    color: var(--color-text-primary);
  }
  .writing__contextBadge {
    position: absolute;
    top: -16px;
    left: 50%;
    transform: translateX(-50%);
    padding: 8px 16px;
    border-radius: 20px;
    background: var(--color-background-secondary);
    border: 1px solid var(--color-border-primary);
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    color: var(--color-text-primary);
    opacity: 0.8;
    white-space: nowrap;
    box-sizing: border-box;
  }

  /* Question heading. */
  .writing__title {
    margin: 0;
    width: 343px;
    text-align: center;
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    color: var(--color-text-primary);
  }

  /*
   * Response section — textarea card + meta row.
   * Card sits at 342×183 by Figma; focus state lifts to brand-ring
   * background + 2 px brand border (padding compensates for the +1
   * border so the card doesn't grow on focus).
   */
  .writing__response {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
  }
  .writing__card {
    width: 342px;
    height: 183px;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    padding: 16px;
    box-sizing: border-box;
    display: block;
    transition: background-color 150ms ease, border-color 150ms ease;
  }
  .writing__card:focus-within {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    padding: 15px;
  }
  .writing__textarea {
    width: 100%;
    height: 100%;
    border: none;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-weight: 400;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    resize: none;
    padding: 0;
    margin: 0;
    display: block;
  }
  .writing__textarea::placeholder {
    color: var(--color-text-secondary);
    opacity: 1;
  }
  .writing__meta {
    width: 343px;
    padding: 0 12px;
    box-sizing: border-box;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.12px;
    color: var(--color-text-secondary);
  }
  .writing__metaLeft {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .writing__metaCount {
    color: var(--color-text-primary);
    font-variant-numeric: tabular-nums;
  }

  /*
   * The Writing screen reuses the project's shared `.actionContainer`
   * + `.actionContainer--dual` for the bottom bar (Skip Question +
   * Next).  Disabled-state styling for the primary CTA also comes
   * from the shared `.cta:disabled` rule (Ring/primary bg + Text/
   * tertiary label) — kept consistent with every other CTA in the
   * funnel.
   */

  /*
   * Writing now sits at 6/6 — wireToeflProgressAnim already drives
   * the rail-fill to 100 % via cur/total, so this old segment-based
   * override is no longer needed.  Block left intentionally empty
   * so adjacent comment ordering doesn't shift; can be removed in
   * a follow-up cleanup. */

  /*
   * Auth screen — "Enter your email" (Figma 6003:14854).
   * No progress bar, no logo in header — just the title, subtitle,
   * and an email input card (same visual family as Name input).
   * Bottom has a small copyright / links row and the fixed CTA.
   */
  .authBody {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    /* Vertical rhythm:
       - 48 px top padding  — top → heading block.
       - 140 px bottom padding — distance from the screen's bottom
         edge to the bottom of the legal `.authFooter`.  Combined
         with `.authFooter { margin-top: auto }`, the footer floats
         to the bottom of the content area (= 140 px above the
         screen edge) regardless of how short the heading + input
         are above it.  Action bar lives outside `.authBody` (as a
         sibling absolute-positioned overlay in fixed-screen mode),
         so it does not collide with the legal block above. */
    padding: 48px 16px 140px;
    box-sizing: border-box;
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .authHead {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    text-align: center;
    width: 100%;
  }
  .authHead__title {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  /* Inline plan id — the title reads as a single line:
       Your plan #78236 is ready
     The middle "#78236" stays inline (no line break) and is
     painted in the brand colour so it visually pops while
     keeping the heading's Bold 24/32 cadence. */
  .authHead__planId {
    /* Green (was brand purple) to echo the "plan ready" banner check. */
    color: var(--color-text-success);
    white-space: nowrap;
  }
  /* Title + check on one row (centered), so the check is a callback to
     the build-plan banner without adding a vertical line above. */
  .authHead__titleRow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
  }
  .authHead__check {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    display: block;
  }
  .authHead__check svg { width: 100%; height: 100%; display: block; }
  /* Springy pop on screen entry (replays each time auth becomes active),
     matching the banner check's overshoot. */
  .screen.active[data-screen="auth"] .authHead__check {
    animation: authCheck-pop 480ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
  @keyframes authCheck-pop {
    0%   { opacity: 0; transform: scale(0.2); }
    60%  { opacity: 1; transform: scale(1.2); }
    100% { opacity: 1; transform: scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .screen.active[data-screen="auth"] .authHead__check { animation: none; }
  }
  .authHead__subtitle {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
  }
  /* Same input card as Name screen.  Earlier this card shipped a
     permanent brand-purple background (active-by-default per Figma),
     but the mobile renders showed that tint as "the input is already
     selected" — confusing on a screen the user hasn't typed in yet.
     Mirror the Name pattern: neutral white surface + grey border by
     default, brand fill + thicker border only on `:focus-within`. */
  .authCard {
    width: 100%;
    max-width: 343px;
    height: 72px;
    /* 32 px from the heading block (title + subtitle) to the top of
       the input card. */
    margin-top: 32px;
    display: flex;
    align-items: center;
    gap: var(--space-16);
    padding: var(--space-16);
    border-radius: var(--radius-medium);
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    cursor: text;
    transition:
      background-color 150ms ease,
      border-color 150ms ease;
  }
  .authCard:focus-within {
    background: var(--color-ring-brand);
    border: 2px solid var(--color-border-brand);
    /* 15 = 16 − 1 to compensate the +1 px the thicker border adds. */
    padding: calc(var(--space-16) - 1px);
  }
  .authCard svg {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    color: var(--color-text-brand);
  }
  .authInput {
    flex: 1;
    min-width: 0;
    height: 24px;
    line-height: 24px;
    border: none;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-size: 16px;
    font-weight: 400;
    color: var(--color-text-primary);
    /* Match `.nameInput` zero-pad/zero-margin so iOS doesn't add its
       own padding that would push the autofill indicator outside the
       card. */
    padding: 0;
    margin: 0;
    caret-color: var(--color-background-brand);
  }
  /* Belt-and-braces focus-ring suppression.  iOS Safari sometimes
     draws a system focus indicator (dashed brand outline) around
     text inputs even when the base style above sets `outline: none`,
     and the card-level `:focus-within` styling we apply doesn't
     stop it.  Force the outline + box-shadow to none in every
     focus state to keep the rendered card matching the design. */
  .authInput:focus,
  .authInput:focus-visible {
    outline: none;
    box-shadow: none;
  }
  .authCard,
  .authCard:focus-within {
    outline: none;
  }
  .authInput::placeholder {
    color: var(--color-text-secondary);
    opacity: 1;
  }
  /* Autofill override — mirror `.nameInput`: a transparent inset
     box-shadow masks the platform's yellow/blue autofill paint
     without forcing a surface colour, so the override survives
     focus / blur swaps and doesn't fight `.authCard`'s own bg. */
  .authInput:-webkit-autofill {
    -webkit-text-fill-color: var(--color-text-primary);
    -webkit-box-shadow: 0 0 0 1000px transparent inset;
    transition: background-color 5000s ease-in-out 0s;
  }
  /*
   * Inline email validation caption.  Hidden by default (`[hidden]`
   * removes it from layout); JS toggles the attribute on submit
   * when the email fails frontend validation, and removes it again
   * the moment the user types into the field.  Same width / max-
   * width as the input card so the caption tucks neatly underneath.
   */
  .authError {
    width: 100%;
    max-width: 343px;
    margin: 8px 0 0;
    padding: 0 12px;
    box-sizing: border-box;
    font-weight: 600;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    color: var(--color-text-danger);
  }
  .authError[hidden] { display: none; }
  /*
   * Social-proof stats under the email field — two olive-wreath stats
   * (rating + users) in one row, matched to the email card's width.
   * Each stat: left olive | value+label | right olive. The fitText
   * utility shrinks the value/label fonts so longer translations stay
   * within the compact two-up columns.
   */
  .authStats {
    width: 100%;
    max-width: 343px;
    margin-top: 24px;
    display: flex;
    /* Stacked (not two-up): long translations didn't fit side by
       side. Each stat is deliberately narrower than the CTA (~65%) —
       this is secondary content and shouldn't compete with the email
       field above it. */
    flex-direction: column;
    align-items: center;
    gap: 12px;
    box-sizing: border-box;
  }
  .authStats__stat {
    width: 65%;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
  }
  .authStats__olive {
    width: 26px;
    height: 60px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
  }
  .authStats__text {
    min-width: 0;
    flex: 0 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    overflow: hidden;
  }
  .authStats__value {
    margin: 0;
    font-weight: 700;
    font-size: 18px;
    line-height: 22px;
    color: var(--color-text-primary);
    white-space: nowrap;
  }
  .authStats__label {
    margin: 0;
    font-weight: 600;
    font-size: 13px;
    line-height: 16px;
    color: var(--color-text-secondary);
  }
  /*
   * `.authCard.is-invalid` — red ring + danger-tinted bg so the
   * field itself reads as "this needs fixing".  Mirrors the brand-
   * focus treatment but in the danger token family.  Applied by
   * JS together with `.authError` reveal; cleared on next keystroke.
   */
  .authCard.is-invalid {
    background: var(--color-fill-ring-danger, rgba(255, 107, 109, 0.18));
    border-color: var(--color-text-danger);
  }
  .authCard.is-invalid svg {
    color: var(--color-text-danger);
  }

  /* Footer block with copy + terms links — sits 16 px directly
   * under the email input card.  Earlier this had `margin-top:
   * auto` to drift to the bottom of the auth scroll area, but the
   * spec was updated: the disclaimer should read as a tight legal
   * caption attached to the input, not as a separate page-bottom
   * block.  16 px is the gap from the input card's bottom edge to
   * the disclaimer's top edge.
   *
   * Width / overflow strategy
   * -------------------------
   * The Russian translation ("Условиями использования и
   * Политикой конфиденциальности") is ~1.7× longer than the
   * English original, and `.authFooter__group` keeps "Terms of
   * Use + and" glued together with `white-space: nowrap` so the
   * conjunction never falls onto its own line.  On narrow mobile
   * viewports (320 px iPhone SE) that nowrap atom can otherwise
   * exceed the available line width and bleed past the screen
   * edge.  Defences applied below, in increasing order of severity:
   *   1. `padding: 0` (was 12 px each side) — reclaims 24 px of
   *      horizontal room on every breakpoint.
   *   2. `min-width: 0` on the flex column + `max-width: 100%` on
   *      every child — prevents any descendant from forcing the
   *      column wider than its parent's content area.
   *   3. `overflow-wrap: anywhere` on the links — if the nowrap
   *      atom *still* doesn't fit (extreme zoom, font-substitution),
   *      it will break inside a word as a last resort instead of
   *      overflowing the screen.
   *   4. `@media (max-width: 360px)` — a small font-size step-down
   *      (12 → 11 px) on very narrow phones, applied to both the
   *      note line and the links, so the layout stays balanced
   *      without jeopardising readability above 360 px.
   */
  .authFooter {
    /* Anchored to the bottom of `.authBody` via `margin-top: auto`
       — `.authBody` is a flex column with `flex: 1 0 auto`, so the
       last child with auto-top-margin floats down.  The exact
       distance from the screen's bottom edge to the footer's bottom
       is set by `.authBody { padding-bottom: 140px }` above.
       Result: legal block always sits 140 px above the screen
       bottom regardless of how short the heading / input above
       happens to be. */
    margin-top: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-width: 0;
    padding: 0;
    box-sizing: border-box;
  }
  .authFooter__note {
    margin: 0;
    max-width: 100%;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.12px;
    color: var(--color-text-secondary);
    text-align: center;
    overflow-wrap: anywhere;
  }
  /* Ratings disclaimer for the social-proof stats above — smaller and
     more muted than the legal note so it reads as a footnote. */
  .authFooter__disclaimer {
    margin: 0;
    max-width: 100%;
    font-weight: 600;
    font-size: 11px;
    line-height: 14px;
    letter-spacing: 0.11px;
    color: var(--color-text-tertiary);
    text-align: center;
    overflow-wrap: anywhere;
  }
  .authFooter__links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 4px;
    text-align: center;
    max-width: 100%;
    min-width: 0;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.12px;
  }
  .authFooter__group {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    max-width: 100%;
    /* Glue the link + conjunction so they never split across lines. */
    white-space: nowrap;
  }
  .authFooter__link {
    color: var(--color-text-brand);
    text-decoration: underline;
    overflow-wrap: anywhere;  /* last-resort break inside word */
  }
  .authFooter__sep {
    color: var(--color-text-secondary);
  }
  @media (max-width: 360px) {
    .authFooter__note,
    .authFooter__links {
      font-size: 11px;
      line-height: 14px;
    }
  }

  /*
   * Paywall (Figma 6003:14971 "Paywall") — a long scrolling page
   * (Figma height ≈ 3602 px). Layout here covers the principal
   * sections that ship with exact copy / values; remaining
   * decorative sections (University slider, testimonial group,
   * feature card carousel, full benefits art) are reduced to their
   * structural skeleton because Figma Plugin API timed out on the
   * larger text scans. See flag in the task summary.
   */
  /*
   * Full paywall stylesheet — covers every block in the Figma frame:
   *   hero (headline + timer) → pricing cards → guarantee link →
   *   CTA → disclaimer → payment logos → exam readiness (chart +
   *   legend + score callout) → trust logos → benefits → how it
   *   works carousel → guarantee emblem block → comparison table →
   *   repeat purchase block.
   *
   * Page scrolls inside a single .paywallBody column capped at 375.
   * Spacing between sections is set with `margin-top` on each top-
   * level block so the block flow matches Figma's exact 24/32/48
   * rhythm rather than using a single flex gap for everything.
   */
  .paywallBody {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    padding: 16px 16px 48px;
    box-sizing: border-box;
    flex: 1 0 auto;
    display: block;
    /*
     * No overflow-x clipping here — the trust marquee + how-it-works
     * carousel inside this body break out to 100vw, and clipping
     * here would cut them off on desktop.  Horizontal-scroll
     * prevention happens on the screen container instead.
     */
    overflow: visible;
  }
  /*
   * NOTE: horizontal-scroll prevention now lives on html/body (see
   * top of stylesheet).  The paywall screen no longer needs its own
   * overflow-x clip — that clip was actually too tight, because
   * `.screen` is inside `.viewport` (max-width: 375), so clipping
   * here would also clip any 100-vw break-out children.
   */

/*
 * preview/css/06-paywall.css
 * Paywall sections — hero, timer, CTA, disclaimer, benefits, comparison table.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L4020–L5046.
 */

  /* ── Section 1 — Hero headline + countdown timer ────────────── */
  .paywallHero {
    text-align: center;
  }
  .paywallHero__title {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  .paywallTimer {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-top: 12px;
    padding: 8px 20px;
    border-radius: 20px;
    max-width: 100%;
    /* Figma Ring/accent-danger surface + Text/danger digits. */
    background: var(--color-ring-accent-danger);
    color: var(--color-text-danger);
    font-weight: 700;
    font-size: 14px;
    line-height: 18px;
    letter-spacing: 0.14px;
    /* Stable width — current "Time left: 10:00" content fits inside
       a 168 px floor; min-width keeps the badge from shrinking as
       digits tick down ("10:00" → "9:59" no longer reflows). The
       value can still grow for longer translations because we use
       `min-width`, not a fixed width. Inside, tabular-nums forces
       every digit glyph to share the same advance width. */
    min-width: 168px;
    box-sizing: border-box;
  }
  .paywallTimer [data-paywall-timer] {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    /* Reserve a slot for the longest current countdown value so the
       digits never push the icon/label sideways. */
    min-width: 36px;
    text-align: left;
  }
  .paywallTimer__icon {
    /* Slightly bumped (16 → 20) to give the clock a bit more
       presence next to the bold 16/20 timer text. */
    width: 20px;
    height: 20px;
    flex-shrink: 0;
  }
  /* Personal plan id row — sits on its own line directly under the
     offer countdown. Block-level flex with `width: fit-content` +
     `margin: auto` for horizontal centring so it never wraps onto
     the same line as `.paywallTimer`. */
  /* Personal plan id — a compact pill ("Plan #NNNNN") centred under the
     offer countdown. */
  .paywallPlanId {
    margin: 12px auto 0;
    /* Block-level flex (not inline-flex) so the pill always sits on its
       OWN line under the timer and stays centred via width:fit-content +
       margin:auto — inline-flex let it ride up beside .paywallTimer on
       wider viewports. */
    display: flex;
    width: fit-content;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: 999px;
    background: var(--color-background-secondary);
    font-size: 14px;
    line-height: 18px;
    color: var(--color-text-primary);
  }
  .paywallPlanId__label {
    font-weight: 600;
    color: var(--color-text-secondary);
  }
  /* Activation check — green check-circle next to the plan id,
     signalling the personal plan is locked in / active. */
  .paywallPlanId__check {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    display: block;
    color: var(--color-text-success);
  }
  .paywallPlanId__num {
    font-weight: 700;
    font-size: 15px;
    line-height: 18px;
    letter-spacing: 0.1px;
    color: var(--color-text-brand);
  }
  .paywallTimer__label {
    color: var(--color-text-primary);
    font-weight: 600;
  }

  /*
   * ── Section 2 — Pricing cards (1 month / 3 month "Most Popular")
   *
   * Re-implemented to match Figma `Due Review Section` (343×120
   * for the 1-month card, 343×148 for Most Popular which adds a
   * brand-coloured top header bar).
   *
   * Layout per card:
   *   - VERTICAL stack of 2 cards (NOT side-by-side as we had).
   *   - Each card is a card body containing 3 horizontal info rows.
   *   - Each row has a left column (check_circle + label/text) and
   *     a right column (price/value).
   *   - The big mid-card price uses the special "€0,XX" rendering:
   *     a 32px main glyph + a 20px superscript-aligned cents.
   *   - The "Most Popular" header is a full-width brand-coloured
   *     bar at the top of the card (clipped by overflow:hidden).
   */
  .plans {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-top: 24px;
  }
  /* No money until the region-correct prices are painted: <html> ships
     with .is-pricing-pending (see prototype.html), pricing-display.js
     removes it on paint — or on fetch failure/timeout, revealing the
     static EUR fallback. visibility (not display) keeps the layout
     stable so the reveal doesn't shift the cards. */
  html.is-pricing-pending .plan__price,
  html.is-pricing-pending .plan__oldPerDay,
  html.is-pricing-pending .plan__totals,
  html.is-pricing-pending .paywallRenew,
  html.is-pricing-pending .paymentDisclaimer,
  html.is-pricing-pending .paymentTotal__old,
  html.is-pricing-pending .paymentTotal__new {
    visibility: hidden;
  }
  /*
   * Stable pricing card — Figma 6260:5225.
   *
   * Key invariants (no layout jump between selected/unselected):
   *   - border: 2 px solid ALWAYS — only the colour flips on toggle,
   *     so the card's outer box never changes size.
   *   - padding constant in both states.
   *   - Most Popular header bar is ALWAYS present on .plan--popular
   *     (32 px tall, occupies the top edge); selection only swaps
   *     its colour treatment, never its presence or height.
   *   - card surface stays --color-background-primary in both states;
   *     "active" feel comes from border colour + filled check_circles
   *     + filled badge + filled Most Popular bar.
   *   - badge .plan__save keeps the same geometry; only its bg/text
   *     swap (background-secondary + text-brand → background-brand +
   *     white).
   */
  .plan {
    position: relative;
    background: var(--color-background-primary);
    border: 2px solid var(--color-border-primary);
    border-radius: 20px;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 0;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
    width: 100%;
    overflow: hidden;
    box-sizing: border-box;
    /* Animate ONLY colours — no size, no padding, no margin. */
    transition:
      border-color 150ms ease,
      background-color 150ms ease;
  }
  /*
   * Paywall pricing-card hover — explicit per-screen exception.
   * Other card-buttons across the project use a 92/8 colour-mix
   * tint, but pricing cards specifically must hover to the solid
   * `background-secondary` token (per design spec for this screen
   * only).  Border colour stays — only the surface fills.
   */
  /* Hover gated to real pointer devices — see `.option:hover` for
     the touch-tap-sticks-hover rationale. */
  @media (hover: hover) {
    .plan:hover:not(.plan--selected) {
      background: var(--color-background-secondary);
      border-color: var(--color-border-primary);
    }
  }
  /*
   * Selected — only colour change. Border stays 2 px, padding stays
   * 16, geometry is identical to the unselected state.
   */
  .plan--selected {
    border-color: var(--color-text-brand);
    background: var(--color-background-primary);
  }

  /*
   * Most Popular variant — always carries the 32 px header bar at the
   * top, regardless of selection. The header is ABSOLUTELY positioned
   * against the card (which is position:relative + overflow:hidden, so
   * the bar clips to the rounded corners) instead of stretching as a
   * flex child with negative margins: the card is a <button>, and
   * WebKit before Safari 26 doesn't stretch flex children of buttons —
   * the bar shrink-wrapped to its text and the -16px margins shoved it
   * over the top-left corner on every iOS browser. The card's top
   * padding reserves the bar's 32 px + the 16 px gap below it.
   */
  .plan--popular {
    padding: 48px 16px 16px;
  }
  .plan--popular .plan__header {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 32px;
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    text-align: center;
    /* Default (unselected) state — muted background + brand text. */
    background: var(--color-background-secondary);
    color: var(--color-text-brand);
    transition:
      background-color 150ms ease,
      color 150ms ease;
  }
  /* Selected popular — header swaps to filled brand + white. */
  .plan--popular.plan--selected .plan__header {
    background: var(--color-text-brand);
    color: var(--color-text-on-accent);
  }
  /* Single info-row inside a pricing card — left column + right column. */
  .plan__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    /* Long localized plan titles (weekly "1 week trial, monthly after"
       runs 2 lines in ru/uk/fi/fr) wrap INSIDE .plan__rowLeft; the gap
       keeps the wrapped text from touching the right-hand price. */
    gap: 12px;
  }
  .plan__row .plan__oldPerDay,
  .plan__row .plan__price { flex-shrink: 0; }
  .plan__row + .plan__row { margin-top: 0; }
  .plan__rowLeft {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  /*
   * check_circle icon — 24×24 slot at the start of every row inside
   * a pricing card.  Per Figma 6260:5225 each of the 3 rows has its
   * own check_circle.  In the SELECTED card all checks are filled
   * (brand-bg circle + white tick); in unselected they are outline
   * rings.  Wired up by `wirePlans()` JS which swaps innerHTML.
   */
  .plan__check {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .plan__check svg { width: 24px; height: 24px; display: block; }
  .plan__duration {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Old (pre-discount) per-day price — strikethrough, top-right of card. */
  .plan__oldPerDay {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-decoration: line-through;
  }
  /*
   * Save 50% pill — Figma uses two variants:
   *   default card  → ring-brand bg + brand text
   *   popular card  → solid brand bg + white text
   */
  /*
   * Save 50% pill — geometry is fixed (padding 4/8, radius 12,
   * 14/16 type) and stays the same in every state.  Only background
   * + text colour change between unselected and selected.
   *
   *   default (unselected): background-secondary + text-brand
   *   selected            : text-brand bg + on-accent (white) text
   */
  .plan__save {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 4px 8px;
    border-radius: 12px;
    /* Green to draw attention to the discount (success token pair). */
    background: var(--color-fill-ring-success);
    color: var(--color-text-success);
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    white-space: nowrap;
    transition:
      background-color 150ms ease,
      color 150ms ease;
  }
  .plan--selected .plan__save {
    /* Stay green even when the plan is selected — the popular 3-month
       plan is selected by default, so this keeps the discount green
       and prominent rather than flipping to the brand fill. */
    background: var(--color-fill-ring-success);
    color: var(--color-text-success);
  }
  /* Big price block, e.g. "€0,50". Uses 32px main with 20px cents. */
  .plan__price {
    display: inline-flex;
    align-items: flex-start;
    font-family: inherit;
    color: var(--color-text-primary);
    line-height: 1;
  }
  .plan__currency {
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
  }
  .plan__cents {
    font-weight: 700;
    font-size: 20px;
    line-height: 24px;
    align-self: flex-end;
    padding-bottom: 4px;
  }
  /* Old/new totals row at the bottom of the card. */
  .plan__totals {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
  }
  .plan__cross {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-decoration: line-through;
  }
  .plan__total {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  .plan__perDay {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }

  /* ── Section 3 — "14-Day Money-Back Guarantee" inline link ──── */
  .guaranteeLink {
    display: block;
    margin: 16px auto 0;
    text-align: center;
    color: var(--color-text-brand);
    font-weight: 600;
    font-size: 14px;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
  }
  .guaranteeLink:hover { text-decoration: underline; }

  /* ── Section 4 — Primary CTA ────────────────────────────────── */
  .paywallCta {
    display: block;
    width: 100%;
    max-width: 343px;
    height: 56px;
    margin: 16px auto 0;
    padding: 16px 24px;
    border: none;
    border-radius: 20px;
    background: var(--color-background-brand);
    color: #fff;
    font-family: inherit;
    font-weight: 700;
    font-size: 16px;
    line-height: 24px;
    letter-spacing: 0.16px;
    cursor: pointer;
    transform-origin: center center;
    transition: background-color 120ms ease, var(--press-transition-cta);
  }
  .paywallCta:hover:not(:disabled) { background: #4f4dc7; }
  .paywallCta:active:not(:disabled) { transform: scale(var(--press-scale-cta)); }

  /* ── Section 5 — Subscription disclaimer microcopy ─────────── */
  .paywallDisclaimer {
    margin: 12px 0 0;
    text-align: center;
    font-weight: 600;
    font-size: 13px;
    line-height: 18px;
    color: var(--color-text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
  }
  .paywallDisclaimer__icon {
    /* Doubled from 16 → 32 per spec. Lock-shield icon now reads
       at the same visual weight as the surrounding row text. */
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    display: block;
  }
  /* Subscription renew microcopy that sits directly under the
     "Your payment is secured" row.  Body small (12/14 regular)
     in text-secondary — same colour as the icon row above so
     they read as one block. */
  .paywallRenew {
    margin: 4px 0 0;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.01em;
    color: var(--color-text-secondary);
  }

  /*
   * ── Section 6 — Payment method logos row (Figma 6252:4823).
   * Per the new design: 7 logos, each 40×27 with radius 4, laid out
   * in a HORIZONTAL flex with 8 px gap.  Container padding is
   * T32 R0 B48 L0 — same vertical breathing room as Figma.
   * Real SVG logos loaded from /preview/images/pay/.
   */
  .payMethods {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    margin: 32px 0 48px;
    flex-wrap: wrap;
  }
  .payMethod {
    width: 40px;
    height: 27px;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    overflow: hidden;
  }
  .payMethod img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  /*
   * ── Section 7 — Exam readiness
   *
   * Figma "Chart Container" 320×199 + "Row Container" 319×30 (skill
   * row legend) + a separate 49 px section underneath for the
   * "Score Projection / Sample Goal" line legend.
   *
   * The chart keeps the square 320×199 viewBox from Figma and uses
   * 4 brand-coloured skill curves rising into a brand projection
   * dot, plus a dashed Sample Goal reference line.
   */
  .readiness {
    margin-top: 48px;
    text-align: center;
  }
  .readiness__title {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    color: var(--color-text-primary);
  }
  /* Plain heading text — no pill / rounded backdrop. Matches the
     `Heading large` style (24/32 Bold, primary text colour) so it
     reads as part of the section heading hierarchy, not a badge. */
  .readiness__value {
    display: block;
    margin: 8px auto 0;
    padding: 0;
    background: transparent;
    border-radius: 0;
    color: var(--color-text-primary);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
  }
  .readiness__subtitle {
    margin: 8px 0 0;
    font-weight: 600;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
  }
  .readinessChart {
    margin: 20px auto 0;
    width: 100%;
    max-width: 343px;
    box-sizing: border-box;
  }
  /* Dark variant — matches the Figma 6215:2882 reference: pure black
     surface with rounded corners, soft inner padding so the chart's
     own grid doesn't bleed into the card edge. The 320×175 viewBox
     scales to fit the inner content width while preserving aspect. */
  /* Chart container is fully transparent — no dark/opaque backdrop.
     The SVG paints directly onto the page surface. */
  .readinessChart--dark {
    background: transparent;
    border-radius: 0;
    padding: 0;
    overflow: visible;
  }
  .readinessChart--dark .readinessChart__skills,
  .readinessChart--dark .readinessChart__skill {
    /* Inherit page text token so labels read on whatever the
       surrounding surface is, light or dark. */
    color: var(--color-text-primary);
    padding: 0;
  }
  .readinessChart__svg {
    display: block;
    width: 100%;
    height: auto;
  }
  /*
   * Skills legend — 4 pills sit directly under the chart at 319×30
   * (Reading / Listening / Speaking / Writing). Inline dot + label,
   * spaced evenly across the chart width.
   */
  .readinessChart__skills {
    display: flex;
    justify-content: space-around;
    align-items: center;
    gap: 8px;
    margin-top: 8px;
  }
  .readinessChart__skill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-weight: 600;
    font-size: 12px;
    line-height: 16px;
    color: var(--color-text-primary);
  }
  .readinessChart__dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex-shrink: 0;
  }
  /*
   * Lower legend — Score Projection (solid brand line) + Sample
   * Goal (dashed brand line).
   */
  .readinessChart__legend {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 24px;
    margin-top: 16px;
    flex-wrap: wrap;
  }
  .readinessChart__legendItem {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-weight: 600;
    font-size: 13px;
    line-height: 18px;
    color: var(--color-text-primary);
  }
  /* Legend swatches mirror the actual lines drawn on the chart:
       - Score Projection: solid neutral grey (#8695aa) — same
         stroke as the projection polyline + milestone dots.
       - Sample Goal: red dashed line (#FF4D4F) using the same
         5 / 4 dash pattern as the chart's `stroke-dasharray="5 4"`
         for a 1:1 visual match. Implemented via a repeating linear
         gradient (renders consistently across browsers regardless
         of border-style dash heuristics). */
  .readinessChart__legendLine {
    width: 24px;
    height: 2px;
    border-radius: 1px;
    flex-shrink: 0;
    background: #8695aa;
  }
  .readinessChart__legendLine--dashed {
    border: none;
    border-radius: 0;
    height: 2px;
    background: repeating-linear-gradient(
      90deg,
      #FF4D4F 0,
      #FF4D4F 5px,
      transparent 5px,
      transparent 9px
    );
  }

  /*
   * ── Section 8 — "Trusted and accepted by organizations" marquee
   *
   * Implements an infinite right-to-left marquee per spec:
   *   - Cards 160×64 (Figma "div.university-slider__card")
   *   - Track full-width: extends past .paywallBody so on a wider
   *     viewport the lane covers the whole browser width.
   *   - Two identical strips inside one track; a CSS keyframe shifts
   *     the track left by half its own width, looping seamlessly.
   *   - Edges fade-out via mask so cards don't pop on/off harshly.
   *
   * NOTE on assets: the Figma "Trust1..Trust8" frames each contain a
   * single placeholder image fill — they are not branded logos. This
   * implementation uses styled text plates that approximate the look
   * (white card, soft border, light tracking). If real logo PNGs are
   * later added to /preview/images/trust/, drop-in via background-
   * image overrides per .trustLogo--N.
   */
  /*
   * NOTE: The Trusted-by carousel was rebuilt as the dedicated
   * `.uniSlider` component (rules below).  The old `.trustSection`,
   * `.marquee`, `.trustLogo` rules are no longer used and have been
   * removed.  Anything that referenced them has been replaced with
   * the new component classes in the markup.
   */
  /*
   * ─── University Slider — Trusted by organizations ───────────
   * 1:1 reproduction of Figma node 6266:5296.
   *
   * Spec verbatim from Figma:
   *   Slider frame   : 1364 × 144, padding T40 R0 B40 L0, gap 12,
   *                    counter-axis CENTER (cards vertically centred).
   *   Card           : 160 × 64, radius 8, fill #ffffff (solid white),
   *                    NO border, primary-axis CENTER + counter-axis
   *                    CENTER (image centred inside the card).
   *   Image inside   : 121.86 × 63.99, scaleMode CROP (object-fit
   *                    cover), centred → 19.07 px transparent edge
   *                    on each side of the card.
   *   Card order     : oxford, princeton, harvard, massachusetts,
   *                    standford, warton, lseps, cambridge.
   *
   * Animation:
   *   - 4 identical 8-card sets in the track (32 cards) so the
   *     strip stays full on ultra-wide viewports.
   *   - translateX(0) → translateX(-25%) over 40 s linear infinite —
   *     25 % = one full set, so the loop is mathematically seamless.
   *   - margin-right per card (NOT container gap) so the inter-card
   *     spacing is identical across the seam between sets.
   *
   * Layout:
   *   - section breaks out of the 343 paywallBody column via
   *     negative margin + width: 100vw so the strip uses the entire
   *     viewport on any screen size.
   *   - .uniSlider__viewport clips overflow at the viewport edge
   *     and applies a soft horizontal fade mask so cards dissolve
   *     into the background near the edges.
   */
  /*
   * Full-viewport-width break-out — works regardless of how narrow
   * the parent column is and regardless of intermediate overflow
   * settings.
   *
   * Technique:
   *   position: relative; left: 50%; margin-left: -50vw;
   *
   * `left: 50%` aligns the slider's left edge with the parent's
   * horizontal centre.  Then `margin-left: -50vw` shifts it back by
   * half the viewport width.  Net result: the slider's left edge
   * sits exactly at the viewport's left edge, and `width: 100vw`
   * reaches the right edge.  Body-level overflow-x: hidden catches
   * any rounding overflow so no horizontal scrollbar appears.
   */
  .uniSlider {
    position: relative;
    left: 50%;
    margin-left: -50vw;
    margin-right: 0;
    margin-top: 48px;
    width: 100vw;
    max-width: 100vw;
  }
  .uniSlider__title {
    margin: 0 auto;
    max-width: 343px;
    text-align: center;
    font-weight: 600;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
    padding: 0 16px;
    box-sizing: border-box;
  }
  .uniSlider__viewport {
    /* 40 px above and below the card row (Figma padding). */
    padding: 40px 0;
    overflow: hidden;
    width: 100%;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%);
  }
  .uniSlider__track {
    display: flex;
    width: max-content;
    animation: uniSlider-scroll 40s linear infinite;
    will-change: transform;
  }
  .uniSlider__viewport:hover .uniSlider__track { animation-play-state: paused; }
  .uniSlider__card {
    width: 160px;
    height: 64px;
    margin-right: 12px;
    border-radius: 8px;
    background: #ffffff;     /* Figma SOLID #ffffff — solid white in both themes */
    /* No border per Figma. */
    display: flex;
    align-items: center;
    justify-content: center; /* primary-axis CENTER */
    box-sizing: border-box;
    flex-shrink: 0;
    overflow: hidden;
  }
  .uniSlider__card img {
    /* Image inset matches Figma — 121.86 ≈ 76 % of the 160 card. */
    display: block;
    width: 121.86px;
    height: 63.99px;
    object-fit: cover;        /* Figma scaleMode CROP */
  }
  /*
   * Loop is exactly one of the 4 identical sets — 25 %.
   */
  @keyframes uniSlider-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-25%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .uniSlider__track { animation: none; }
  }

  /* ── Section 9 — "What you get with LangArena" benefits ─────── */
  .paywallSection {
    margin-top: 48px;
  }
  .paywallSection__title {
    margin: 0 0 20px;
    text-align: center;
    font-weight: 700;
    font-size: 22px;
    line-height: 28px;
    color: var(--color-text-primary);
  }
  .benefits {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .benefit {
    display: flex;
    align-items: center;
    gap: 12px;
    font-weight: 500;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-primary);
  }
  /* Check icon — pulled verbatim from /preview/images/check.svg
     (24×24, brand-purple stroke). All benefit rows share this size
     so they line up on a single vertical axis. */
  .benefit__icon {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    display: block;
  }
  .benefit__text {
    display: block;
    flex: 1 1 auto;
  }

  /* App-screen showcase — horizontal swipeable slider; one large
     screenshot per slide with its caption above. Replaces the old
     check-list "What you get". */
  .appSlider {
    margin-top: 40px;
    display: flex;
    gap: 14px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;            /* Firefox — hide scrollbar */
    /* Full-viewport break-out (same technique as .uniSlider) so the
       screens run edge-to-edge and overflow the sides. */
    position: relative;
    left: 50%;
    margin-left: -50vw;
    width: 100vw;
    max-width: 100vw;
    box-sizing: border-box;
    /* Side insets so the active slide centres while its neighbours peek
       at the viewport edges; scroll-padding keeps snapping centred. */
    padding: 4px 16vw;
    scroll-padding-inline: 16vw;
  }
  .appSlider::-webkit-scrollbar { display: none; }   /* WebKit */
  .appShot {
    flex: 0 0 68vw;                   /* big centred slide; neighbours peek */
    max-width: 320px;
    scroll-snap-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
  }
  .appShot__cap {
    font-weight: 700;
    font-size: 20px;
    line-height: 26px;
    text-align: center;
    color: var(--color-text-brand);
    /* Reserve two lines so a one- vs two-line caption never shifts the
       phone image below it — every screenshot starts at the same Y. The
       caption centres vertically within the reserved space so one-line
       captions sit mid-way rather than hugging the phone. */
    min-height: 52px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* No card framing — the screenshots are transparent PNGs of a phone
     mockup, so they sit directly on the page background. */
  .appShot__img {
    display: block;
    width: 100%;
    aspect-ratio: 720 / 1320;
    object-fit: contain;
  }

  /*
   * ── Section 10 — "How LangArena Works" horizontal cards
   *
   * Figma desktop variant ships 5 Feature Cards 239×269 in a strip
   * that overflows the 1325 viewport width — so on wide viewports
   * we extend the strip to span the whole browser, and on narrow
   * mobile (≤375) we keep horizontal scroll-snap so all 5 cards
   * remain reachable without truncating any.
   */
  /*
   * ─── How LangArena Works — Figma 6215:3013 ─────────────────
   * Same full-width seamless marquee architecture as .uniSlider:
   *   - 5 different cards × 4 sets = 20 cards in the track
   *   - margin-right per card (instead of container gap) so the
   *     spacing is identical across the seam between sets
   *   - translateX(0) → translateX(-25%) over 40 s linear infinite
   *     → loop is mathematically seamless
   *   - position: relative + left: 50% + margin-left: -50vw → break
   *     out of the 343 paywallBody column to span the full viewport
   *
   * Card spec verbatim from Figma 6215:3014:
   *   239 × 269, padding 24 all sides, radius 24
   *   inner content: 189 × 219, vertical flex, gap 16, items-center
   *     ├── image 96 × 96
   *     ├── h4 title (Semibold)
   *     └── description (3 lines, Body)
   */
  .howSlider {
    position: relative;
    left: 50%;
    margin-left: -50vw;
    margin-top: 16px;
    width: 100vw;
    max-width: 100vw;
  }
  .howSlider__viewport {
    overflow: hidden;
    width: 100%;
    padding: 4px 0;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
  }
  .howSlider__track {
    /* Step carousel (driven by wireCenteredCarousels in JS) — same
       behaviour as `.reviewsMarquee__track`: one card centred per
       viewport, 3 s hold, slow ease into the next. */
    display: flex;
    width: max-content;
    will-change: transform;
  }
  .howCard {
    width: 239px;
    /* Height is dynamic — Figma's 269 px design value becomes a
       min-height floor.  The flex track (`.howSlider__track`)
       inherits the default `align-items: stretch`, so every
       card in the carousel auto-grows to match the tallest
       card's content height.  This keeps the carousel visually
       even regardless of how long any single translation is.
       The 24 px padding-bottom is preserved through `padding`,
       so the gap from the last text line to the card edge
       never collapses below the design value. */
    min-height: 269px;
    margin-right: 16px;
    border-radius: 24px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    padding: 24px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;        /* Figma counter-axis CENTER */
    gap: 16px;                  /* Figma item-spacing 16 */
    flex-shrink: 0;
  }
  .howCard__icon {
    width: 96px;
    height: 96px;
    flex-shrink: 0;
    background: transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
  }
  .howCard__icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  .howCard__title {
    margin: 0;
    font-weight: 600;
    font-size: 18px;
    line-height: 28px;
    color: var(--color-text-primary);
    text-align: center;
    /* Reserve two lines so a one- vs two-line title never shifts the
       description (and keeps every card's text block aligned). */
    min-height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .howCard__desc {
    margin: 0;
    font-weight: 500;
    font-size: 14px;
    line-height: 21px;
    color: var(--color-text-secondary);
    text-align: center;
  }

  /*
   * ── Section 11 — Money-Back Guarantee block
   * Emblem is now a 16-pointed star drawn directly in SVG (path data
   * supplied alongside the design spec).  Container ≈ 90×90 with the
   * "14" numeral overlaid — caption follows underneath.
   */
  .guarantee {
    margin-top: 48px;
    text-align: center;
    padding: 0;
  }
  .guarantee__emblem {
    width: 90px;
    height: 90px;
    margin: 0 auto;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Clip the Lottie shimmer (Shape Layer 1) so the diagonal blick
       stays inside the 90×90 badge frame instead of bleeding past
       the scalloped silhouette. */
    overflow: hidden;
  }
  /*
   * Lottie animation slot — fills the 90×90 emblem.  lottie-web
   * mounts an SVG inside this div and animates it; the SVG inherits
   * the parent's size so the badge scales with the emblem.
   */
  .guarantee__emblemAnim {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }
  .guarantee__emblemAnim svg {
    width: 100% !important;
    height: 100% !important;
    display: block;
    overflow: hidden;
  }
  /*
   * "14" numeral overlaid on top of the animated star — sits on
   * z-index 1 so it stays legible above the spinning artwork.
   */
  /* .guarantee__emblemNum was the static "14" overlay used while we
     waited for the Lottie animation to render. The Lottie file
     paints its own "14" on the badge so the overlay is no longer
     needed and was removed from the markup; the rule is kept
     deleted to avoid orphaned styles. */
  .guarantee__title {
    margin: 16px 0 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    color: var(--color-text-primary);
  }
  .guarantee__body {
    margin: 12px auto 0;
    max-width: 303px;
    font-weight: 500;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
    text-align: left;   /* body reads left-aligned; emblem + title stay centred */
  }

  /* ── Section 12 — Comparison table (LangArena vs Tutor) ─────── */
  .compare {
    margin-top: 48px;
  }
  .compare__title {
    margin: 0 0 16px;
    text-align: center;
    font-weight: 700;
    font-size: 22px;
    line-height: 28px;
    color: var(--color-text-primary);
  }
  .compareTable {
    width: 100%;
    border-radius: 20px;
    overflow: hidden;
    border: 1px solid var(--color-border-primary);
    background: var(--color-ring-inverse);
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
  .compareTable__head {
    padding: 14px 12px;
    font-weight: 700;
    font-size: 14px;
    line-height: 18px;
    text-align: center;
    border-bottom: 1px solid var(--color-border-primary);
  }
  .compareTable__head--brand {
    background: var(--color-ring-brand);
    color: var(--color-text-brand);
  }
  .compareTable__head--alt {
    background: var(--color-background-tertiary);
    color: var(--color-text-primary);
  }
  .compareTable__cell {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 14px 12px;
    font-weight: 500;
    font-size: 13px;
    line-height: 18px;
    color: var(--color-text-primary);
    border-bottom: 1px solid var(--color-border-primary);
    box-sizing: border-box;
  }
  .compareTable__cell:nth-last-child(-n+2) { border-bottom: none; }
  .compareTable__cell--brand { background: rgba(88, 86, 214, 0.04); }
  .compareTable__cell--alt   { color: var(--color-text-secondary); }
  .compareTable__tick,
  .compareTable__cross {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    margin-top: 0;
    display: block;
  }
  /* Tick is rendered as a CSS mask so its colour is driven by
     `background-color` (the project's token system has only
     `--color-text-success` — #248a3d light / #30db5b dark — and no
     `--color-background-success`; using the closest valid green).

     The mask source is an *inlined* data URL containing just the
     check path — referencing `images/check.svg` via `url()` works
     on http(s) but is blocked or rendered empty on file://, which
     made the icons disappear in the previous build. The data URL
     is identical in shape to the repo asset (same path + viewBox),
     just stripped of the internal `<mask>` element which the
     external file uses for the Figma export. */
  .compareTable__tick {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M9.5496 18L3.8496 12.3L5.2746 10.875L9.5496 15.15L18.7246 5.975L20.1496 7.4L9.5496 18Z'/></svg>");
            mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M9.5496 18L3.8496 12.3L5.2746 10.875L9.5496 15.15L18.7246 5.975L20.1496 7.4L9.5496 18Z'/></svg>");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-size: contain;
            mask-size: contain;
    background-color: var(--color-text-success);
  }
  .compareTable__cross  { color: #d4384d; }

  /* ── Small helper for terms/secondary body copy ────────────── */
  .paywallTerms {
    text-align: center;
    font-weight: 500;
    font-size: 12px;
    line-height: 16px;
    color: var(--color-text-secondary);
    margin: 24px 0 0;
  }
  .paywallTerms a {
    color: var(--color-text-brand);
    text-decoration: underline;
  }

  /* ─────────────────────────────────────────────────────────────
     Purchase-success screen — logo + outlined check + thank-you
     title, then a platform-specific panel (install steps + store CTA
     on mobile, hint + platform CTA on desktop).
     ───────────────────────────────────────────────────────────── */
  .success.screen.active {
    align-items: center;
    justify-content: flex-start;
  }
  .successBody {
    width: 100%;
    max-width: 343px;
    margin: 0 auto;
    padding-top: 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .success__check {
    width: 56px;
    height: 56px;
    margin-top: 48px;
    display: block;
  }
  .success__check svg { width: 100%; height: 100%; display: block; }
  .success__title {
    margin: 28px 0 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 30px;
    color: var(--color-text-primary);
    /* Two title lines stacked with a blank row between them. */
    display: flex;
    flex-direction: column;
    gap: 30px;
  }
  /* Second title line ("Welcome to the LangArena family!") in brand
     blue, matching the "Sign In" emphasis. Hidden until the typewriter
     reveals it (JS adds .is-shown) so the full line never flashes in
     before it's typed out. */
  .success__welcome { color: var(--color-text-brand); visibility: hidden; }
  .success__welcome.is-shown { visibility: visible; }

  /* ─────────────────────────────────────────────────────────────
     Success celebration entrance — confetti burst, the check pops,
     line 1 fades up, line 2 ("Welcome…") types out, then the card(s)
     reveal. Driven by wireSuccess() toggling `.is-celebrating` on the
     screen and `.is-prereveal` / `.is-revealed` on the active panel.
     ───────────────────────────────────────────────────────────── */
  .success.screen { position: relative; }
  .successConfetti {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: 560px;
    aspect-ratio: 1 / 1;
    pointer-events: none;
    z-index: 1;
  }
  .successConfetti svg { width: 100% !important; height: 100% !important; display: block; }
  /* Keep the content legible above the confetti. */
  .successBody { position: relative; z-index: 2; }

  .success.is-celebrating .success__check {
    animation: successCheckPop 680ms cubic-bezier(0.34, 1.56, 0.45, 1) both;
  }
  @keyframes successCheckPop {
    0%   { transform: scale(0);   opacity: 0; }
    60%  { opacity: 1; }
    100% { transform: scale(1);   opacity: 1; }
  }
  .success.is-celebrating .success__title > span:first-child {
    animation: successFadeUp 460ms ease both 340ms;
  }
  @keyframes successFadeUp {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
  }
  /* Blinking caret while the welcome line types out. */
  .success__welcome.is-typing::after {
    content: "";
    display: inline-block;
    width: 2px;
    height: 1em;
    margin-left: 3px;
    background: currentColor;
    vertical-align: -0.12em;
    animation: successCaret 760ms steps(1) infinite;
  }
  @keyframes successCaret { 50% { opacity: 0; } }

  /* Mobile panel reveals as one block; desktop staggers its two cards. */
  .success__panel.is-prereveal { opacity: 0; }
  .success__panel:not(.success__panel--desktop).is-prereveal { transform: translateY(18px); }
  .success__panel:not(.success__panel--desktop).is-revealed {
    opacity: 1;
    transform: none;
    transition: opacity 560ms ease, transform 660ms cubic-bezier(0.2, 0.8, 0.3, 1);
  }
  .success__panel--desktop.is-prereveal { opacity: 1; }
  .success__panel--desktop.is-prereveal .successCard { opacity: 0; transform: translateY(18px); }
  .success__panel--desktop.is-revealed { opacity: 1; }
  .success__panel--desktop.is-revealed .successCard {
    opacity: 1;
    transform: none;
    transition: opacity 560ms ease, transform 660ms cubic-bezier(0.2, 0.8, 0.3, 1);
  }
  .success__panel--desktop.is-revealed .successCard:nth-child(2) { transition-delay: 220ms; }

  @media (prefers-reduced-motion: reduce) {
    .successConfetti { display: none; }
    .success.is-celebrating .success__check,
    .success.is-celebrating .success__title > span:first-child { animation: none; }
    .success__panel.is-prereveal,
    .success__panel--desktop.is-prereveal .successCard { opacity: 1; transform: none; }
    .success__welcome.is-typing::after { display: none; }
  }
  .success__panel {
    width: 100%;
    margin-top: 32px;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .success__panel[hidden] { display: none; }
  .success__steps {
    margin: 0;
    padding: 0 0 0 26px;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 16px;
  }
  .success__step {
    font-weight: 600;
    font-size: 19px;
    line-height: 26px;
    color: var(--color-text-primary);
  }
  .success__email {
    font-weight: 700;
    color: var(--color-text-brand);
    overflow-wrap: anywhere;
  }
  /* Emphasised inline label (e.g. the "Sign In" button name in step 2) —
     bold + brand purple so the action stands out in the sentence. */
  .success__emph {
    font-weight: 700;
    color: var(--color-text-brand);
  }
  .success__hint {
    margin: 0;
    font-weight: 500;
    font-size: 16px;
    line-height: 22px;
    color: var(--color-text-primary);
  }
  /* CTA is an <a> here — the shared .cta rule targets buttons'
     box model but anchors need the link chrome stripped + flex
     centring for the label. */
  .success__cta {
    margin-top: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    box-sizing: border-box;
  }
  /* Native store badge (App Store / Google Play). Both official SVGs
     share the same 3.375:1 aspect ratio, so a fixed height keeps the
     two variants identical in footprint. */
  .success__store {
    margin-top: 28px;
    display: block;
    /* No double-tap-zoom on these targets — keeps the tap a clean,
       immediate activation with no synthesized ghost-click drift. */
    touch-action: manipulation;
  }
  .success__storeBadge {
    height: 56px;
    display: block;
  }
  /* "To get started:" heading above the numbered steps. */
  .success__getStarted {
    width: 100%;
    margin: 0 0 16px;
    text-align: left;
    font-weight: 700;
    font-size: 19px;
    line-height: 26px;
    color: var(--color-text-primary);
  }
  /* Two stacked store buttons (App Store + Google Play). The wider gap
     keeps the two ~56px-tall badges well clear of each other so a
     near-boundary tap can't land on the wrong store. */
  .success__stores {
    margin-top: 28px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }
  .success__stores .success__store { margin-top: 0; }
  /* Desktop variant — two white cards (web-app + scan-to-download).
     The cards are white so the laptop/QR webps' white backgrounds blend
     seamlessly. */
  .success__panel--desktop { gap: 24px; }
  .successCard {
    width: 100%;
    box-sizing: border-box;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    border-radius: 24px;
    padding: 28px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 22px;
  }
  .successCard__caption {
    margin: 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 22px;
    text-align: center;
    color: var(--color-text-primary);
  }
  /* "To get started on mobile:" heading + steps inside the desktop QR
     card — the card's flex gap handles spacing, so drop the heading's
     own margin and let the list span the card width (left-aligned). */
  .successCard .success__getStarted { margin: 0; }
  .successCard .success__steps { align-self: stretch; }
  /* Store buttons inside the desktop card use the card's flex gap. */
  .successCard .success__stores { margin-top: 0; }
  .success__desktopImg {
    width: 200px;
    max-width: 70%;
    height: auto;
    display: block;
    margin: 0;
  }
  .success__qr {
    width: 188px;
    max-width: 66%;
    height: auto;
    display: block;
    margin: 0;
  }
  /* CTA inside a success card — same button chrome as .success__cta but
     no standalone top margin (the card's flex gap handles spacing). */
  .success__cardCta {
    width: 100%;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    box-sizing: border-box;
  }


  /* ─────────────────────────────────────────────────────────────
     v3 · Paywall score reveal — the number the ad promised lands
     here, at the exit-vs-buy decision point. Rendered by
     wireScoreReveal() from quiz + targetScore + testDate.
     ───────────────────────────────────────────────────────────── */
  .scoreReveal {
    width: 100%;
    max-width: 343px;
    margin: 0 auto 24px;
    background: var(--color-background-primary);
    border: 2px solid var(--color-text-brand);
    border-radius: 20px;
    padding: 20px 16px;
    box-sizing: border-box;
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .scoreReveal__label {
    margin: 0;
    font-weight: 600;
    font-size: 14px;
    line-height: 19px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--color-text-secondary);
  }
  .scoreReveal__scaleHint {
    display: inline-block;
    margin-left: 2px;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--color-background-secondary);
    color: var(--color-text-brand);
    font-weight: 700;
    font-size: 11px;
    letter-spacing: 0.2px;
    text-transform: none;
    vertical-align: middle;
  }
  .scoreReveal__value {
    font-weight: 800;
    font-size: 64px;
    line-height: 1.02;
    letter-spacing: -1.5px;
    background: linear-gradient(180deg, #7b6cff 0%, var(--color-text-brand) 55%, #3f2fd6 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: var(--color-text-brand);
  }

  /* v3 · current · gap · target row beneath the big number. */
  .scoreGap {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: end;
    gap: 8px;
    margin-top: 2px;
  }
  .scoreGap__now {
    font-weight: 800;
    font-size: 20px;
    line-height: 1;
    color: var(--color-text-brand);
    white-space: nowrap;
  }
  .scoreGap__target {
    font-weight: 800;
    font-size: 20px;
    line-height: 1;
    color: var(--color-text-success);
    white-space: nowrap;
  }
  .scoreGap__bracket {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-width: 0;
  }
  .scoreGap__bracketLabel {
    font-weight: 800;
    font-size: 18px;
    line-height: 1;
    color: var(--color-text-brand);
    white-space: nowrap;
  }
  .scoreGap__bracketLine {
    width: 100%;
    height: 8px;
    border: 2px solid var(--color-border-primary);
    border-bottom: 0;
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
  }

  /* v3 · Today ●── Strategy │ Practice ──● test-date timeline. */
  .scoreTrack {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 12px;
  }
  .scoreTrack__dot {
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    box-sizing: border-box;
    border: 7px solid var(--color-background-primary);
  }
  .scoreTrack__dot--now  { background: var(--color-text-brand);   box-shadow: 0 0 0 2px var(--color-text-brand); }
  .scoreTrack__dot--goal { background: var(--color-text-success); box-shadow: 0 0 0 2px var(--color-text-success); }
  .scoreTrack__bar {
    flex: 1 1 auto;
    display: flex;
    height: 38px;
    border-radius: 8px;
    overflow: hidden;
    min-width: 0;
  }
  .scoreTrack__seg {
    flex: 1 1 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 15px;
    color: var(--color-text-brand);
    min-width: 0;
  }
  .scoreTrack__seg--strategy {
    background-color: var(--color-background-secondary);
    background-image: repeating-linear-gradient(
      -45deg,
      rgba(88, 86, 214, 0.16) 0 6px,
      rgba(88, 86, 214, 0) 6px 12px
    );
  }
  .scoreTrack__seg--practice {
    background-color: var(--color-background-secondary);
    background-image: radial-gradient(
      rgba(88, 86, 214, 0.22) 1.4px,
      transparent 1.5px
    );
    background-size: 9px 9px;
  }
  .scoreTrack__labels {
    display: flex;
    justify-content: space-between;
    margin-top: 8px;
  }
  .scoreTrack__chip {
    font-weight: 700;
    font-size: 15px;
    line-height: 1;
    padding: 7px 12px;
    border-radius: 9px;
  }
  .scoreTrack__chip--now {
    color: var(--color-text-brand);
    background: var(--color-background-secondary);
  }
  .scoreTrack__chip--goal {
    color: var(--color-text-success);
    background: var(--color-fill-ring-success);
  }

  /* v3 · Strategy + Practice breakdown — the gap reframed as the two
     workstreams the plan covers. */
  .scorePlan__heading {
    margin: 18px 0 0;
    font-weight: 800;
    font-size: 17px;
    line-height: 22px;
    color: var(--color-text-primary);
  }
  .scorePlan__subheading {
    margin: 4px 0 0;
    font-weight: 600;
    font-size: 13px;
    line-height: 18px;
    color: var(--color-text-secondary);
  }
  /* Flex row (not grid): a CSS grid child inside a flex-column parent
     (.scoreReveal) hits an iOS Safari intrinsic-height bug. Columns are a
     FIXED width — wide enough that long words (e.g. RU
     "Конспектирование", FI "Luentomuistiinpanojen") never overflow the
     box — so the row can exceed the card width and scrolls horizontally:
     the user sees the first box plus a peek of the second and swipes it
     into view. `overflow-wrap: break-word` on the list items is a final
     safety net for any extreme word in untested locales. */
  .scorePlan {
    display: flex;
    gap: 12px;
    margin-top: 12px;
    text-align: left;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;            /* Firefox — hide scrollbar */
  }
  .scorePlan::-webkit-scrollbar { display: none; }   /* WebKit */
  .scorePlan__col {
    flex: 0 0 200px;                  /* fixed, no shrink — fits long words */
    scroll-snap-align: start;
    background: var(--color-background-secondary);
    border-radius: 14px;
    padding: 12px 12px 14px;
  }
  .scorePlan__head {
    display: flex;
    align-items: center;
    gap: 7px;
    padding-bottom: 8px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--color-border-primary);
  }
  .scorePlan__icon { font-size: 16px; line-height: 1; }
  .scorePlan__title {
    font-weight: 800;
    font-size: 17px;
    color: var(--color-text-brand);
  }
  .scorePlan__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 9px;
  }
  .scorePlan__list li {
    position: relative;
    padding-left: 26px;
    font-weight: 500;
    font-size: 14px;
    line-height: 18px;
    color: var(--color-text-primary);
    overflow-wrap: break-word;
  }
  .scorePlan__list li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 1px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--color-background-brand)
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Cpath d='M5 12.5l4 4 10-10' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
      center / 12px 12px no-repeat;
  }

  /* v3 · Platform availability badges — informational, no links. */
  .platformsBlock {
    margin-top: 40px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
  }
  .platformsBlock__caption {
    margin: 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 22px;
    color: var(--color-text-primary);
    text-align: center;
  }
  .platformsBlock__badges {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .platformsBlock__badges img {
    height: 44px;
    display: block;
  }
/*
 * preview/css/07-mid-flow.css
 * Ready-3-min test, Plan English practice, How-your-brain-learns, Auth, Persona flow.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L5047–L5994.
 */

  /* ─── Ready for a 3 minute test? (6003:13307) ──────────────── */
  /*
   * Header on this screen has no progress bar but extra bottom
   * padding (52) so the gap between the logo and the title lines
   * up with Figma.
   */
  .header--ready {
    /* 32-px gap from the logo to the title — was 72 per the
       previous spec; lifted by 40 px so the whole below-title
       stack (title + image + options) sits 40 px higher up the
       screen.  Title has inline `margin-top: 0`, so this padding
       IS the gap; everything below follows in column flow. */
    padding-bottom: 32px;
  }
  /*
   * Centered 280 × 280 image between title and the two option cards.
   * Generic container — swap asset via `<img src>`.
   */
  .readyImage {
    /* 200×200 hero between title and option cards.  Top margin =
       20 px (gap from title bottom to image top, per spec).  The
       image itself still uses object-fit: contain so the asset
       isn't distorted at the smaller size. */
    width: 200px;
    height: 200px;
    margin: 20px auto 0;
    background: transparent;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .readyImage > img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .screen[data-screen="ready3MinTest"] .options {
    margin-top: 20px;
  }

  /* ─── Plan your English practice (6003:11751) ──────────────── */
  /*
   * Image 335 × 180 + period labels (Today | One Month) directly
   * beneath. Image container is generic — swap the asset without
   * touching layout.
   */
  .planMedia {
    width: 100%;
    /* lifted 32 px from the previous 48 — content below the title
       (image stack + labels + options) sits tighter against the
       heading.  Internal rhythm is unchanged: planMedia → options
       gap stays at 20 px via the dedicated override below. */
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  /*
   * Hero image on the Plan-practice screen.  Two stacked <img>s
   * cross-fade in place when the user picks a different time slot.
   * Container is transparent so the 3D scene's own canvas shows
   * through; both layers are absolutely-positioned to stack inside
   * the 180-tall slot.
   */
  .planMedia__image {
    position: relative;
    width: 100%;
    height: 180px;
    background: transparent;
    overflow: visible;
  }
  .planMedia__layer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0;
    transition: opacity 320ms ease;
    display: block;
  }
  .planMedia__layer.is-visible {
    opacity: 1;
  }
  /*
   * Labels under the image — Figma spec:
   *   Today    : left-aligned to image left edge
   *   One Month: right-aligned to image right edge
   *   style    : Semibold 16/24, text-primary (#393c46)
   */
  .planMedia__labels {
    display: flex;
    justify-content: space-between;
    gap: 4px;
  }
  .planMedia__label {
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  .planMedia__label:first-child  { text-align: left;  }
  .planMedia__label:last-child   { text-align: right; }
  /*
   * On this screen the options row follows the planMedia block. The
   * Figma gap between image+labels block and the first card is 20.
   */
  .screen[data-screen="planEnglishPractice"] .options {
    margin-top: 20px;
  }

  /* ─── How your brain learns (6003:11696) ───────────────────── */
  /*
   * Labels row + image stack + big "2x" stat. Simplified from Figma's
   * overlapping layout (Figma uses a negative -24 gap to nest labels
   * into the top of the image) — kept here as a clean vertical stack
   * because the Image Placeholder has no asset anyway. Replace with
   * overlap if the illustration lands and requires it.
   */
  .brain {
    width: 100%;
    margin-top: 16px;       /* lifted 32 px from the previous 48 — content below the
                               title now sits tighter against it.  Figma was 48; revised
                               here to compress the screen so the Spaced-Repetition stat
                               is visible above the fold on shorter mobile devices. */
    display: flex;
    flex-direction: column;
    /* Was gap: 24px; per Figma the labels sink INTO the image and
       the image sits flush against the 2× stat — no positive gap.
       The vertical bleed is encoded as negative margin on .brain__labels. */
    gap: 0;
  }
  .brain__labels {
    display: flex;
    justify-content: space-between;
    gap: 4px;
    /* Figma: labels live in an 80-tall container with items-end so the
       text hugs the bottom edge, then mb-(-24) makes the bottom 24
       overlap the brain image (so the brain bodies poke up into the
       label row, exactly as in the design). */
    height: 80px;
    align-items: flex-end;
    margin-bottom: -24px;
    position: relative;
    z-index: 1;
  }
  .brain__label {
    flex: 1;
    text-align: center;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Spaced Repetition sits 24 px higher than Regular Study per
     spec — use translateY so the underlying flex geometry (and the
     overlap with the brain image) is unchanged. */
  .brain__label:nth-child(2) {
    transform: translateY(-24px);
  }
  /*
   * Brain-learns image — transparent slot (no card frame).  The
   * supplied brain_learns.webp asset has its own background so we
   * just drop it onto the canvas.  object-fit: contain preserves
   * the full chart/diagram.
   */
  .brain__image {
    width: 100%;
    height: 180px;
    background: transparent;
    overflow: visible;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .brain__image > img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .brain__stat {
    text-align: center;
  }
  .brain__stat-value {
    margin: 0;
    font-weight: 700;
    font-size: 40px;
    line-height: 48px;
    color: var(--color-text-primary);
    letter-spacing: -0.5px;
  }
  .brain__stat-caption {
    margin: 4px 0 0;        /* Figma gap-4 from "2x" value */
    font-weight: 600;       /* Figma "Header/Heading small" = Semibold */
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-secondary);
  }

  /*
   * Source footer — Figma 6003:9681 / 6003:11739.
   * Head row: presentation-01 icon + "Source" text, aligned inline.
   * Below: citation details.
   */
  .source {
    margin-top: 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    color: var(--color-text-secondary);
  }

  /*
   * Trust block — Figma "Frame 296484701" on the goodHands screen
   * (146×107, VERTICAL gap 24): 69×69 logo + Semibold-small caption.
   */
  .trustBlock {
    margin: 32px auto 0;
    width: 146px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 24px;
  }
  .trustBlock__logo {
    width: 69px;
    height: 69px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .trustBlock__logo img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  .trustBlock__caption {
    margin: 0;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.12px;
    color: var(--color-text-secondary);
    text-align: center;
  }
  .source__head {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--color-text-primary);
  }
  .source__head svg {
    width: 20px;
    height: 20px;
    display: block;
    flex-shrink: 0;
    color: var(--color-text-brand);
  }
  .source__title {
    margin: 0;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    color: var(--color-text-primary);
  }
  .source__details {
    margin: 0;
    text-align: center;
    font-weight: 400;
    font-size: 12px;
    line-height: 14px;
    color: var(--color-text-secondary);
  }

  @media (prefers-reduced-motion: reduce) {
    .arc, .legendItem { transition: none; animation: none; opacity: 1; transform: none; }
  }

  /* ─── Auth screen confetti overlay ─────────────────────────── */
  /* Lottie celebration that bursts at the top of the auth ("Your
     plan is ready") screen. The native viewBox is 500×500; we let
     it span the full content width and self-size with aspect-ratio
     so it doesn't push the title down. pointer-events:none keeps it
     fully decorative — clicks/taps go through to whatever's under. */
  .screen[data-screen="auth"] { position: relative; }
  .authConfetti {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: 500px;
    aspect-ratio: 1 / 1;
    pointer-events: none;
    z-index: 1;
  }
  .authConfetti svg {
    width: 100% !important;
    height: 100% !important;
    display: block;
  }
  /* Title stack sits above the confetti so the email card and
     copy stay legible if the animation is mid-play. */
  .screen[data-screen="auth"] .authBody { position: relative; z-index: 2; }
  @media (prefers-reduced-motion: reduce) {
    .authConfetti { display: none; }
  }

  /* ─── Personalization flow (Figma 6272:5957 → 6272:5593) ───── */
  /*
   * 11 screens that sit between Writing (last assessment) and
   * "Ready for a 3-minute test". Six processing screens (image +
   * Lottie overlay) interleaved with five Yes/No question screens.
   *
   * Each screen carries the same in-body progress bar — it never
   * resets, only fills further:
   *   addingAi      0 → 15 ─►  qIUnderstand      15
   *   analyzing    15 → 30 ─►  qIRead            30
   *   vocabulary   30 → 45 ─►  qCannotRemember   45
   *   pronunciation45 → 60 ─►  qSoundNatural     60
   *   mapping      60 → 70 ─►  qNoProgress       70
   *   dialogues    70 → 100 ─►  ready3MinTest (auto)
   *
   * Note: Figma frame 6272:5896 displays "75%" for qNoProgress,
   * but the user spec pinned that checkpoint at 70%. Following the
   * spec — flagged for review.
   */
  /* Layout: title + image flow naturally from the top of the
     screen; the progress bar + counter are pinned 148 px from the
     bottom of the screen (absolute), so they sit in the same place
     regardless of whether Yes/No buttons are present. The action
     overlay (Yes/No) keeps its own fixed bottom-0 position so the
     two zones never collide.

     Do NOT set `display` on `.personaScreen` directly — that would
     override `.screen { display: none }` (equal specificity). The
     base `.screen.active` rule already sets `display: flex`; we
     scope our layout overrides under the .active state below. */
  .screen.personaScreen.active {
    flex-direction: column;
    align-items: center;
    padding: 24px 24px 192px;       /* 172 bar offset + 20 buffer */
    box-sizing: border-box;
    background: var(--color-background-canvas);
    position: relative;             /* anchor for the absolute topBar */
    min-height: 100%;
  }
  /* The fixed action overlay is ≈ 104 px tall (24 padding-top +
     56 button + 24 padding-bottom).  We pin the percent label +
     progress bar 172 px from the screen bottom — that's 104 (action
     height) + 48 (visual gap to the action top) + 20 (recent lift
     to clear the action zone more comfortably on short devices).
     On notched devices `env(safe-area-inset-bottom)` lifts the
     action overlay; the same offset is folded into the bar's
     anchor so the gap is preserved everywhere.  Both the percent
     counter and the bar live inside `.personaTopBar`, so they
     always travel together as one block and never end up under
     the action overlay. */

  /* Pinned zone — animated % label + progress bar. */
  .personaTopBar {
    position: absolute;
    bottom: calc(172px + env(safe-area-inset-bottom, 0px));
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    pointer-events: none;
  }
  .personaPercent {
    margin: 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    color: var(--color-text-primary);
    text-align: center;
  }
  .personaProgress {
    width: 100%;
    max-width: 272px;
    height: 8px;
    border-radius: 4px;
    background: var(--color-background-secondary);
    overflow: hidden;
    flex-shrink: 0;
  }
  .personaProgress__fill {
    height: 100%;
    background: var(--color-text-brand);
    border-radius: 4px;
    /* Linear motion — no easing curve.  Per spec the bar should
       fill at a constant velocity from segment start to target
       (no decelerating end, no accelerating end). */
    transition: width 2400ms linear;
    will-change: width;
  }

  /* Title sits below the top bar with a stable 32 gap. */
  .personaTitle,
  .personaQuestion__title {
    margin: 32px 0 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
    text-align: center;
  }
  .personaTitle           { max-width: 272px; }
  .personaQuestion__title { max-width: 263px; }

  /* All 11 personalization screens (processing + question) now
     render a static 200×200 image positioned 200 px from the top
     of the screen. Absolute positioning keeps the image at exactly
     the same Y-coordinate regardless of how tall the title above
     it wraps, so the layout stays consistent across screens.
     JSON Lottie overlays were removed — same image, no animation. */
  .personaMedia {
    position: absolute;
    top: 200px;
    left: 50%;
    transform: translateX(-50%);
    width: 200px;
    height: 200px;
    flex-shrink: 0;
  }
  .personaMedia__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  /* Question screens use the framed Figma variant (6272:5652):
     150×150 brand-tinted card, rounded 50 px, with the image scaled
     to ~76 % inside it. Both the dimensions and Y-offset (top:225)
     match the design so the % label below stays at the same Y across
     loading and question screens. */
  .personaScreen--q .personaMedia {
    top: 225px;
    width: 150px;
    height: 150px;
    background: var(--color-ring-brand);
    border-radius: 50px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .personaScreen--q .personaMedia__img {
    position: static;
    inset: auto;
    width: 76.21%;          /* matches Figma image-inside-frame ratio */
    height: 76.21%;
    object-fit: contain;
  }

  /* Yes/No action overlay — full-viewport white bg pinned to the
     bottom of the screen, mirroring the project-wide .actionContainer
     pattern. The buttons inside stay constrained to the mobile
     content width. */
  .personaActions {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 10;
    width: 100%;
    background: var(--color-background-primary);
    padding: var(--space-24) var(--space-16) calc(var(--space-24) + env(safe-area-inset-bottom, 0px));
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
  }
  .personaActions__row {
    width: 100%;
    max-width: 343px;
    display: flex;
    gap: 12px;
  }
  .personaCta {
    flex: 1 1 0;
    min-width: 0;
    height: 56px;
    padding: 0 16px;
    border-radius: 20px;
    font-family: inherit;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    cursor: pointer;
    transition: background-color 160ms ease, border-color 160ms ease;
    /* Default = ACTIVE state (question screens) per Figma 6272:5652:
       brand-tinted bg with a subtle white inner border. Loading screens
       opt into the disabled variant via `.personaCta--disabled`. */
    background: var(--color-ring-brand);
    border: 1px solid var(--color-ring-inverse);
    color: var(--color-text-primary);
  }
  /* Hover feedback only on devices that truly hover — on touch
     screens the browser keeps :hover latched on the last-tapped
     element, so the reused Yes/No buttons in the build-plan popup
     carried the previous answer's highlight into the next question. */
  @media (hover: hover) {
    .personaCta:hover {
      background: color-mix(in srgb, var(--color-text-brand) 18%, transparent);
      outline: none;
    }
  }
  .personaCta:focus-visible {
    background: color-mix(in srgb, var(--color-text-brand) 18%, transparent);
    outline: none;
  }
  .personaCta:active {
    background: color-mix(in srgb, var(--color-text-brand) 25%, transparent);
    border-color: var(--color-text-brand);
  }
  /*
   * Final-stage Next button (`mapping` screen) — primary CTA style.
   * When the button flips out of the disabled state at 100 %
   * progress, it must read as the project's main brand CTA, not
   * the softer brand-tinted variant used on the persona Yes/No
   * question screens.  Uses the same `--color-background-brand`
   * token + `--color-text-on-accent` label colour as the shared
   * `.cta` rule, with the matching `:hover` and `:active` press
   * feedback.  Disabled state stays the same as the rest of the
   * persona-flow disabled buttons (ring-inverse / muted), scoped
   * via `:not(.personaCta--disabled)` so we override only the
   * active state.
   */
  .screen[data-screen="mapping"] .personaCta:not(.personaCta--disabled) {
    background: var(--color-background-brand);
    border-color: var(--color-background-brand);
    color: var(--color-text-on-accent);
  }
  .screen[data-screen="mapping"] .personaCta:not(.personaCta--disabled):hover,
  .screen[data-screen="mapping"] .personaCta:not(.personaCta--disabled):focus-visible {
    background: #4f4dc7;
    border-color: #4f4dc7;
    color: var(--color-text-on-accent);
    outline: none;
  }
  .screen[data-screen="mapping"] .personaCta:not(.personaCta--disabled):active {
    transform: scale(var(--press-scale-cta));
  }

  /* Disabled variant — Figma 6272:5957. Loading screens render the
     action overlay so the layout matches the question screens, but
     buttons are visually muted and not interactive. */
  .personaCta--disabled {
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-disabled);
    color: var(--color-text-ghost);
    cursor: not-allowed;
    pointer-events: none;
  }
  .personaCta--disabled:hover,
  .personaCta--disabled:focus-visible,
  .personaCta--disabled:active {
    background: var(--color-ring-inverse);
    border-color: var(--color-border-disabled);
    color: var(--color-text-ghost);
  }
  /*
   * Attention pulse — on the persona question screens (.personaScreen--q),
   * the Yes/No buttons softly fade into the hover colour, fade back to
   * default, then wait ~2 s and pulse again. Loops until the user
   * interacts; pauses cleanly under hover/press so the user's own
   * intent always wins visually.
   *
   * Keyframe layout (total cycle 2800 ms):
   *   0%   – default surface
   *   14%  – peak in hover surface  (~390 ms in)
   *   28%  – back to default        (~780 ms in)
   *   28→100% – static hold (~2 s) before the next pulse
   *
   * Scoped via `.screen.active.personaScreen--q` so the animation
   * only runs while the question screen is on screen — switching
   * away cleanly resets it; coming back replays from 0.
   */
  @keyframes personaCta-pulse {
    0%, 28%, 100% { background-color: var(--color-ring-brand);                                    }
    14%           { background-color: color-mix(in srgb, var(--color-text-brand) 18%, transparent); }
  }
  .screen.active.personaScreen--q .personaCta {
    animation: personaCta-pulse 2800ms cubic-bezier(0.4, 0, 0.2, 1) 600ms infinite;
  }
  /* Hover / focus / press wins — the user's intent overrides the
     auto-pulse so the surface doesn't keep flickering under their
     finger. */
  .screen.active.personaScreen--q .personaCta:hover,
  .screen.active.personaScreen--q .personaCta:focus-visible,
  .screen.active.personaScreen--q .personaCta:active {
    animation-play-state: paused;
  }
  @media (prefers-reduced-motion: reduce) {
    .screen.active.personaScreen--q .personaCta { animation: none; }
  }

  /* ─────────────────────────────────────────────────────────────
     Paywall — premium subtle motion accents.
       1) `.paywallCta` (Get my plan) gets a very light, slow halo
          pulse so the primary action stays visually alive while
          the user reads. Pure box-shadow — keeps the existing
          press scale untouched.
       2) Exam Readiness title + value fade up with a soft bounce
          when the section scrolls into view.
       3) The "What you get with LangArena" benefits list reveals
          row-by-row with a gentle stagger.

     Reveal-on-scroll is driven by a small IntersectionObserver
     (`wirePaywallReveals` in the JS block) which toggles
     `.is-in-view` once an element crosses the viewport threshold.
     ───────────────────────────────────────────────────────────── */
  @keyframes paywallCta-pulse {
    0%, 100% { box-shadow: 0 4px 14px rgba(88, 86, 214, 0.12); }
    50%      { box-shadow: 0 10px 28px rgba(88, 86, 214, 0.34); }
  }
  .paywallCta {
    animation: paywallCta-pulse 2800ms ease-in-out 600ms infinite;
  }
  .paywallCta:hover { animation-play-state: paused; }
  @media (prefers-reduced-motion: reduce) {
    .paywallCta { animation: none; }
  }

  /* Reveal-on-scroll base — invisible until the IO marks the
     element with `.is-in-view`. */
  [data-paywall-reveal] {
    opacity: 0;
    transform: translateY(10px) scale(0.96);
    transition:
      opacity 620ms cubic-bezier(0.34, 1.3, 0.64, 1),
      transform 620ms cubic-bezier(0.34, 1.3, 0.64, 1);
    will-change: opacity, transform;
  }
  [data-paywall-reveal].is-in-view {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  /* Slight extra delay on the readiness value so it lands just
     after the title for a small one-two beat. */
  .readiness__value[data-paywall-reveal] { transition-delay: 120ms; }

  /* Benefits list — parent reveals instantly; children stagger.
     Override the base reveal so the .benefits container itself
     stays in place (it's a positioned wrapper) and only the rows
     animate. */
  .benefits[data-paywall-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .benefits[data-paywall-reveal] .benefit {
    opacity: 0;
    transform: translateX(-12px);
    transition:
      opacity 480ms cubic-bezier(0.34, 1.3, 0.64, 1),
      transform 480ms cubic-bezier(0.34, 1.3, 0.64, 1);
    will-change: opacity, transform;
  }
  .benefits[data-paywall-reveal].is-in-view .benefit {
    opacity: 1;
    transform: translateX(0);
  }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(1) { transition-delay: 0ms;   }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(2) { transition-delay: 80ms;  }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(3) { transition-delay: 160ms; }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(4) { transition-delay: 240ms; }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(5) { transition-delay: 320ms; }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(6) { transition-delay: 400ms; }
  .benefits[data-paywall-reveal].is-in-view .benefit:nth-child(7) { transition-delay: 480ms; }

  /* App-screen slider reveal — container holds; each slide fades/rises
     with a short stagger (mirrors the benefits list). */
  .appSlider[data-paywall-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .appSlider[data-paywall-reveal] .appShot {
    opacity: 0;
    transform: translateY(12px);
    transition:
      opacity 480ms cubic-bezier(0.34, 1.3, 0.64, 1),
      transform 480ms cubic-bezier(0.34, 1.3, 0.64, 1);
    will-change: opacity, transform;
  }
  .appSlider[data-paywall-reveal].is-in-view .appShot { opacity: 1; transform: translateY(0); }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(1) { transition-delay: 0ms;   }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(2) { transition-delay: 70ms;  }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(3) { transition-delay: 140ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(4) { transition-delay: 210ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(5) { transition-delay: 280ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(6) { transition-delay: 350ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(7) { transition-delay: 420ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(8) { transition-delay: 490ms; }
  .appSlider[data-paywall-reveal].is-in-view .appShot:nth-child(9) { transition-delay: 560ms; }

  @media (prefers-reduced-motion: reduce) {
    [data-paywall-reveal],
    .benefits[data-paywall-reveal] .benefit {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

  /* ─────────────────────────────────────────────────────────────
     Final Touches — chat-like reveal between Auth and Paywall.

     Top zone (logo + progress + "Final Touches" title) is `position:
     sticky` against the page scroller — the chat content slides
     under it as the user scrolls / new messages append. Bottom CTA
     uses the project's standard fixed `.actionContainer`.
     ───────────────────────────────────────────────────────────── */
  .screen[data-screen="finalTouches"] {
    /* Anchor sticky children against the screen itself if/when the
       outer page is non-scrolling — falls back gracefully to body
       scrolling when the section is taller than the viewport. */
    position: relative;
  }
  .finalTouches__sticky {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--color-background-canvas);
    width: 100%;
    /* Slight bottom shadow keeps a soft separation from the chat
       content scrolling underneath. Subtle, not a hard divider. */
    box-shadow: 0 6px 8px -8px rgba(57, 60, 70, 0.18);
  }
  .finalTouches__title {
    margin: 0 auto;
    padding: 0 16px 16px;
    text-align: center;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  .finalTouches__chat {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    /* Bottom padding generous enough that the last bubble at rest
       still sits above the fixed `.actionContainer` (≈104 px tall:
       56 px CTA + 24×2 padding) plus a typing-bubble’s height
       (~48 px) and breathing room. */
    padding: 20px 20px 200px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 20px;
  }

  /* Each message — avatar icon (left) + bubble (right). Both pieces
     fade in together when JS appends them. The same entrance keyframe
     replays on the bubble when the typing bubble swaps to a text
     bubble (see `.finalChat__bubble--enter`), so every step in the
     chat has its own gentle fade-in + small rise from below.

     Easing: ease-out-expo — long tail, no bounce, glides into final.
     Duration ~720 ms — roughly 2× slower than before, deliberately
     calm so the conversation reads as confident and controlled. */
  .finalChat__msg {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    gap: 12px;
    opacity: 0;
    transform: translateY(10px);
    animation: finalChat-rise 720ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
    /* Reserve scroll space below each message so when JS calls
       `scrollIntoView({ block: "end" })` on a freshly-appended
       bubble, the browser keeps it well clear of the fixed Next
       CTA (~104 px) and the next typing bubble that follows it. */
    scroll-margin-bottom: 160px;
  }
  .finalChat__bubble--enter {
    /* Apply the same fade + rise on the bubble itself when content
       swaps from typing → text. Class is briefly removed/re-added in
       JS to re-trigger the keyframe. */
    animation: finalChat-rise 720ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
  @keyframes finalChat-rise {
    0%   { opacity: 0; transform: translateY(10px); }
    100% { opacity: 1; transform: translateY(0);    }
  }
  .finalChat__icon {
    width: 24px;
    height: 24px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
    /* Nudge down so the avatar aligns with the bubble's first text
       line rather than its very top edge. */
    margin-top: 6px;
  }
  .finalChat__bubble {
    width: 100%;
    max-width: 335px;
    padding: 16px;
    border-radius: 20px;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    box-sizing: border-box;
  }
  .finalChat__text {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Inline brand-coloured lead phrase used in the bullet-style
     bubbles (Daily Speaking Practice., Pronunciation Mastery., …). */
  .finalChat__lead { color: var(--color-text-brand); }
  /* Inline brand-coloured highlight painted mid-sentence (the target
     score in the "Your target is …" bubble). */
  .finalChat__hl { color: var(--color-text-brand); }

  /* Typing bubble — same surface/border as text bubbles, but a
     fixed compact width with 3 animated dots so it reads as
     "system is preparing the next message". */
  .finalChat__bubble--typing {
    width: 70px;
    min-height: 48px;
    padding: 14px 16px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0;
  }
  .finalChat__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-text-secondary);
    /* Doubled cycle (1.2 s → 2.4 s) so the dot rhythm matches the
       calmer overall pace of the chat reveal. */
    animation: finalChat-typing 2.4s infinite ease-in-out;
  }
  .finalChat__dot:nth-child(2) { animation-delay: 0.30s; }
  .finalChat__dot:nth-child(3) { animation-delay: 0.60s; }
  @keyframes finalChat-typing {
    0%, 60%, 100% { transform: translateY(0);    opacity: 0.45; }
    30%           { transform: translateY(-3px); opacity: 1;    }
  }
  @media (prefers-reduced-motion: reduce) {
    .finalChat__msg { animation: none; opacity: 1; transform: none; }
    .finalChat__dot { animation: none; opacity: 0.7; }
  }

  /* ─────────────────────────────────────────────────────────────
     Paywall FAQ — final accordion section. Light theme — sits on
     the same surface as the rest of Paywall; no dark inversion.
       - Brand-coloured "FAQ" centered title.
       - Single-open accordion (wirePaywallFaq in JS).
       - Question text uses `--color-text-primary`,
         answer text uses `--color-text-secondary`,
         dividers use `--color-border-primary`.
       - Chevron rotates 180° on open; expand/collapse via the
         `grid-template-rows: 0fr → 1fr` trick.
     ───────────────────────────────────────────────────────────── */
  .paywallFaq {
    margin-top: 48px;
    padding: 0;
    background: transparent;
  }
  .paywallFaq__title {
    margin: 0 0 24px;
    text-align: center;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    letter-spacing: 0.32px;
    color: var(--color-text-brand);
  }
  .paywallFaq__list {
    max-width: 343px;
    margin: 0 auto;
  }
  .paywallFaq__item {
    border-bottom: 1px solid var(--color-border-primary);
  }
  .paywallFaq__item:first-child {
    border-top: 1px solid var(--color-border-primary);
  }
  .paywallFaq__head {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 20px 0;
    background: transparent;
    border: none;
    text-align: left;
    cursor: pointer;
    font-family: inherit;
    color: var(--color-text-primary);
  }
  .paywallFaq__q {
    flex: 1 1 auto;
    margin: 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  .paywallFaq__chev {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    color: var(--color-text-secondary);
    transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
                color 180ms ease;
  }
  .paywallFaq__item--open .paywallFaq__chev {
    transform: rotate(180deg);
    color: var(--color-text-brand);
  }
  /* Smooth height animation via grid-template-rows. The wrapper
     is a 1-cell grid; track size goes 0fr → 1fr on open. The
     inner div has overflow: hidden so the row shrinks cleanly. */
  .paywallFaq__answer-wrap {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 320ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  .paywallFaq__item--open .paywallFaq__answer-wrap {
    grid-template-rows: 1fr;
  }
  .paywallFaq__answer {
    overflow: hidden;
    opacity: 0;
    transition: opacity 220ms ease 60ms;
  }
  .paywallFaq__item--open .paywallFaq__answer {
    opacity: 1;
  }
  .paywallFaq__answer p {
    margin: 0;
    padding: 4px 0 20px;
    font-weight: 400;
    font-size: 14px;
    line-height: 22px;
    color: var(--color-text-secondary);
  }
  /* "LangArena.com" inside answer 2 — accent link colour using
     the brand text token, with a soft underline. */
  .paywallFaq__answer a {
    color: var(--color-text-brand);
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  @media (prefers-reduced-motion: reduce) {
    .paywallFaq__chev,
    .paywallFaq__answer-wrap,
    .paywallFaq__answer {
      transition: none;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .personaProgress__fill { transition: none; }
  }

  /* ─── Build-plan — alternative single-screen loader (#buildPlan) ───
     One screen with a labelled progress bar per build step. Each bar
     fills to ~50%, a Yes/No question pops up, then it completes; the
     next step follows. Reuses the success/brand tokens + the persona
     images and the shared .personaCta button.
     NOTE: no `display`/`flex-direction` on `.buildPlan` itself — the
     base `.screen { display: none }` + `.screen.active { display: flex;
     flex-direction: column }` rules already handle show/hide and the
     column layout. A `.buildPlan { display: flex }` here would have
     equal specificity but win on cascade order, leaving the screen
     permanently visible over every other screen. */
  .buildPlan__head {
    padding: 28px 20px 4px;
    text-align: center;
  }
  .buildPlan__title {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  .buildPlan__steps {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    /* Sit close to the title at the top — leaves the lower half of the
       screen free for the question popup. */
    padding: 20px 20px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 20px;
    /* Natural height at the top (right under the title). The
       `.buildPlan__rest` region below takes the remaining space. */
    flex: 0 0 auto;
  }
  /* Fills the space between the last step and the footer, and centers
     the "plan ready" banner within it. Bottom padding reserves the
     fixed footer CTA height so the banner centers between the steps and
     the button — not behind it. */
  .buildPlan__rest {
    flex: 1 1 auto;
    width: 100%;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-bottom: calc(104px + env(safe-area-inset-bottom, 0px));
  }
  .buildPlan__step {
    display: flex;
    align-items: center;
    gap: 14px;
    opacity: 0.4;
    transition: opacity 320ms ease;
  }
  .buildPlan__step.is-active,
  .buildPlan__step.is-done { opacity: 1; }
  .buildPlan__icon {
    width: 52px;
    height: 52px;
    flex-shrink: 0;
    border-radius: 16px;
    background: var(--color-background-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .buildPlan__icon img {
    width: 42px;
    height: 42px;
    object-fit: contain;
    display: block;
  }
  .buildPlan__main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .buildPlan__label {
    margin: 0;
    font-weight: 600;
    font-size: 15px;
    line-height: 20px;
    color: var(--color-text-primary);
  }
  .buildPlan__track {
    width: 100%;
    height: 8px;
    border-radius: 999px;
    background: var(--color-background-secondary);
    overflow: hidden;
  }
  .buildPlan__fill {
    width: 0%;
    height: 100%;
    border-radius: 999px;
    background: var(--color-text-brand);
  }
  .buildPlan__step.is-done .buildPlan__fill {
    background: var(--color-text-success);
  }
  .buildPlan__check {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity 200ms ease, transform 200ms ease;
  }
  .buildPlan__check svg { width: 100%; height: 100%; display: block; }
  .buildPlan__step.is-done .buildPlan__check { opacity: 1; transform: scale(1); }

  /* "Plan ready" banner — sits at the bottom of the content and is
     revealed (fade + slide-up) once the final bar completes. The step
     list (flex: 1) grows above it, pushing this to the bottom. */
  .buildPlan__ready {
    position: relative;          /* anchor for the corner check badge */
    width: calc(100% - 40px);
    max-width: 335px;
    /* Centering is handled by `.buildPlan__rest`. */
    margin: 0 auto;
    padding: 16px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: 14px;
    border-radius: 20px;
    background: var(--color-fill-ring-success);
    opacity: 0;
  }
  .buildPlan__ready[hidden] { display: none; }
  /* Bouncy entrance — fade + slide-up with a scale overshoot and a tiny
     settle, so the banner clearly "pops" as it lands. */
  .buildPlan__ready.is-shown {
    animation: buildReady-pop 560ms cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  @keyframes buildReady-pop {
    0%   { opacity: 0; transform: translateY(20px) scale(0.88); }
    55%  { opacity: 1; transform: translateY(-5px) scale(1.05); }
    78%  { transform: translateY(1px) scale(0.99); }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }
  /* Larger version of a step row: webp (left) + text + check (right). */
  .buildPlan__readyMedia {
    width: 64px;
    height: 64px;
    flex-shrink: 0;
    border-radius: 16px;
    background: var(--color-background-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .buildPlan__readyMedia img {
    width: 56px;
    height: 56px;
    object-fit: contain;
    display: block;
  }
  .buildPlan__readyText {
    margin: 0;
    flex: 1 1 auto;
    min-width: 0;
    /* leave room so the first line never tucks under the corner badge */
    padding-right: 18px;
    font-weight: 700;
    font-size: 19px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Check badge — sits on the top-right corner, half outside the
     container so it "pops" off the edge rather than crowding the text. */
  .buildPlan__readyCheck {
    position: absolute;
    top: -16px;
    right: -16px;
    width: 46px;
    height: 46px;
    display: block;
    opacity: 0;
    transform: scale(0.4);
  }
  .buildPlan__readyCheck svg { width: 100%; height: 100%; display: block; }
  /* Springy pop with overshoot and a tiny wiggle, landing just after the
     banner has settled. */
  .buildPlan__ready.is-shown .buildPlan__readyCheck {
    animation: buildReady-check 480ms cubic-bezier(0.34, 1.56, 0.64, 1) 340ms both;
  }
  @keyframes buildReady-check {
    0%   { opacity: 0; transform: scale(0.2) rotate(-12deg); }
    60%  { opacity: 1; transform: scale(1.35) rotate(4deg); }
    100% { opacity: 1; transform: scale(1) rotate(0deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .buildPlan__ready.is-shown,
    .buildPlan__ready.is-shown .buildPlan__readyCheck {
      animation: none;
      opacity: 1;
      transform: none;
    }
  }

  /* Question popup */
  .buildPlan__modal {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    /* Anchor the card to the bottom so the step bars stay visible
       (dimmed) in the upper part of the screen while a question is up. */
    align-items: flex-end;
    justify-content: center;
    padding: 20px 20px 28px;
  }
  .buildPlan__modal[hidden] { display: none; }
  .buildPlan__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(20, 22, 30, 0.45);
    opacity: 0;
    transition: opacity 200ms ease;
  }
  .buildPlan__modal.is-open .buildPlan__backdrop { opacity: 1; }
  .buildPlan__dialog {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 335px;
    box-sizing: border-box;
    background: var(--color-background-primary);
    border-radius: 24px;
    padding: 24px 20px;
    box-shadow: 0 20px 48px rgba(20, 22, 30, 0.18);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    text-align: center;
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 240ms ease,
      transform 240ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  .buildPlan__modal.is-open .buildPlan__dialog {
    opacity: 1;
    transform: none;
  }
  .buildPlan__qimg {
    width: 140px;
    height: 140px;
    object-fit: contain;
    display: block;
  }
  .buildPlan__qtitle {
    margin: 0;
    font-weight: 700;
    font-size: 18px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  .buildPlan__answers {
    display: flex;
    gap: 12px;
    width: 100%;
  }
  .buildPlan__answers .personaCta { flex: 1 1 0; }
  @media (prefers-reduced-motion: reduce) {
    .buildPlan__fill,
    .buildPlan__backdrop,
    .buildPlan__dialog { transition: none; }
  }


  /* ═════════════════════════════════════════════════════════════
     v3 flow components — targetScore/testDate text options, quiz
     intro facts, insight callout, speaking preview, section pulse,
     wedge, comparison.
     ═══════════════════════════════════════════════════════════ */

  /* Text-only option card (no image slot) — targetScore / testDate. */
  /* Left-aligned content like the whyToefl option cards: a fixed-width
     emoji slot at the left, then the label starting at a consistent x
     across every button. */
  .option--text {
    justify-content: flex-start;
    min-height: 56px;
    padding: 16px;
  }
  .option--text .option__label {
    flex: 1;
    text-align: left;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 14px;
  }
  .option__emoji {
    width: 28px;
    flex-shrink: 0;
    text-align: center;
    font-size: 22px;
    line-height: 1;
  }
  /* Leading webp illustration (testDate options) — matched to the
     whyToefl option image (54px desktop / 48px mobile) so the two
     question screens read consistently. */
  .option__leadImg {
    width: 54px;
    height: 54px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
  }
  @media (max-width: 768px) {
    .option__leadImg {
      width: 48px;
      height: 48px;
    }
  }
  .optionsHint {
    margin: 16px 0 0;
    text-align: center;
    font-weight: 500;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
  }

  /* Quiz intro facts list (ready3MinTest v3). */
  .readyFacts {
    list-style: none;
    margin: 24px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 100%;
    max-width: 343px;
    margin-left: auto;
    margin-right: auto;
  }
  .readyFact {
    position: relative;
    padding-left: 32px;
    font-weight: 500;
    font-size: 16px;
    line-height: 22px;
    color: var(--color-text-primary);
    /* Left-aligned next to the check, like the paywall benefits list —
       the inherited centering floated the text away from the icons. */
    text-align: left;
  }
  .readyFact::before {
    content: "";
    position: absolute;
    left: 0;
    top: 1px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: var(--color-text-success);
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm-1.2 14.4-4.2-4.2 1.7-1.7 2.5 2.5 5.9-5.9 1.7 1.7-7.6 7.6Z'/></svg>");
            mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm-1.2 14.4-4.2-4.2 1.7-1.7 2.5 2.5 5.9-5.9 1.7 1.7-7.6 7.6Z'/></svg>");
    -webkit-mask-size: contain;
            mask-size: contain;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
  }

  /* Quiz insight callout — same card format as the buildPlan
     question dialog: floating rounded card near the bottom over a
     dimmed backdrop (the answered question stays visible above). */
  .quizCallout {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 20px 20px 28px;
  }
  .quizCallout[hidden] { display: none; }
  .quizCallout__backdrop {
    /* Fixed + bled past the top/bottom edges (not absolute/inset:0
       inside the parent) so the dim always covers the strips iOS 26
       Safari leaves when it sizes a fixed inset:0 box to the SMALL,
       toolbar-excluded viewport. The fill is a flat colour, so the
       off-screen overflow is invisible. The card keeps its placement
       from .quizCallout, which stays inset:0. */
    position: fixed;
    top: -15vh;
    bottom: -15vh;
    left: 0;
    right: 0;
    background: rgba(20, 22, 30, 0.45);
    opacity: 0;
    transition: opacity 200ms ease;
    /* Promote to its own GPU layer so the dim fade is composited
       instead of repainting the whole quiz screen behind it every
       frame — fixes the stuttery backdrop animation on iOS Safari. */
    will-change: opacity;
    transform: translateZ(0);
    backface-visibility: hidden;
  }
  .quizCallout.is-open .quizCallout__backdrop { opacity: 1; }
  .quizCallout__card {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 335px;
    box-sizing: border-box;
    background: var(--color-background-primary);
    border-radius: 24px;
    padding: 24px 20px;
    box-shadow: 0 20px 48px rgba(20, 22, 30, 0.18);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 12px;
    opacity: 0;
    transform: translateY(16px) scale(0.97);
    transition: transform 220ms cubic-bezier(0.2, 0.8, 0.3, 1), opacity 200ms ease;
    /* Keep the card on its own compositor layer so the slide+fade (and
       its large drop shadow) animate on the GPU without re-rastering. */
    will-change: transform, opacity;
  }
  .quizCallout.is-open .quizCallout__card {
    opacity: 1;
    transform: none;
  }
  /* Illustration above the title — transparent webp, pops in a beat
     after the card lands (same spring as the wedge icons). */
  .quizCallout__img {
    width: 104px;
    height: 104px;
    object-fit: contain;
    display: block;
  }
  .quizCallout__img[hidden] { display: none; }
  .quizCallout.is-open .quizCallout__img {
    animation: wedgeIcon-pop 460ms cubic-bezier(0.34, 1.45, 0.45, 1) 100ms backwards;
  }
  @media (prefers-reduced-motion: reduce) {
    .quizCallout.is-open .quizCallout__img { animation: none; }
  }
  .quizCallout__title {
    margin: 0;
    font-weight: 700;
    font-size: 18px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Bullet list — bigger type, primary colour (no grey-on-grey),
     brand dot markers, left-aligned for scanability. */
  .quizCallout__list {
    margin: 0;
    padding: 0;
    list-style: none;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 8px;
    text-align: left;
  }
  .quizCallout__list li {
    position: relative;
    padding-left: 18px;
    font-weight: 500;
    font-size: 16px;
    line-height: 23px;
    color: var(--color-text-primary);
  }
  .quizCallout__list li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 8px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--color-text-brand);
  }
  .quizCallout__cta { margin-top: 8px; width: 100%; }
  @media (prefers-reduced-motion: reduce) {
    .quizCallout__backdrop,
    .quizCallout__card { transition: none; }
  }

  /* Speaking preview — mock waveform + feedback chips. */
  /* Lead line directly under the title — the product promise. */
  .spkLead {
    margin: 12px auto 0;
    max-width: 343px;
    text-align: center;
    font-weight: 500;
    font-size: 16px;
    line-height: 23px;
    color: var(--color-text-primary);
  }
  .spkDemo {
    margin: 28px auto 0;
    width: 100%;
    max-width: 343px;
    background: var(--color-background-primary);
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    padding: 24px 20px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18px;
  }
  /* mic | wave | ai row. */
  .spkWaveRow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
  }
  .spkWaveRow__end {
    width: 40px;
    height: 40px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
  }
  .spkWave {
    display: flex;
    align-items: center;
    gap: 4px;
    height: 48px;
  }
  .spkWave span {
    width: 5px;
    border-radius: 3px;
    background: var(--color-text-brand);
    animation: spkWave-bounce 1100ms ease-in-out infinite;
  }
  .spkWave span:nth-child(4n+1) { height: 16px; animation-delay: 0ms;   }
  .spkWave span:nth-child(4n+2) { height: 34px; animation-delay: 140ms; }
  .spkWave span:nth-child(4n+3) { height: 46px; animation-delay: 280ms; }
  .spkWave span:nth-child(4n)   { height: 24px; animation-delay: 420ms; }
  @keyframes spkWave-bounce {
    0%, 100% { transform: scaleY(0.55); }
    50%      { transform: scaleY(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .spkWave span { animation: none; }
  }
  /* Feedback list — one item per row, icon on a shared left rail so
     every correct.webp / x.webp lines up vertically. */
  /* Single caption above the two result columns. */
  .spkDemo__title {
    margin: 0 0 14px;
    text-align: center;
    font-weight: 700;
    font-size: 13px;
    line-height: 17px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--color-text-secondary);
  }
  /* Two result columns (green checks | red x's) — no per-column head. */
  .spkCols {
    display: flex;
    gap: 14px;
    width: 100%;
  }
  .spkCol {
    flex: 1 1 0;
    min-width: 0;
  }
  .spkCol__head {
    margin: 0 0 10px;
    font-weight: 700;
    font-size: 13px;
    line-height: 17px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    /* Header sits above the icon rail; items left-align too (the
       screen otherwise inherits text-align:center). */
    text-align: left;
    padding-left: 28px;
  }
  .spkCol__head--good { color: var(--color-text-success); }
  .spkCol__head--warn { color: #b25e00; }
  .spkList {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  /* Each item is a 2-column grid (icon | text) so the icons form a
     clean left rail and the text wraps within its own column — the
     "Vocabulary range" wrap no longer drags its icon out of line. */
  .spkList__item {
    display: grid;
    grid-template-columns: 20px 1fr;
    column-gap: 8px;
    align-items: start;
    text-align: left;
    font-weight: 600;
    font-size: 14px;
    line-height: 19px;
  }
  .spkList__icon {
    width: 20px;
    height: 20px;
    object-fit: contain;
    display: block;
  }
  .spkList__item--good { color: var(--color-text-success); }
  .spkList__item--warn { color: #b25e00; }

  /* Section pulse — icon + label/tag + 3-segment strength meter per
     row; data-strength on the row drives how many segments fill and
     in which colour (3 green / 2 brand / 1 orange / pending grey). */
  .pulse {
    margin: 28px auto 0;
    width: 100%;
    max-width: 343px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .pulseRow {
    display: flex;
    align-items: center;
    gap: 14px;
    background: var(--color-background-primary);
    border: 1px solid var(--color-border-primary);
    border-radius: 18px;
    padding: 14px 16px;
  }
  .pulseRow__icon {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--color-ring-brand, rgba(88, 86, 214, 0.10));
    color: var(--color-text-brand);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .pulseRow__icon svg { width: 24px; height: 24px; }
  /* Webp illustration variant — artwork carries its own colour, so no
     tinted circle. Sized 54×54 to match the Struggles option icons on
     the very next screen (the 44px circle slot the SVGs used is too
     small for the 3D webp art). */
  .pulseRow__icon--img {
    width: 54px;
    height: 54px;
    background: transparent;
    border-radius: 0;
  }
  .pulseRow__icon--img img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  .pulseRow__main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .pulseRow__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }
  .pulseRow__label {
    font-weight: 600;
    font-size: 16px;
    line-height: 22px;
    color: var(--color-text-primary);
    /* Label takes the remaining width and WRAPS to a second line when a
       long translation (e.g. RU "Техника прохождения теста" / PL
       "Technika egzaminacyjna") would otherwise push the status tag off
       the card's right edge. The tag (below) is pinned and never shrinks. */
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: break-word;
    text-align: left;
  }
  .pulseTag {
    padding: 3px 10px;
    border-radius: 999px;
    font-weight: 600;
    font-size: 12px;
    line-height: 17px;
    white-space: nowrap;
    /* Pinned: never shrink or overflow — the label wraps instead. */
    flex: 0 0 auto;
    text-align: center;
  }
  .pulseTag--good {
    background: var(--color-fill-ring-success, rgba(0, 199, 143, 0.10));
    color: var(--color-text-success);
  }
  .pulseTag--mid {
    background: var(--color-ring-brand, rgba(88, 86, 214, 0.10));
    color: var(--color-text-brand);
  }
  .pulseTag--warn {
    background: rgba(255, 159, 10, 0.12);
    color: #b25e00;
  }
  .pulseTag--pending {
    background: var(--color-background-secondary);
    color: var(--color-text-secondary);
    /* Long pending copy may wrap to two lines instead of overflowing. */
    white-space: normal;
  }
  .pulseMeter {
    display: flex;
    gap: 4px;
  }
  .pulseMeter span {
    flex: 1;
    height: 6px;
    border-radius: 3px;
    background: var(--color-background-secondary);
  }
  .pulseRow[data-strength="3"] .pulseMeter span { background: var(--color-text-success); }
  .pulseRow[data-strength="2"] .pulseMeter span:nth-child(-n+2) { background: var(--color-text-brand); }
  .pulseRow[data-strength="1"] .pulseMeter span:nth-child(1) { background: #f0a23c; }
  .pulseRow[data-strength="pending"] .pulseMeter span {
    background: repeating-linear-gradient(
      -45deg,
      var(--color-background-secondary) 0 4px,
      var(--color-border-primary) 4px 8px
    );
  }
  /* v3 — bars fill in (grow left→right) when the pulse screen opens,
     staggered row by row, then segment by segment. Driven purely by the
     active-step ancestor so it replays on every entry. */
  @keyframes pulseSegFill {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
  #app[data-active-step="sectionPulse"] .pulseMeter span {
    transform-origin: left center;
    animation: pulseSegFill 720ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
    animation-delay: calc(var(--pulseRowDelay, 0ms) + var(--pulseSegDelay, 0ms));
  }
  #app[data-active-step="sectionPulse"] .pulseMeter span:nth-child(1) { --pulseSegDelay: 0ms; }
  #app[data-active-step="sectionPulse"] .pulseMeter span:nth-child(2) { --pulseSegDelay: 150ms; }
  #app[data-active-step="sectionPulse"] .pulseMeter span:nth-child(3) { --pulseSegDelay: 300ms; }
  #app[data-active-step="sectionPulse"] .pulse .pulseRow:nth-of-type(1) { --pulseRowDelay: 200ms; }
  #app[data-active-step="sectionPulse"] .pulse .pulseRow:nth-of-type(2) { --pulseRowDelay: 640ms; }
  #app[data-active-step="sectionPulse"] .pulse .pulseRow:nth-of-type(3) { --pulseRowDelay: 1080ms; }
  #app[data-active-step="sectionPulse"] .pulse .pulseRow:nth-of-type(4) { --pulseRowDelay: 1520ms; }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="sectionPulse"] .pulseMeter span { animation: none; }
  }
  .pulseNote {
    margin: 24px auto 0;
    max-width: 343px;
    text-align: center;
    font-weight: 500;
    font-size: 14px;
    line-height: 20px;
    color: var(--color-text-secondary);
  }

  /* Wedge — one BIG card at a time (icon on top), stepped by the
     Next CTA so each statement gets read. */
  /* All cards share one grid cell (the stack sizes to the tallest),
     so the outgoing card can slide left while the incoming one slides
     in from the right — a real hand-off, not a hard swap. */
  .wedgeStack {
    margin: 28px auto 0;
    width: 100%;
    max-width: 343px;
    display: grid;
  }
  .wedgeBig {
    grid-area: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 16px;
    background: var(--color-background-primary);
    border: 1px solid var(--color-border-primary);
    border-radius: 24px;
    padding: 36px 24px;
    min-height: 280px;
    box-sizing: border-box;
    justify-content: center;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
  }
  .wedgeBig.is-current {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    animation: wedgeBig-in 420ms cubic-bezier(0.2, 0.8, 0.3, 1);
    z-index: 1;
  }
  .wedgeBig.is-leaving {
    visibility: visible;
    pointer-events: none;
    animation: wedgeBig-out 300ms cubic-bezier(0.55, 0, 0.55, 0.2) forwards;
  }
  @keyframes wedgeBig-in {
    from { opacity: 0; transform: translateX(56px) scale(0.95); }
    60%  { opacity: 1; }
    to   { opacity: 1; transform: none; }
  }
  @keyframes wedgeBig-out {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateX(-56px) scale(0.95); }
  }
  /* The illustration pops in a beat after the card lands. */
  .wedgeBig.is-current .wedgeBig__icon--img img {
    animation: wedgeIcon-pop 460ms cubic-bezier(0.34, 1.45, 0.45, 1) 120ms backwards;
  }
  @keyframes wedgeIcon-pop {
    from { opacity: 0; transform: scale(0.4) rotate(-8deg); }
    70%  { opacity: 1; transform: scale(1.08) rotate(2deg); }
    to   { opacity: 1; transform: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .wedgeBig.is-current,
    .wedgeBig.is-leaving,
    .wedgeBig.is-current .wedgeBig__icon--img img { animation: none; }
    .wedgeBig.is-leaving { visibility: hidden; }
  }
  .wedgeBig__icon {
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--color-ring-brand, rgba(88, 86, 214, 0.10));
    color: var(--color-text-brand);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .wedgeBig__icon svg { width: 36px; height: 36px; }
  /* Webp illustration variant — the artwork carries its own colour,
     so no tinted circle behind it. */
  .wedgeBig__icon--img {
    width: 110px;
    height: 110px;
    background: transparent;
    border-radius: 0;
  }
  .wedgeBig__icon--img img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  .wedgeBig__head {
    margin: 0;
    font-weight: 700;
    font-size: 22px;
    line-height: 28px;
    color: var(--color-text-primary);
  }
  .wedgeBig__text {
    margin: 0;
    font-weight: 500;
    font-size: 17px;
    line-height: 25px;
    color: var(--color-text-primary);
  }
  .wedgeBig--accent {
    background: var(--color-fill-ring-success, rgba(0, 199, 143, 0.10));
    border-color: transparent;
  }
  .wedgeBig--accent .wedgeBig__icon:not(.wedgeBig__icon--img) {
    background: var(--color-background-primary);
    color: var(--color-text-success);
  }
  .wedgeBig--accent .wedgeBig__head { color: var(--color-text-success); }

  /* Comparison — LangArena (centre, emphasised: a raised purple tab over
     a soft-green column band with green ticks) vs a standard course
     (right, muted grey crosses). Card is white with a soft shadow. */
  .cmp {
    position: relative;
    /* Extra top margin leaves room for the purple tab to rise above the
       card's top edge (see .cmp__col--brand margin-top). */
    margin: 40px auto 0;
    width: 100%;
    max-width: 343px;
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    /* visible (not hidden) so the raised tab isn't clipped. Safe here:
       the only opaque backgrounds are the purple tab + green band, both
       in the MIDDLE column, so their square corners never collide with
       the card's rounded corners (which sit in the outer columns). */
    overflow: visible;
    background: var(--color-background-primary);
    box-shadow: 0 8px 24px rgba(17, 24, 39, 0.06);
  }
  /* 3-column matrix: row-label | LangArena | Standard course.
     minmax(0, …) — NOT bare fr — so the columns keep identical widths in
     every row; a plain fr honours min-content and would shove columns. */
  .cmp__head,
  .cmp__row {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
  }
  .cmp__head { background: transparent; }
  /* Empty top-left cell (no "Criteria" label). */
  .cmp__corner { background: transparent; }
  /* Standard-course header — muted grey text, no fill. */
  .cmp__col {
    padding: 8px 8px;
    font-weight: 700;
    font-size: 14px;
    line-height: 19px;
    color: var(--color-text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    /* A long single-word header (e.g. PL "Standardowy", DE
       "Standardkurs") is wider than its 1fr column — break it instead
       of letting it spill past the card's right edge. */
    min-width: 0;
    overflow-wrap: break-word;
    hyphens: auto;
  }
  /* LangArena header — raised purple tab with rounded top corners that
     rises ~14px above the card and flows into the green band below. */
  .cmp__col--brand {
    margin-top: -14px;
    z-index: 2;
    color: var(--color-text-on-accent);
    background: var(--color-background-brand);
    border-radius: 16px 16px 0 0;
    font-size: 15px;
    line-height: 19px;
    padding: 12px 6px 10px;
    box-shadow: 0 6px 16px rgba(74, 58, 255, 0.18);
  }
  /* Divider above every benefit row — including the first, so there's a
     line between the header and "Optimized…" matching the rows below. */
  .cmp__row { border-top: 1px solid var(--color-border-primary); }
  /* Row benefit label — left column, vertically centered. */
  .cmp__rowLabel {
    display: flex;
    align-items: center;
    padding: 18px 12px;
    font-weight: 700;
    font-size: 15px;
    line-height: 20px;
    color: var(--color-text-primary);
    min-width: 0;
    overflow-wrap: break-word;
    hyphens: manual;            /* wrap at spaces, never mid-word ("ques-tion") */
    text-align: left;
  }
  .cmp__cell {
    padding: 18px 8px;
    display: flex;
    align-items: center;
    justify-content: center;   /* centre the lone ✓/✗ marker */
  }
  /* LangArena cells — soft-green band running down the emphasised column. */
  .cmp__cell--brand {
    position: relative;
    background: var(--color-fill-ring-success, rgba(0, 199, 143, 0.10));
  }
  /* White disc behind each green tick (matches the attached). */
  .cmp__cell--brand::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: var(--color-background-primary);
    box-shadow: 0 2px 6px rgba(17, 24, 39, 0.08);
    z-index: 0;
  }
  /* Tick / cross markers — inline-mask trick so colours come from tokens. */
  .cmp__tick,
  .cmp__cross {
    position: relative;
    z-index: 1;
    flex-shrink: 0;
    width: 24px;
    height: 24px;
  }
  .cmp__tick {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M9.5496 18L3.8496 12.3L5.2746 10.875L9.5496 15.15L18.7246 5.975L20.1496 7.4L9.5496 18Z'/></svg>");
            mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M9.5496 18L3.8496 12.3L5.2746 10.875L9.5496 15.15L18.7246 5.975L20.1496 7.4L9.5496 18Z'/></svg>");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-size: contain;
            mask-size: contain;
    background-color: var(--color-text-success);
  }
  .cmp__cross {
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M6.4 18.65 5.35 17.6 10.95 12 5.35 6.4 6.4 5.35 12 10.95l5.6-5.6 1.05 1.05L13.05 12l5.6 5.6-1.05 1.05L12 13.05l-5.6 5.6Z'/></svg>");
            mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'><path d='M6.4 18.65 5.35 17.6 10.95 12 5.35 6.4 6.4 5.35 12 10.95l5.6-5.6 1.05 1.05L13.05 12l5.6 5.6-1.05 1.05L12 13.05l-5.6 5.6Z'/></svg>");
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-size: contain;
            mask-size: contain;
    background-color: #c2c7d0;
  }

  /* Wedge card-position dots (1 of 3). */
  .wedgeDots {
    margin-top: 20px;
    display: flex;
    justify-content: center;
    gap: 8px;
  }
  .wedgeDots__dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-border-primary);
    transition: background-color 200ms ease, transform 200ms ease;
  }
  .wedgeDots__dot.is-active {
    background: var(--color-text-brand);
    transform: scale(1.25);
  }

  /* Contain the card slide-in (wedgeBig-in starts at translateX(56px))
     so it can't spawn a document horizontal scrollbar — that scrollbar
     shrank the layout viewport and dropped the fixed action bar ~34px
     ("CTA jumps between steps" on Android Chrome). Scoped to the wedge
     because it's the only screen with this overflow; a global clip would
     cut the paywall's full-bleed carousels. `clip` makes no scroll
     container and leaves vertical scroll untouched. */
  .screen[data-screen="wedge"] { overflow-x: clip; }
  /* Wedge: center the card + dots vertically in the space between the
     title and the (absolutely positioned) Next footer. */
  .screen[data-screen="wedge"] .sbody {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding-bottom: calc(104px + env(safe-area-inset-bottom, 0px));
  }
  .screen[data-screen="wedge"] .wedgeStack { margin-top: auto; }
  .screen[data-screen="wedge"] .wedgeDots  { margin-bottom: auto; }

  /* speakingPreview: same treatment — the analysis-example card sits
     vertically centered in the space between the lead and the Next
     footer (margin:auto centres it on both axes in the flex column). */
  .screen[data-screen="speakingPreview"] .sbody {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding-bottom: calc(104px + env(safe-area-inset-bottom, 0px));
  }
  .screen[data-screen="speakingPreview"] .spkDemo { margin: auto; }
/*
 * preview/css/08-closing.css
 * Ratings, Reviews, Paywall rotator, Offer, Payment, misc late sections.
 * Concatenated into docs/styles.css by scripts/build-docs.sh.
 * Lines correspond to original styles.css L5995–L7458.
 */

  /* ─── Ratings screen (Figma 6383:2407) ─────────────────────
   * Sits between Language and Name. Layout:
   *   • full-width flag ribbon (19 flags × 4 duplicate sets,
   *     scrolls right→left infinitely)
   *   • "140+ countries" headline
   *   • central illustration (raing.webp, 160×160)
   *   • rating row (olive + 125,000+ ratings + olive)
   *
   * Flag ribbon uses the same break-out trick as the University
   * Slider / How-LangArena-Works marquees: position:relative;
   * left:50%; margin-left:-50vw; width:100vw to escape the
   * 375-wide content column and span the entire viewport. */
  /*
   * NOTE on top spacing — the previous version had `padding: 16 16
   * 128`, which (combined with `.ratings__title { margin-top: 16 }`)
   * pushed the headline 32 px below where the language screen's
   * `What's your native language?` title sits.  Removing the top
   * padding here + the title's margin-top below puts both screens'
   * headlines at the same vertical position, so navigating Language
   * → Ratings no longer feels like the title jumps down.
   * The stats block is shifted down by an equal 32 px (see
   * `.ratings__stats { margin-top }`) so it stays visually at the
   * same absolute position as before.
   */
  .ratings {
    width: 100%;
    flex: 1 0 auto;
    padding: 0 16px 128px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  /*
   * Ratings flag marquee — canonical 2-group infinite marquee
   * pattern shared with the Trusted-by `.uniSlider` and review
   * `.reviewsMarquee` carousels on the Paywall.
   *
   * Structure:
   *   .ratingsMarquee              — full-width wrapper (100vw breakout)
   *   └ .ratingsMarquee__viewport  — centred viewport, overflow:hidden,
   *                                   symmetric edge-fade mask
   *     └ .ratingsMarquee__track   — flex row of TWO groups
   *       ├ .ratingsMarquee__group  ← group A: base set of flags
   *       └ .ratingsMarquee__group  ← group B: exact copy of A
   *
   * Animation:
   *   translateX(0) → translateX(-50%)
   *   The track contains exactly two identical groups, so 50 % of
   *   the track width === one group's width down to the pixel.
   *   At the end of the cycle, group B occupies the position group A
   *   started in. The loop is bit-perfect — no jump, no flicker,
   *   no manual reset, no approximation.
   *
   * Centring:
   *   Only the VIEWPORT is centred (`margin: 0 auto` inside the
   *   100vw wrapper). The track is laid out by its animation
   *   transform — it does not participate in layout centring.
   *   The viewport is therefore symmetric within the screen.
   *
   * Fade edges:
   *   The mask gradient is symmetric — identical fade on left
   *   and right. No one-sided fade, no left-edge pinning.
   *
   * Spacing:
   *   Each flag carries `margin-right: 20px` (NOT container `gap`).
   *   Container `gap` only inserts space *between* siblings, which
   *   means the seam between groups would be 20 px short and the
   *   loop would never line up. With per-item margin, the stride
   *   is a uniform 52 px (32 + 20) including the A→B seam.
   *
   * Hover pauses on the viewport; `prefers-reduced-motion` stops
   * the animation entirely.
   */
  /*
   * Wrapper escapes only the parent `.ratings` 16px horizontal
   * padding — NOT the viewport. We deliberately do NOT use the
   * `100vw + margin-left:-50vw` breakout here because the funnel
   * runs inside a centred `.viewport { max-width: 375px }` (or
   * 425/768 px on wider screens), which is itself centred inside a
   * larger browser canvas. A `100vw` wrapper would escape the
   * `.viewport` and bleed onto the surrounding canvas — visually
   * shifting the marquee whenever the browser is wider than the
   * `.viewport`.  By sizing the wrapper to `100% of .ratings + 32 px`
   * (= the viewport's content width), the marquee always fills the
   * "phone screen" edge-to-edge and stays symmetric inside the
   * actual app frame on every screen size.
   */
  .ratingsMarquee {
    margin-left: -16px;
    margin-right: -16px;
    width: calc(100% + 32px);
    margin-top: 16px;
  }
  .ratingsMarquee__viewport {
    width: 100%;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(
      90deg,
      transparent 0%,
      #000 12%,
      #000 88%,
      transparent 100%
    );
            mask-image: linear-gradient(
      90deg,
      transparent 0%,
      #000 12%,
      #000 88%,
      transparent 100%
    );
  }
  .ratingsMarquee__track {
    display: flex;
    width: max-content;
    animation: ratingsMarquee-scroll 40s linear infinite;
    will-change: transform;
  }
  .ratingsMarquee__viewport:hover .ratingsMarquee__track { animation-play-state: paused; }
  .ratingsMarquee__group {
    display: flex;
    flex-shrink: 0;
  }
  .ratingsMarquee__flag {
    width: 32px;
    height: 24px;
    margin-right: 20px;
    flex-shrink: 0;
    border-radius: 4px;
    border: 1px solid var(--color-border-primary);
    overflow: hidden;
    background: var(--color-background-primary);
    display: block;
  }
  .ratingsMarquee__flag img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  @keyframes ratingsMarquee-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .ratingsMarquee__track { animation: none; }
  }
  .ratings__title {
    /* margin-top: 0 so the headline aligns with the language screen's
       `What's your native language?` title (Figma: same baseline at
       the screen transition).  Marquee below stays glued to this
       title via its own `margin-top: 16 px`. */
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
    text-align: center;
  }
  /* Three vertically stacked olive-stat blocks (Figma 6488:7793).
     Replaces the older single rotating typewriter block. Each row
     is the same compose: olive | text column | olive. The text
     column has a fixed width so olive branches stay aligned across
     rows regardless of label length. */
  .ratings__stats {
    /* 24 px below the marquee — pulls the olive-wreath stack up the
       screen so the trailing `Based on average ratings…` disclaimer
       is no longer hidden behind the fixed action bar on shorter
       devices.  Earlier this was 56 px (compensating an upper-block
       lift) which on 812-px-tall mobile frames pushed the disclaimer
       into the action-overlay zone. */
    margin-top: 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Zero gap — the olive branches' bottom-aligned silhouette
       already supplies the only intended visual spacing between
       rows.  Tighter stack matches the updated spec. */
    gap: 0;
  }
  .ratings__stat {
    display: flex;
    align-items: center;
    /* Slightly tighter than before (20 → 14 px) per spec — the
       olive branches sit a bit closer to the text column. */
    gap: 14px;
  }
  .ratings__statText {
    /* Cap container at 180 px so long translations never push the
       composition wider — the fitText utility shrinks the inner
       value/label font down to its declared min when the rendered
       text would otherwise overflow this width.
         flex-shrink: 0 — locks the column at 180 px even if a long
                          rendered label otherwise leaks the row
                          wider than its parent.
         overflow: hidden — final safety guard: if a translation is
                            so long that fitText hits its min-font
                            floor, the trailing edge stays clipped
                            inside the column instead of crashing
                            into the olive branches on either side. */
    width: 180px;
    max-width: 180px;
    flex-shrink: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    color: var(--color-text-primary);
  }
  .ratings__statValue {
    margin: 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    white-space: nowrap;
  }
  .ratings__statLabel {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    white-space: nowrap;
  }
  .ratings__olive {
    width: 56px;
    height: 129px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
    transform-origin: center center;
    will-change: transform, opacity;
  }
  /* Supporting microcopy under the stat block — body/small/emphasize
     (12/14 Semibold, same token spec as `.authFooter__note`),
     centred to match the stat composition.  16 px gap above per
     updated spec (was 32 — pulled tighter against the stats). */
  .ratings__disclaimer {
    margin: 16px 0 0;
    text-align: center;
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.12px;
    color: var(--color-text-secondary);
  }
  /* ─────────────────────────────────────────────────────────────
     Ratings entrance — sequenced per stat row, with overlap.
     For each `.ratings__stat`:
       1. Olive branches fade in close-to-center (translateX inward)
       2. Then ease out to their final flex positions
       3. Text column (both lines together) fades + scales up
     The 3 rows stagger by ~700 ms each — the next row's olives
     start while the previous row's text is still appearing, so
     the sequence reads as a soft cascade instead of stop-go.

     Easing: ease-out-quart (`cubic-bezier(0.22, 1, 0.36, 1)`) —
     long, smooth tail without overshoot.
     ───────────────────────────────────────────────────────────── */
  /* Initial states (only kick in while the screen is active so
     the animation restarts cleanly on re-entry). */
  #app[data-active-step="ratings"] .ratings__stat .ratings__olive,
  #app[data-active-step="ratings"] .ratings__stat .ratings__statText {
    opacity: 0;
  }
  /* Olive entrance — left and right are mirrored. The first child
     of `.ratings__stat` is the left olive; the third is the right. */
  /* Combined fade + spread — both olives in a row run in parallel and
     fade-in happens AT THE SAME TIME as the spread (no intermediate
     keyframe stop, no two-stage motion). */
  @keyframes ratings-olive-in-left {
    0%   { opacity: 0; transform: translateX(34px); }   /* invisible, close to centre */
    100% { opacity: 1; transform: translateX(0);    }   /* visible, in final position */
  }
  @keyframes ratings-olive-in-right {
    0%   { opacity: 0; transform: translateX(-34px); }
    100% { opacity: 1; transform: translateX(0);     }
  }
  /* Text: fade + scale-up from 0.3 → 1 in a single combined motion.
     Both lines (value + label) live inside .ratings__statText so they
     animate together as one composition. */
  @keyframes ratings-text-in {
    0%   { opacity: 0; transform: scale(0.3); }
    100% { opacity: 1; transform: scale(1);   }
  }
  /* Per-row timeline — olive 2500 ms, text 1300 ms, text starts T+300 ms
     after its olives so the wreath frame establishes itself first and
     the text slips into the cleared space. Per-row stagger 400 ms gives
     overlap between rows without a sequential feel. */
  #app[data-active-step="ratings"] .ratings__stat:nth-child(1) .ratings__olive:first-child {
    animation: ratings-olive-in-left 2500ms cubic-bezier(0.22, 1, 0.36, 1) 0ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(1) .ratings__olive:last-child {
    animation: ratings-olive-in-right 2500ms cubic-bezier(0.22, 1, 0.36, 1) 0ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(1) .ratings__statText {
    animation: ratings-text-in 1300ms cubic-bezier(0.22, 1, 0.36, 1) 300ms both;
  }
  /* Stat 2 — starts well before stat 1 settles. */
  #app[data-active-step="ratings"] .ratings__stat:nth-child(2) .ratings__olive:first-child {
    animation: ratings-olive-in-left 2500ms cubic-bezier(0.22, 1, 0.36, 1) 400ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(2) .ratings__olive:last-child {
    animation: ratings-olive-in-right 2500ms cubic-bezier(0.22, 1, 0.36, 1) 400ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(2) .ratings__statText {
    animation: ratings-text-in 1300ms cubic-bezier(0.22, 1, 0.36, 1) 700ms both;
  }
  /* Stat 3 — same pattern, +400 ms further. */
  #app[data-active-step="ratings"] .ratings__stat:nth-child(3) .ratings__olive:first-child {
    animation: ratings-olive-in-left 2500ms cubic-bezier(0.22, 1, 0.36, 1) 800ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(3) .ratings__olive:last-child {
    animation: ratings-olive-in-right 2500ms cubic-bezier(0.22, 1, 0.36, 1) 800ms both;
  }
  #app[data-active-step="ratings"] .ratings__stat:nth-child(3) .ratings__statText {
    animation: ratings-text-in 1300ms cubic-bezier(0.22, 1, 0.36, 1) 1100ms both;
  }
  /* Disclaimer enters last with a soft fade — no scale, no movement,
     so it stays clearly secondary to the stats. */
  #app[data-active-step="ratings"] .ratings__disclaimer {
    opacity: 0;
    animation: ratings-disclaimer-in 800ms cubic-bezier(0.22, 1, 0.36, 1) 1700ms forwards;
  }
  @keyframes ratings-disclaimer-in {
    to { opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="ratings"] .ratings__stat .ratings__olive,
    #app[data-active-step="ratings"] .ratings__stat .ratings__statText,
    #app[data-active-step="ratings"] .ratings__disclaimer {
      animation: none;
      opacity: 1;
      transform: none;
    }
  }
  /* Per-character span used by `wireReviewsTypewriter` — each
     character fades from 0→1 over 220 ms so the "wave" of typing
     looks smooth instead of stamped. Class is reused across the
     paywall reviews stat block ("2+ million users"). */
  .ratings__char {
    display: inline;
    opacity: 0;
    transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  .ratings__char.is-in {
    opacity: 1;
  }
  /* Subtle bounce on the olives the moment a state finishes typing.
     Soft "back" easing with a tiny overshoot — celebratory accent
     but not cartoony. Triggered by adding .ratings__olive--bounce
     in JS; the keyframe ends at scale(1) so the class can be
     removed on animationend. */
  @keyframes ratings-olive-bounce {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.08); }
    100% { transform: scale(1); }
  }
  .ratings__olive--bounce {
    animation: ratings-olive-bounce 600ms cubic-bezier(0.34, 1.3, 0.64, 1);
  }
  @media (prefers-reduced-motion: reduce) {
    .ratings__char {
      transition: none;
      opacity: 1;
    }
    .ratings__olive--bounce { animation: none; }
  }
  /* Width was 146 (sized for the shorter state "ratings"); bumped to
     180 so the longer state "average rating" stays on a single line
     during the typewriter cycle. min-height locks in each line so the
     block doesn't collapse / jump when text is empty between cycles. */
  .ratings__ratingText {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 180px;
    text-align: center;
    color: var(--color-text-primary);
  }
  .ratings__ratingValue {
    margin: 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    min-height: 40px;          /* keep slot height while empty */
    white-space: nowrap;        /* never wrap during typing */
  }
  .ratings__ratingLabel {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    min-height: 32px;
    white-space: nowrap;
  }

  /* ─── Reviews screen (Figma 6292:2962) ────────────────────
   * Sits between Ratings and Name. Two zones:
   *   • full-width testimonial marquee (very slow R→L scroll)
   *   • olive stat block (2+ million / users, static)
   *
   * Marquee uses the same full-viewport break-out trick as the
   * Ratings flag ribbon. Track contains 4 duplicate sets × 6
   * unique cards = 24 total; the keyframe animates -25 % so we
   * loop on a single set boundary with no visible reset. */
  /* Block layout (NOT flex) so the `.reviewsMarquee` 100vw
     breakout calculates correctly — same setup as `.paywallBody`
     wrapping `.howSlider`. With flex + align-items:center the
     marquee was pre-centered, then `left:50% + margin-left:-50vw`
     shifted it again, producing the asymmetric padding the user
     reported. */
  /* Reviews block container — used to be the body of a standalone
     screen with a fixed-bottom action container (hence the 128 px
     bottom padding). It now lives inline inside `.paywallBody`,
     so the heavy bottom-padding is gone; spacing is handled by
     `.paywallReviews` and the surrounding paywall sections. */
  .reviews {
    width: 100%;
    padding: 0;
    box-sizing: border-box;
    display: block;
    overflow: visible;
  }
  .reviewsMarquee {
    position: relative;
    left: 50%;
    margin-left: -50vw;
    width: 100vw;
    max-width: 100vw;
    margin-top: 16px;
  }
  /* Edge fade — same 6 % / 94 % stops as `.howSlider__viewport`
     so both carousels feel identical at the soft viewport edges. */
  .reviewsMarquee__viewport {
    overflow: hidden;
    width: 100%;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
  }
  .reviewsMarquee__track {
    /* Step carousel (driven by wireCenteredCarousels in JS).
       The track is a static flex row; JS animates a translateX
       so that one card sits exactly in the centre of the viewport,
       holds, then eases to the next card. Seamless infinite loop
       via the duplicate-set + snap-back trick. Touch/mouse drag is
       wired on the viewport — pause on hover/drag, resume after. */
    display: flex;
    align-items: stretch;
    gap: 20px;
    width: max-content;
    will-change: transform;
    /* Browser default: don't try to scroll-pan the page on
       horizontal swipe inside the track — we own the gesture. */
    touch-action: pan-y;
    -webkit-user-select: none;
            user-select: none;
  }
  /* 5-dot pagination row — sits in normal flow under the carousel
     inside the paywall reviews section. Centred via auto margins. */
  .reviewsPagination {
    margin: 16px auto 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    pointer-events: none;
  }
  .reviewsPagination__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-text-tertiary);
    transition: background-color 240ms cubic-bezier(0.4, 0, 0.2, 1);
    flex-shrink: 0;
  }
  .reviewsPagination__dot--active {
    background: var(--color-text-brand);
  }
  /* Card geometry from Figma: 283×260, 20-radius, 1px primary
     border, white bg, padding 28 top / 20 sides / 20 bottom. */
  .reviewCard {
    flex-shrink: 0;
    width: 283px;
    /* min-height (not fixed height) so longer translations (e.g. the
       Russian reviews) grow the card instead of overflowing it and
       clipping the author name. The track's `align-items: stretch`
       then levels every card to the tallest one in the row. */
    min-height: 236px;
    padding: 22px 22px 20px;
    box-sizing: border-box;
    background: var(--color-ring-inverse);
    border: 1px solid var(--color-border-primary);
    border-radius: 20px;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;  /* stars pinned to top; name pinned to bottom (below) so both align across cards */
  }
  /* 5-star rating row at the top of each card. */
  .reviewCard__stars {
    display: flex;
    gap: 3px;
    margin-bottom: 14px;
  }
  .reviewCard__star {
    width: 18px;
    height: 18px;
    color: #FFB400;      /* gold fill via currentColor */
    flex-shrink: 0;
  }
  .reviewCard__text {
    margin: 0;
    font-weight: 400;
    font-size: 17px;
    line-height: 25px;
    color: var(--color-text-secondary);
  }
  .reviewCard__author {
    display: flex;
    align-items: center;
    gap: 20px;
    margin-top: auto;      /* pin the name row to the card bottom */
    padding-top: 16px;
  }
  .reviewCard__avatar {
    width: 66px;
    height: 66px;
    border-radius: 20px;
    overflow: hidden;
    background: var(--color-background-primary);
    flex-shrink: 0;
    display: block;
  }
  .reviewCard__avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  /* Initials avatar — used for real reviews that ship without a photo.
     The per-author colour is set inline in the markup. */
  .reviewCard__avatar--initial {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: 700;
    font-size: 28px;
    line-height: 1;
  }
  .reviewCard__meta {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }
  .reviewCard__name {
    margin-top: auto;      /* pin name to the card bottom so names align across cards */
    padding-top: 16px;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Olive stat block — same composition as the Ratings screen
     (olive1 + 32/40 Bold value + 24/32 Bold label + olive2),
     but rendered as static markup (no typewriter cycle). */
  /* Reviews section inside paywall — gives the embedded carousel
     breathing room above and below. */
  .paywallReviews { margin-top: 32px; }
  /* Olive stat block — sits at the bottom of the paywall reviews
     section (formerly its own screen).  Block-level flex with
     `margin: auto` for horizontal centring inside the block-layout
     `.reviews` host. */
  .reviewsStat {
    margin: 32px auto 0;
    display: flex;
    width: fit-content;
    align-items: center;
    gap: 20px;
  }
  .reviewsStat__olive {
    width: 56px;
    height: 129px;
    object-fit: contain;
    flex-shrink: 0;
    display: block;
    transform-origin: center center;
    will-change: transform;
  }
  /* Olive bounce — same easing/keyframe as the Ratings screen so
     both stat blocks feel like the same component. Triggered in JS
     once both value and label have finished typing. */
  @keyframes reviews-olive-bounce {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.08); }
    100% { transform: scale(1); }
  }
  .reviewsStat__olive--bounce {
    animation: reviews-olive-bounce 600ms cubic-bezier(0.34, 1.3, 0.64, 1);
  }
  .reviewsStat__text {
    /* Stretches between the two olive images.  The flex parent
       (`.paywallStatRotator__slot`) is fixed-width with 24-px
       gutters and the olives are flex-shrink:0, so this column
       gets exactly the remaining space.  `min-width: 0` lets it
       shrink below its intrinsic size (default flex behaviour
       would otherwise lock it at the natural text width and push
       the olives off-edge); `overflow: hidden` is the final
       safety guard so a long translation never visually escapes
       into the olive area even if fitText hits its min font. */
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    color: var(--color-text-primary);
  }
  .reviewsStat__value {
    margin: 0;
    font-weight: 700;
    font-size: 32px;
    line-height: 40px;
    min-height: 40px;          /* hold slot height while typing */
    white-space: nowrap;
  }
  .reviewsStat__label {
    margin: 0;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    min-height: 32px;
    white-space: nowrap;
  }
  @media (prefers-reduced-motion: reduce) {
    .reviewsStat__olive--bounce { animation: none; }
  }
  /* ─── Paywall stat rotator ────────────────────────────────────
     Two olive-stat blocks ("2+ million users" / "4.8 average rating")
     occupy the same slot above the review carousel and crossfade by
     swapping in turn. Pure sequential fade — slot A fades in, holds
     2 s, fades out, then slot B does the same.
     Cycle = 2 × (enter + hold + exit) = 2 × (800 + 4000 + 800) = 11200 ms.
     Both slots share one keyframe; the second is offset by 5600 ms so
     its active period (in its own timeline) lines up with slot 1's
     hidden half.
     ───────────────────────────────────────────────────────────── */
  .paywallStatRotator {
    position: relative;
    width: 100%;
    height: 129px;            /* matches the olive height — keeps the carousel below from shifting */
    margin: 0 auto 12px;      /* centred; small gap before the disclaimer */
  }
  /* "Based on average ratings across our products." — sits under the
     rotating rating/user stat, mirrors the welcome/auth disclaimer. */
  .reviewsStat__disclaimer {
    margin: 0 auto 24px;
    text-align: center;
    font-weight: 500;
    font-size: 11px;
    line-height: 14px;
    color: var(--color-text-ghost, #9aa3b5);
  }
  .paywallStatRotator__slot {
    position: absolute;
    inset: 0;
    /* Per spec the olive branches sit 12 px from each edge of the
       375-px viewport; the inner text column stretches between
       them.  Explicit 12-px horizontal padding + `space-between`
       guarantees the olive positions stay symmetric regardless
       of how long the inner translation is. */
    padding: 0 12px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    opacity: 0;               /* keyframes will reveal */
  }
  @keyframes paywallStatRotate {
    /* % stops:
         0     → invisible (start)
         7.14% → fully in   (800 ms / 11200 ms)
         42.86%→ end of hold (4800 ms)
         50%   → invisible (after fade-out, 5600 ms)
         100%  → invisible (other slot's turn) */
    0%     { opacity: 0; }
    7.14%  { opacity: 1; }
    42.86% { opacity: 1; }
    50%    { opacity: 0; }
    100%   { opacity: 0; }
  }
  #app[data-active-step="paywall"] .paywallStatRotator__slot {
    animation: paywallStatRotate 11200ms linear infinite;
  }
  #app[data-active-step="paywall"] .paywallStatRotator__slot--2 {
    animation-delay: 5600ms;  /* wait for slot 1 to finish before turning on */
  }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="paywall"] .paywallStatRotator__slot { animation: none; }
    /* Show the first state only when motion is reduced. */
    #app[data-active-step="paywall"] .paywallStatRotator__slot--1 { opacity: 1; }
  }

  /* ─────────────────────────────────────────────────────────────
     "You are in good hands" — Figma 6276:2504.
     Single absolute-positioned canvas (375 wide, ~480 tall) so
     the experts photo, the venn-circle decoration, the central
     logo sign and the two icon cards all line up exactly the
     way the design specifies, regardless of font line-height.
     ───────────────────────────────────────────────────────────── */
  /* Section title — sits between the header and the venn
     composition. Same Bold 24/32 cadence as the rest of the
     funnel headings. */
  .goodHands__title {
    margin: 0 auto 16px;
    padding: 0 16px;
    text-align: center;
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.24px;
    color: var(--color-text-primary);
  }
  .goodHands {
    position: relative;
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    height: 480px;
    flex: 1 0 auto;
    overflow: visible;
  }
  .goodHands__circles {
    position: absolute;
    top: 73px;
    left: 0;
    right: 0;
    height: 373px;
    pointer-events: none;
  }
  /* Three lavender venn circles. Fill from Figma node 6288:2787 —
     Linear Gradient, both stops #C9C5FF, α 60% → 0%. The gradient
     is vertically flipped (0deg = bottom-to-top), and the whole
     element renders at 40% opacity for the soft venn read.
     Side circles inherit the mirrored gradient automatically
     because their final state is rotate(180deg). */
  .goodHands__circle {
    position: absolute;
    width: 253px;
    height: 253px;
    border-radius: 50%;
    background: linear-gradient(
      0deg,
      rgba(201, 197, 255, 0.60) 0%,
      rgba(201, 197, 255, 0.00) 100%
    );
    opacity: 0.4;
    will-change: transform;
  }
  /* Static positions per Figma 6276:2504. The side circles keep
     `rotate(180deg)` only so their gradient mirrors against the
     top one (the gradient stops are vertical-flipped relative to
     the top circle's natural orientation). */
  .goodHands__circle--top {
    top: 0;
    left: 50%;
    margin-left: -126.5px;
  }
  .goodHands__circle--bl,
  .goodHands__circle--br {
    transform: rotate(180deg);
  }
  .goodHands__circle--bl { top: 120px; left: 0;  }
  .goodHands__circle--br { top: 119px; right: 0; }

  /* Circles fade in to their resting opacity (0.4). Rotation
     removed per spec — circles stay static in their final
     positions defined above.

     Reveal sequence on screen entry is strictly:
       Logo (renders immediately) → main content (experts photo,
       caption, sign, cards, labels — see delays below) → circles
       LAST, as the decorative layer.
     The 2200 ms delay puts the circle fade right after the last
     card text settles (~1700 ms + 700 ms text-fade = 2400 ms),
     so brand + content are fully on-screen before the venn
     paints in. */
  #app[data-active-step="goodHands"] .goodHands__circle--top,
  #app[data-active-step="goodHands"] .goodHands__circle--bl,
  #app[data-active-step="goodHands"] .goodHands__circle--br {
    /* `both` fill mode ⇒ the 0 % state (opacity: 0) applies during
       the 2200 ms delay, otherwise the static `opacity: 0.4` base
       would peek through before the fade actually starts. */
    animation: goodHands-fade 1200ms cubic-bezier(0.4, 0, 0.2, 1) 2200ms both;
  }
  @keyframes goodHands-fade {
    0%   { opacity: 0;   }
    100% { opacity: 0.4; }
  }
  .goodHands__experts {
    position: absolute;
    top: 73px;
    left: 50%;
    transform: translateX(-50%) scale(1);
    width: 160px;
    height: 70px;
    object-fit: contain;
    z-index: 2;
  }
  .goodHands__caption {
    position: absolute;
    top: 146px;
    left: 50%;
    transform: translateX(-50%);
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-align: center;
    white-space: nowrap;
    z-index: 2;
  }
  .goodHands__caption span { display: block; }
  .goodHands__sign {
    position: absolute;
    top: 235px;
    left: 50%;
    transform: translateX(-50%) scale(1);
    width: 76px;
    height: 76px;
    z-index: 3;
  }
  .goodHands__card {
    position: absolute;
    top: 322px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    z-index: 2;
    /* Cap card width at 130 px so the inner label can wrap to a
       second line (white-space: normal on .goodHands__card p) but
       the card never grows wider than the design.  fitText with
       data-fit-text-lines="2" further shrinks the font if even
       two lines would overflow this width. */
    max-width: 130px;
  }
  .goodHands__card--left  { left: 38px;  }
  .goodHands__card--right { right: 38px; }
  .goodHands__card img {
    width: 54px;
    height: 54px;
    object-fit: contain;
    display: block;
    transform: scale(1);
  }
  .goodHands__card p {
    margin: 0;
    font-weight: 600;
    font-size: 16px;
    line-height: 24px;
    color: var(--color-text-primary);
    text-align: center;
    /* Allow up to two lines for translated labels (Adaptive
       Platform / etc.).  When the text is short enough it stays
       on one line; when it isn't, it wraps to a second line.  If
       even two lines overflow, the fitText utility shrinks the
       font down to its declared minimum (data-fit-text-lines="2"
       on the Adaptive Platform <p>). */
    white-space: normal;
    overflow-wrap: break-word;
  }
  /* Text labels fade in as a whole (no per-char typewriter) — same
     fade keyframe used for the rest of the screen elements. */
  #app[data-active-step="goodHands"] .goodHands__caption {
    animation: goodHands-textFade 700ms cubic-bezier(0.33, 0, 0.2, 1) 600ms both;
  }
  #app[data-active-step="goodHands"] .goodHands__card--left p {
    animation: goodHands-textFade 700ms cubic-bezier(0.33, 0, 0.2, 1) 1500ms both;
  }
  #app[data-active-step="goodHands"] .goodHands__card--right p {
    animation: goodHands-textFade 700ms cubic-bezier(0.33, 0, 0.2, 1) 1700ms both;
  }
  @keyframes goodHands-textFade {
    0%   { opacity: 0; }
    100% { opacity: 1; }
  }

  /* Images scale 0.7 → 1.0 with an opacity ramp on screen entry.
     Staggered: experts photo first, then the centre logo sign over
     the venn intersection, then the two bottom-row card images. */
  #app[data-active-step="goodHands"] .goodHands__experts {
    animation: goodHands-experts-in 1100ms cubic-bezier(0.33, 0, 0.2, 1) 200ms both;
  }
  #app[data-active-step="goodHands"] .goodHands__sign {
    animation: goodHands-sign-in 1100ms cubic-bezier(0.33, 0, 0.2, 1) 800ms both;
  }
  #app[data-active-step="goodHands"] .goodHands__card--left img {
    animation: goodHands-cardImg-in 1100ms cubic-bezier(0.33, 0, 0.2, 1) 1300ms both;
  }
  #app[data-active-step="goodHands"] .goodHands__card--right img {
    animation: goodHands-cardImg-in 1100ms cubic-bezier(0.33, 0, 0.2, 1) 1500ms both;
  }
  @keyframes goodHands-experts-in {
    0%   { opacity: 0; transform: translateX(-50%) scale(0.7); }
    100% { opacity: 1; transform: translateX(-50%) scale(1.0); }
  }
  @keyframes goodHands-sign-in {
    0%   { opacity: 0; transform: translateX(-50%) scale(0.7); }
    100% { opacity: 1; transform: translateX(-50%) scale(1.0); }
  }
  @keyframes goodHands-cardImg-in {
    0%   { opacity: 0; transform: scale(0.7); }
    100% { opacity: 1; transform: scale(1.0); }
  }

  /* Reduced-motion: snap straight to the final composition. */
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="goodHands"] .goodHands__circle--top,
    #app[data-active-step="goodHands"] .goodHands__circle--bl,
    #app[data-active-step="goodHands"] .goodHands__circle--br,
    #app[data-active-step="goodHands"] .goodHands__experts,
    #app[data-active-step="goodHands"] .goodHands__sign,
    #app[data-active-step="goodHands"] .goodHands__card--left img,
    #app[data-active-step="goodHands"] .goodHands__card--right img,
    #app[data-active-step="goodHands"] .goodHands__caption,
    #app[data-active-step="goodHands"] .goodHands__card--left p,
    #app[data-active-step="goodHands"] .goodHands__card--right p {
      animation: none;
      opacity: 1;
    }
    .goodHands__circle--top,
    .goodHands__circle--bl,
    .goodHands__circle--br { opacity: 0.4; }
  }

  /* ─────────────────────────────────────────────────────────────
     Reusable "bouncy" image entrance.
     Applied to the hero image of all of these screens, identically:
       Ratings  · Did-You-Know · Difficulty Adaptation
       Updated Writing · New Speaking · Shorter Test · Evaluate
       Lack of Practice · Pronunciation · Fear of Speaking
       Ready-for-3-min-test
     Pattern: scale 0.70 → 1.06 → 1.00 with a single soft overshoot,
     650 ms `cubic-bezier(0.4, 0, 0.2, 1)`, no fade as a primary
     effect (a brief 0→1 ramp only covers the first frames).
     ───────────────────────────────────────────────────────────── */
  @keyframes bouncyImageEnter {
    0%   { opacity: 0; transform: scale(0.70); }
    60%  { opacity: 1; transform: scale(1.06); }
    100% { opacity: 1; transform: scale(1.00); }
  }
  #app[data-active-step="ratings"]              .ratings__hero img,
  #app[data-active-step="didYouKnow"]           .dyk__hero img,
  #app[data-active-step="difficultyAdaptation"] .sbody__image img,
  #app[data-active-step="updatedWriting"]       .sbody__image img,
  #app[data-active-step="newSpeaking"]          .sbody__image img,
  #app[data-active-step="shorterTest"]          .sbody__image img,
  #app[data-active-step="lackOfPractice"]       .sbody__image img,
  #app[data-active-step="pronunciation"]        .sbody__image img,
  #app[data-active-step="fearOfSpeaking"]       .sbody__image img,
  #app[data-active-step="ready3MinTest"]        .readyImage   img,
  /* Personalization flow — same bouncy entrance on every persona image
     so the 11 200×200 illustrations feel consistent with the rest of
     the project. Targeted via active-step so it re-triggers each time
     the user lands on the screen. */
  #app[data-active-step="addingAi"]              .personaMedia__img,
  #app[data-active-step="qIUnderstand"]          .personaMedia__img,
  #app[data-active-step="analyzing"]             .personaMedia__img,
  #app[data-active-step="qIRead"]                .personaMedia__img,
  #app[data-active-step="vocabulary"]            .personaMedia__img,
  #app[data-active-step="qCannotRemember"]       .personaMedia__img,
  #app[data-active-step="pronunciationTraining"] .personaMedia__img,
  #app[data-active-step="qSoundNatural"]         .personaMedia__img,
  #app[data-active-step="mapping"]               .personaMedia__img,
  #app[data-active-step="qNoProgress"]           .personaMedia__img,
  #app[data-active-step="dialogues"]             .personaMedia__img {
    transform-origin: center center;
    animation: bouncyImageEnter 650ms cubic-bezier(0.4, 0, 0.2, 1) both;
    will-change: transform;
  }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="ratings"]              .ratings__hero img,
    #app[data-active-step="didYouKnow"]           .dyk__hero img,
    #app[data-active-step="difficultyAdaptation"] .sbody__image img,
    #app[data-active-step="updatedWriting"]       .sbody__image img,
    #app[data-active-step="newSpeaking"]          .sbody__image img,
    #app[data-active-step="shorterTest"]          .sbody__image img,
    #app[data-active-step="lackOfPractice"]       .sbody__image img,
    #app[data-active-step="pronunciation"]        .sbody__image img,
    #app[data-active-step="fearOfSpeaking"]       .sbody__image img,
    #app[data-active-step="ready3MinTest"]        .readyImage   img,
    #app[data-active-step="addingAi"]              .personaMedia__img,
    #app[data-active-step="qIUnderstand"]          .personaMedia__img,
    #app[data-active-step="analyzing"]             .personaMedia__img,
    #app[data-active-step="qIRead"]                .personaMedia__img,
    #app[data-active-step="vocabulary"]            .personaMedia__img,
    #app[data-active-step="qCannotRemember"]       .personaMedia__img,
    #app[data-active-step="pronunciationTraining"] .personaMedia__img,
    #app[data-active-step="qSoundNatural"]         .personaMedia__img,
    #app[data-active-step="mapping"]               .personaMedia__img,
    #app[data-active-step="qNoProgress"]           .personaMedia__img,
    #app[data-active-step="dialogues"]             .personaMedia__img {
      animation: none;
      transform: scale(1);
      opacity: 1;
    }
  }

  /* ─────────────────────────────────────────────────────────────
     Welcome screen entrance.
     Both text lines (heading + subtitle) animate in with a soft
     bounce/balance — subtle, not cartoony. The chart wrapper gets
     its own enter (matching motion) and a final balance accent
     after the arcs finish filling, so the sequence reads as:
       1) chart appears,
       2) arcs fill (existing 1100 ms transition driven by JS),
       3) gentle balance accent on the chart wrapper.
     ───────────────────────────────────────────────────────────── */
  @keyframes welcomeTextEnter {
    0%   { opacity: 0; transform: translateY(8px) scale(0.94); }
    65%  { opacity: 1; transform: translateY(0)   scale(1.03); }
    100% { opacity: 1; transform: translateY(0)   scale(1.00); }
  }
  @keyframes welcomeChartEnter {
    0%   { opacity: 0; transform: scale(0.85); }
    65%  { opacity: 1; transform: scale(1.04); }
    100% { opacity: 1; transform: scale(1.00); }
  }
  @keyframes welcomeChartAccent {
    0%   { transform: scale(1.00); }
    35%  { transform: scale(1.04); }
    65%  { transform: scale(0.985); }
    100% { transform: scale(1.00); }
  }
  #app[data-active-step="welcome"] .heading {
    animation: welcomeTextEnter 600ms cubic-bezier(0.4, 0, 0.2, 1) 100ms both;
  }
  #app[data-active-step="welcome"] .subtitle {
    animation: welcomeTextEnter 600ms cubic-bezier(0.4, 0, 0.2, 1) 240ms both;
  }
  #app[data-active-step="welcome"] .chartWrap {
    transform-origin: center center;
    animation:
      welcomeChartEnter  700ms cubic-bezier(0.4, 0, 0.2, 1)  350ms both,
      welcomeChartAccent 600ms cubic-bezier(0.4, 0, 0.2, 1) 1900ms both;
  }
  @media (prefers-reduced-motion: reduce) {
    #app[data-active-step="welcome"] .heading,
    #app[data-active-step="welcome"] .subtitle,
    #app[data-active-step="welcome"] .chartWrap {
      animation: none;
      opacity: 1;
      transform: none;
    }
  }

  /* ─────────────────────────────────────────────────────────────
     Pin the rating / review stat module exactly 412 px below the
     top of the screen, on both Rating and Review screens. The
     anchor is the screen container itself — the position is
     independent of header height, marquee height, or any flex
     layout above. Both screens use absolute positioning so the
     module never shifts due to differing content stacks.
     ───────────────────────────────────────────────────────────── */
  .screen[data-screen="ratings"] { position: relative; }

  /* ─── Offer screen — Figma 6573:12935 ────────────────────────────
   * Reveals the unlocked 50 % discount.  Stacked column with
   * 46-px gap between the visual cluster (header → title → image →
   * unlocked copy) and the limited-time badge underneath.  Bottom
   * action container = standard project-wide pattern.
   * ----------------------------------------------------------- */
  .screen[data-screen="offer"] {
    background: var(--color-background-canvas);
    min-height: 100%;
  }
  .screen[data-screen="offer"] .offerBody {
    width: 100%;
    max-width: 375px;
    margin: 0 auto;
    padding: 0 16px 128px;       /* room for fixed actionContainer */
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }
  .offerHeader {
    width: 100%;
    /* Header reserves 80 px of vertical space.  Combined with the
       parent `.offerBody { gap: 20px }` flex gap to the next child
       (the title), the title's top edge sits 80 + 20 = 100 px from
       the screen's top edge — matches spec exactly. */
    height: 80px;
    padding: 24px 0 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
  }
  .offerTitle {
    margin: 0;
    width: 343px;
    max-width: 100%;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.01em;
    color: var(--color-text-primary);
    text-align: center;
  }
  /* Visual block — 200×200 frame.  offer.webp sits at object-fit:
     contain (so the artwork is never cropped), the Lottie overlay
     is absolutely positioned over the entire frame so the confetti
     shower covers the whole image area. */
  .offerVisual {
    position: relative;
    width: 200px;
    height: 200px;
  }
  .offerVisual__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    pointer-events: none;
  }
  /* Confetti overlay sits centred on the 250×250 image but spans
     375×375 so the burst overflows the picture boundary (per
     Figma the confetti is bigger than the artwork).  pointer-
     events:none so the larger box never blocks the underlying
     CTA. */
  .offerVisual__anim {
    position: absolute;
    width: 285px;
    height: 285px;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 1;
  }
  .offerVisual__anim svg {
    width: 100% !important;
    height: 100% !important;
    display: block;
  }
  /* "You've unlocked" + "50% off" — sits exactly 20 px below the
     image visual via `.offerBody { gap: 20px }`.  No additional
     margin: total visual → unlock distance = 20 px (per spec). */
  .offerUnlock {
    margin-top: 0;
    width: 343px;
    max-width: 100%;
    text-align: center;
  }
  .offerUnlock__sub {
    margin: 0;
    font-family: var(--font-family-base);
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    letter-spacing: 0.01em;
    color: var(--color-text-primary);
  }
  .offerUnlock__pct {
    margin: 0;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 40px;
    line-height: 48px;
    letter-spacing: -0.005em;
    color: #00c78f;              /* Figma literal mint accent */
  }
  /* "limited-time" pill — secondary surface, brand border, info-blue
     label.  Clock-icon glyph reuses the paywall timer SVG path. */
  .offerBadge {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px 8px 8px;
    border-radius: var(--radius-medium);
    background: var(--color-background-secondary);
    border: 1px solid var(--color-border-primary);
  }
  .offerBadge__icon {
    width: 24px;
    height: 24px;
    color: var(--color-text-info);
    flex-shrink: 0;
  }
  .offerBadge__label {
    font-family: var(--font-family-base);
    font-weight: 600;
    font-size: 14px;
    line-height: 16px;
    color: var(--color-text-info);
    opacity: 0.8;
    white-space: nowrap;
  }

  /* ─── Payment screen — Figma 6570:12040 (1:1) ────────────────────
   * Final conversion step that follows the Paywall.  This iteration
   * is UI-only (no Stripe.js / no backend); the card panel is a
   * visual fallback whose `[data-stripe-mount]` wrapper is the
   * future Stripe Elements mount target.
   *
   * Token mapping → Figma:
   *   --color-ring-secondary           Ring/secondary
   *   --color-background-canvas        Background/canvas
   *   --color-background-primary       Background/primary
   *   --color-background-brand         Background/brand
   *   --color-border-primary           Border/primary
   *   --color-text-primary             Text/primary
   *   --color-text-secondary           Text/secondary
   *   --color-text-on-accent           Text/onAccent
   * ----------------------------------------------------------- */
  .screen[data-screen="payment"] {
    background: var(--color-background-canvas);
    min-height: 100%;
  }
  .screen[data-screen="payment"] .paymentBody {
    width: 100%;
    max-width: 343px;
    margin: 0 auto;
    /* Reserve room at the bottom so the fixed .actionContainer
       (≈ 104 px tall: 24 + 56 + 24) doesn't hide the disclaimer. */
    padding: 0 0 120px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
  }
  /* 1. Header — left spacer + close X.  The 28-px top padding
       matches Figma's pt-[28px] on the "Word Count Info" frame. */
  .paymentHeader {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: 28px;
  }
  .paymentHeader__spacer {
    /* Empty 44×14 placeholder per Figma — keeps the X on the
       right edge while preserving the original frame width. */
    width: 44px;
    height: 14px;
    display: inline-block;
  }
  .paymentHeader__close {
    width: 24px;
    height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: transparent;
    color: var(--color-text-brand);
    cursor: pointer;
    padding: 0;
    transition: var(--press-transition), opacity 150ms ease;
  }
  .paymentHeader__close:hover { opacity: 0.7; }
  .paymentHeader__close:active { transform: scale(var(--press-scale-small)); }
  .paymentHeader__close svg { width: 20px; height: 20px; display: block; }
  /* 2. Title — Header/Heading large (24/32 Bold) */
  .paymentTitle {
    margin: 0;
    width: 325px;
    max-width: 100%;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 24px;
    line-height: 32px;
    letter-spacing: 0.01em;
    color: var(--color-text-primary);
    text-align: center;
  }
  /* 3. Word-count + price row.  Two columns split across 343 px,
       inner horizontal padding 12 px to match Figma. */
  .paymentTotal {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    box-sizing: border-box;
  }
  /* Total row — prominent (per design mock): bold 18px for the label
     and the charged price, on every plan. */
  .paymentTotal__words {
    font-weight: 700;
    font-size: 18px;
    line-height: 24px;
    letter-spacing: 0.01em;
    color: var(--color-text-primary);
  }
  .paymentTotal__priceGroup {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
  }
  .paymentTotal__old {
    font-weight: 600;
    font-size: 18px;
    line-height: 24px;
    color: var(--color-text-secondary);
    text-decoration: line-through;
    text-decoration-skip-ink: none;
  }
  .paymentTotal__new {
    font-weight: 700;
    font-size: 18px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* Weekly-trial second line — "1 week trial, monthly after: <monthly
     price>". Secondary to the total above: grey like the struck old
     price (without the strike), clearly smaller than the 18px total.
     The negative margin cancels most of .paymentBody's 16px flex gap
     so the line hugs the total row (≈4px visual gap, per mock). */
  .paymentTotal--after {
    margin-top: -12px;
  }
  .paymentTotal--after[hidden] { display: none; }
  .paymentTotal__afterLabel,
  .paymentTotal__afterPrice {
    font-weight: 600;
    font-size: 12px;
    line-height: 16px;
    color: var(--color-text-secondary);
  }
  /* Long localized labels (ru/uk run ~45 chars) wrap while the price
     stays pinned right. */
  .paymentTotal__afterLabel { padding-right: 12px; }
  .paymentTotal__afterPrice { flex-shrink: 0; }
  /* 4. Lead — Header/Heading (20/24 Semibold), centred */
  .paymentLead {
    margin: 0;
    width: 100%;
    text-align: center;
    font-family: var(--font-family-base);
    font-weight: 600;
    font-size: 20px;
    line-height: 24px;
    color: var(--color-text-primary);
  }
  /* 5. Express + card group — gap 8 px per Figma */
  .paymentMethods {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  /* Pre-warm layout for the payment screen. While the checkout
     pre-mounts in the background (from the paywall — see
     integration/checkout.js), the screen needs REAL layout so
     Stripe's iframes can measure themselves: mounted inside
     `display:none` they paint at 0 height and never recover. The
     screen renders at viewport size but invisible, non-interactive
     and behind everything. The `:not(.active)` guard means
     activating the screen neutralizes this override instantly, even
     before JS removes the class. */
  .screen--prewarm:not(.active) {
    display: flex;
    position: fixed;
    inset: 0;
    overflow: hidden;
    visibility: hidden;
    pointer-events: none;
    z-index: -1;
  }

  /* — Express wallets row (Apple Pay / Google Pay) — Stripe's
       Express Checkout Element renders + styles its own buttons
       inside [data-express-checkout]; the container only needs to
       span the column. — */
  .expressWallets {
    width: 100%;
  }
  .expressWallets[hidden] { display: none; }
  /* — Card panel (Figma 6571:12634, 376 × 343, radius 8.196) — */
  .cardPanel {
    width: 100%;
    background: var(--color-background-primary);
    border: 1px solid var(--color-border-primary);
    border-radius: 8.196px;
    padding: 18px 24px 24px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .cardPanel__title {
    margin: 0 0 4px;
    font-family: var(--font-family-base);
    font-weight: 700;
    font-size: 16px;
    line-height: 25px;
    letter-spacing: -0.01em;
    color: var(--color-text-primary);
  }
  .cardPanel__field {
    display: flex;
    flex-direction: column;
    gap: 7px;
    flex: 1 1 0;
    min-width: 0;
  }
  .cardPanel__label {
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.01em;
    color: var(--color-text-secondary);
  }
  .cardPanel__input {
    height: 41px;
    background: var(--color-ring-secondary);
    border: 1px solid var(--color-border-primary);
    border-radius: 4px;
    padding: 0 16px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    transition: border-color 150ms ease;
  }
  .cardPanel__input:focus-within {
    border-color: var(--color-border-brand);
  }
  .cardPanel__input input,
  .cardPanel__input select {
    width: 100%;
    border: 0;
    background: transparent;
    outline: none;
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    color: var(--color-text-primary);
    padding: 0;
    appearance: none;
    -webkit-appearance: none;
  }
  .cardPanel__input input::placeholder { color: var(--color-text-primary); }
  .cardPanel__input select {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'><path d='M3 4.5L6 7.5L9 4.5' stroke='%236e7f97' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0 center;
    padding-right: 18px;
    cursor: pointer;
  }
  .cardPanel__row {
    display: flex;
    gap: 12px;
  }
  .cardPanel__note {
    margin: 4px 0 0;
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.01em;
    color: var(--color-text-secondary);
  }
  /* 6. Secured row */
  .paymentSecured {
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }
  .paymentSecured__icon { width: 24.6px; height: 24.6px; display: block; }
  .paymentSecured__label {
    font-family: var(--font-family-base);
    font-weight: 600;
    font-size: 12px;
    line-height: 14px;
    letter-spacing: 0.01em;
    color: var(--color-text-secondary);
  }
  /* 7. Brand-badges row — gap 16, native badge sizes (no plate) */
  .screen[data-screen="payment"] .paymentBadges {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
  }
  .screen[data-screen="payment"] .paymentBadge {
    display: block;
    flex: 0 0 auto;
    width: 40px;
    height: 27px;
    object-fit: contain;
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 0;
    overflow: visible;
  }
  .paymentBadge--jcb { line-height: 0; }
  /* 8. Auto-renew disclaimer */
  .paymentDisclaimer {
    margin: 4px 0 0;
    width: 100%;
    font-family: var(--font-family-base);
    font-weight: 400;
    font-size: 12px;
    line-height: 16px;
    color: #87879a;            /* Figma literal — not in the token set */
    text-align: center;
  }
  /* CTA uses the project-wide `.actionContainer` + `.cta` (full
     viewport-width white plate, 24 / 16 padding, 56-px brand
     button) — defined once at the top of this file. No
     payment-specific override needed. */
