/* ==========================================================================
   YAMATCH MARKETING SITE — STYLES
   ==========================================================================

   TABLE OF CONTENTS
   -----------------
    1. Webfont (@font-face — Frick 0.3)
    2. Design Tokens (:root)
    3. Reset & base elements (box, html/body, img, a, ul, h1/h4, p)
    4. Skip link
    5. Focus visibility (generic :focus-visible)
    6. Wordmark (sticky pill, scroll states, entrance keyframe)
    7. Hero section
       7.1 .hero outer canvas
       7.2 .hero-card (lime block) + cardEntrance
       7.3 .hero-card-inner
       7.4 .hero-card-waves SVG + wavesEntrance
       7.5 .hero-title / .hero-title-slant / .hero-subtitle + titleEntrance
       7.6 .hero-buttons + .btn-glass (variants, hover, shine, fallback)
       7.7 .glass-stack / .glass-eyebrow / .glass-label
    8. Phone image (shared across carousel slides)
    9. Screens rail / carousel
       9.1 .screens-rail wrapper
       9.2 .hero-carousel-embla root (tokens + cursor)
       9.3 .hero-carousel-embla--container (flex row + snap transition)
       9.4 .hero-carousel-embla--slide
       9.5 Active slide phone lift + arrival entrance
       9.6 .screen-card variants (lime / white / arrival)
       9.7 .screen-card__phone / __caption / __caption-primary / __caption-secondary
   10. How-it-works quest (sticky 3-step gamified section)
       10.1 .how-quest section + sticky panel
       10.2 .how-quest-header (eyebrow + title + persona pill-bar)
       10.3 .quest-persona-tabs (glass-morphism segmented pill-bar + sliding indicator)
       10.4 (removed — quest-meter / quest-step-count / quest-xp-current retired)
       10.5 .quest-steps stage + .quest-progress-bar (vertical 1-2-3 rail)
       10.6 .quest-step cards (single-column stack, locked / unlocked)
       10.7 .quest-completion-pill
   11. FAQ accordion
   12. Footer
   13. Toast
   14. QR widget (floating glass-morphism panel, desktop/tablet only)
   15. Download page (website/download/index.html — utility "coming soon")
   16. Mobile media queries (consolidated — overrides hero + carousel + quest)
   17. Reduced motion (NB: this label is the historical TOC slot — actual
       Reduced motion section is now §19; §17 in the body is the consolidated
       Mobile media queries block, §18 the Cursor trail. Pre-existing TOC
       drift kept here for reference; canonical numbering is in the body.)
   18. Cursor trail (volleyball emoji + canvas comet — desktop only)
   19. Reduced motion
   ==========================================================================
*/


/* ==========================================================================
   1. WEBFONT — Frick 0.3 by Dennis Grauel (OFL — see fonts/OFL.txt)
   ========================================================================== */
@font-face {
    font-family: 'Frick 0.3';
    src: url('fonts/Frick0.3-Regular.woff2') format('woff2'),
         url('fonts/Frick0.3-Regular.woff') format('woff');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}


/* ==========================================================================
   2. DESIGN TOKENS (:root)
   ========================================================================== */
:root {
    --color-accent: #D7FF00;
    --color-dark-bg: #1A1A1A;
    --color-dark-surface: #000000;
    --color-light-bg: #FFFFFF;
    --color-light-gray: #F3F4F6;
    --color-medium-gray: #99A1AF;
    --color-text-primary: #101828;
    --color-text-white: #FFFFFF;
    /* Pure black, distinct from --color-text-primary (#101828). Used where the
       user explicitly chose pure black: wordmark, hero title/subtitle on lime,
       faq accordion chevron icon. Memory: "Subtitle copy is `color: #000` (pure
       black, NOT the muted rgba — the user explicitly chose pure black)." */
    --color-text-black: #000000;
    --color-divider: #E0E0E0;
    --color-error: #EF4444;
    --color-success: #4CAF50;
    /* Hover background for the dark glass buttons (.btn-glass:hover). Slight
       blue-tinted lift over --color-text-primary (#101828) to read as a state
       change without breaking the dark palette. */
    --color-button-glass-hover: #1F2A3D;

    --font-heading: 'Roboto', system-ui, -apple-system, sans-serif;
    --font-body: 'Inter', system-ui, -apple-system, sans-serif;

    --radius-card: 24px;
    --radius-input: 16px;
    --radius-pill: 100px;

    --shadow-light: 0 10px 15px rgba(0, 0, 0, 0.10), 0 4px 6px rgba(0, 0, 0, 0.10);
    --shadow-dark: 0 8px 32px rgba(0, 0, 0, 0.4);

    --transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);

    --container-max: 1200px;

    --page-pad: clamp(24px, 4.5vw, 60px);

    /* Vertical gap between the active phone's top edge and the .hero-buttons'
       bottom edge at scroll = 0. Read by script.js (computeBaseLift) which
       writes the resulting --scroll value (in px) on .hero-carousel-embla.
       clamp(24px, 4vh, 56px): 24px floor on tiny viewports, 56px ceiling on 4K. */
    --desired-button-gap: clamp(24px, 4vh, 56px);

    /* Height of the launch announcement ticker that runs across the very top of
       the document (.launch-ticker, section 4.5 below). Consumed by:
         - .launch-ticker { height }                  → the ticker's own box height
         - .wordmark { top }                          → pushes the fixed wordmark BELOW
                                                        the ticker AT scroll = 0 (desktop:
                                                        line 367; mobile: section 17)
       Since 2026-05-12 the ticker is `position: relative` (in document flow), so
       `.hero { padding-block-start }` no longer compensates for it (the ticker
       pushes `.hero` down naturally by its own height — no reservation needed).
       Kept as a fixed 30 px (no clamp) for predictable layout maths in the two
       remaining calc() consumers above. The token must remain defined even on
       pages where the .launch-ticker DOM node is absent (legal / download / 404)
       — the wordmark `top` calc reads it unconditionally. On those utility pages
       the wordmark is overridden to `position: static` (sections 15 / 16 /
       §error-page), so the (now larger) `top` value is dead-code. */
    --launch-ticker-height: 30px;
}


/* ==========================================================================
   3. RESET & BASE ELEMENTS
   ========================================================================== */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    -webkit-text-size-adjust: 100%;
}

/* Horizontal viewport clipping lives here (on the outermost full-viewport-width boxes)
   so carousel slides 0 and 4 can extend into the page-pad gutter on the left/right of
   .screens-rail and still be clipped at the viewport edges (no horizontal page scroll).
   `overflow-x: clip` is mandatory (not `hidden`): per CSS spec, `clip` allows asymmetric
   per-axis overflow values without promoting overflow-y to auto, so the active phone
   image's translateY(var(--scroll)) lift remains visible. */
html, body {
    overflow-x: clip;
}

body {
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.6;
    color: var(--color-text-primary);
    background-color: var(--color-light-bg);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

img, svg {
    display: block;
    max-width: 100%;
}

a {
    color: inherit;
    text-decoration: none;
    transition: color var(--transition), opacity var(--transition);
}

ul {
    list-style: none;
}

h1, h4 {
    font-family: var(--font-heading);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.01em;
    color: var(--color-text-primary);
}

p {
    line-height: 1.65;
}

/* Responsive utilities — small helpers reused across components.
   .br-mobile-only: hide a <br> on desktop, show it on mobile. Used inline
   in the markup to force a line break only on the narrow viewport (e.g.
   .hero-subtitle "Compose ton équipe,<br class="br-mobile-only"> il y a match").
   The mobile-side `display: inline` override lives in section 13 next to
   the rest of the @media (max-width: 767px) rules. */
.br-mobile-only {
    display: none;
}


/* ==========================================================================
   4. SKIP LINK
   ========================================================================== */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 8px;
    z-index: 1000;
    background-color: var(--color-text-primary);
    color: var(--color-text-white);
    padding: 10px 16px;
    border-radius: var(--radius-pill);
    font-size: 14px;
    font-weight: 500;
}
.skip-link:focus {
    left: 16px;
}


/* ==========================================================================
   4.5 LAUNCH TICKER (in-flow announcement bar, top of document)
   --------------------------------------------------------------------------
   Lime announcement strip at the top of the document, in normal flow (NOT
   `position: fixed` — see history note below). A duplicated set of 4 spans
   inside .launch-ticker__track is translated horizontally (-50%) by the
   `launchTickerScroll` keyframe so the row appears to scroll continuously
   without visible seams (the second half is identical to the first, so the
   loop wraps invisibly).

   Stacking: z-index 80 sits ABOVE the wordmark (z-index 50, §6) so the
   wordmark slides in BELOW the ticker at scroll=0, and BELOW .skip-link /
   .toast (z-index 1000, §4 / §13) and the cursor trail / emoji
   (z-index 9998 / 9999, §18) — the cursor companion intentionally floats
   over the ticker. The z-index is preserved (even though the ticker is no
   longer fixed) so that the entrance animation of the wordmark — which
   briefly translates downward through the ticker's plane — never paints
   above the ticker.

   The .launch-ticker__sr <p> is visually hidden but exposed to assistive tech
   (the .launch-ticker__track itself is aria-hidden because the duplicated
   spans would be read 4× by a screen reader).

   Reduced motion override lives in §19 — the animation is killed and a single
   centred copy of the message is shown statically.

   _2026-05-12: `position: fixed; top: 0` → `position: relative` (in flow).
   The ticker now scrolls AWAY with the page rather than staying pinned at the
   viewport top forever. `.hero { padding-block-start: var(--page-pad) }` no
   longer adds `--launch-ticker-height` because the ticker's in-flow height
   already pushes `.hero` down naturally; mobile `.wordmark { top }` was
   bumped by `--launch-ticker-height` so the wordmark sits in the white strip
   BELOW the ticker at scroll=0 with balanced top/bottom gap._
   ========================================================================== */
.launch-ticker {
    /* `position: relative` (in normal flow, NOT `fixed`): the ticker sits at the
       top of the document and scrolls AWAY with the page — once the user has
       scrolled past it, it is gone for the rest of the session. The wordmark
       (still `position: fixed`, §6) takes over as the only permanent top-of-
       viewport UI after the ticker scrolls out. Previous `position: fixed; top: 0`
       kept the ticker permanently pinned, which was retired 2026-05-12 on user
       request: the announcement should be a one-time discovery, not a perpetual
       banner. `--launch-ticker-height` is still consumed by `.wordmark { top }`
       (§6) to offset the wordmark below the ticker AT scroll=0 — once the user
       scrolls past, the fixed wordmark naturally appears to rise toward the
       viewport top because the ticker is no longer underneath. Width is no
       longer 100% (it's a block in flow and defaults to full width of its
       container, here `<body>`). */
    position: relative;
    z-index: 80;
    width: 100%;
    height: var(--launch-ticker-height);
    overflow: hidden;
    display: flex;
    align-items: center;
    background: var(--color-accent);
    color: var(--color-text-black);
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}

.launch-ticker__track {
    display: flex;
    align-items: center;
    /* `width: max-content` lets the row size to its 4 spans + 3 inter-gaps
       intrinsic width; `min-width: 200%` guarantees the row is at least
       twice the viewport so the -50% keyframe never reveals an empty edge
       even if the message is very short.

       Seamless-loop math: 4 spans (S) with `gap` (G) yield 3 internal gaps
       and a track width of `4S + 3G`. The `-50%` keyframe translates by
       `2S + 1.5G`, but the natural repetition period of the pattern is
       `2S + 2G` (2 spans + their bracketing gaps). The 0.5G mismatch
       produced a visible micro-jump on every loop. Closing the loop with
       `padding-right: var(--ticker-gap)` makes the track width `4S + 4G`,
       so `-50% = 2S + 2G` lands exactly on a repetition point — fully
       seamless. The gap value is hoisted into a local custom property so
       the `padding-right` always matches the `gap` precisely. */
    --ticker-gap: clamp(56px, 8vw, 96px);
    width: max-content;
    min-width: 200%;
    gap: var(--ticker-gap);
    padding-right: var(--ticker-gap);
    white-space: nowrap;
    will-change: transform;
    /* `backface-visibility: hidden` is a compositor-stability hint:
       promotes the element onto its own GPU layer with no back-face
       culling cost, which on high-DPR displays smooths sub-pixel
       interpolation of `translate3d` under a `linear` timing function
       (the most demanding case — no easing to mask jitter). */
    backface-visibility: hidden;
    /* Slowed from 18s → 36s (×2) per user request: a more contemplative,
       premium pace. Combined with the seamless-loop fix above and the
       lighter typography below, the bar reads as calm rather than urgent. */
    animation: launchTickerScroll 36s linear infinite;
}

.launch-ticker__track span {
    flex: 0 0 auto;
    font-family: var(--font-body);
    /* Fixed 14 px (was clamp(9.5px, 1.1vw, 11px)) — the previous clamp
       read as too timid on every viewport. A fixed 14 px keeps the bar
       discreet but legible at arm's length on desktop and on a held phone.
       Vertical fit: 14 px line-height-1 in a 30 px box (--launch-ticker-height)
       leaves ~8 px of lime above and below the glyphs — comfortable, no
       clipping. Holding `--launch-ticker-height: 30px` keeps the wordmark
       vertical-centring calc untouched on desktop (.wordmark top, §6) and
       on mobile (§17). */
    font-size: 14px;
    /* Regular weight (was 800) — the mixed-case typography (only the Y
       of Yamatch and the L of Les capitalised, per HTML) already reads
       as restrained; 400 keeps the bar quiet above the hero. */
    font-weight: 400;
    line-height: 1;
    /* Reduced from 0.04em → 0.01em: the wider tracking was tuned for
       all-caps legibility. In mixed case, 0.04em reads as awkwardly
       spaced; 0.01em respects the natural Inter metrics while still
       giving a hair of breathing room for small sizes on the lime bar. */
    letter-spacing: 0.01em;
    /* text-transform removed (was `uppercase`) — the HTML now carries
       the intended casing (only Y of "Yamatch" and L of "Les" capitalised);
       forcing uppercase here would override that typographic intent. */
}

/* Visually-hidden accessible copy — single readable sentence for screen
   readers. Same recipe as the standard sr-only pattern (1×1 px box clipped
   with `clip-path: inset(50%)`, taken out of flow with `position: absolute`,
   nowrap to avoid synthetic line breaks). */
.launch-ticker__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

@keyframes launchTickerScroll {
    /* `--ticker-start-offset` (px) is set on `.launch-ticker__track` by an
       IIFE in script.js to visually centre the first span on page load
       (mobile primarily — desktop receives a larger offset by virtue of
       wider viewports relative to the span width). Adding the same offset
       to BOTH `from` and `to` shifts the whole animation timeline without
       changing the translation distance: it remains exactly `-50%` of the
       track width = `4(S+G)` with 8 spans = an integer multiple of the
       repetition period `S+G`, so the seamless-loop math is preserved.
       The `0px` fallback ensures pages where the IIFE does not run (or
       where `.launch-ticker` is absent) still animate correctly from 0. */
    from { transform: translate3d(var(--ticker-start-offset, 0px), 0, 0); }
    to   { transform: translate3d(calc(var(--ticker-start-offset, 0px) - 50%), 0, 0); }
}


/* ==========================================================================
   5. FOCUS VISIBILITY (generic — feature overrides live with their feature)
   ========================================================================== */
:focus-visible {
    outline: 2px solid var(--color-text-primary);
    outline-offset: 3px;
    border-radius: 4px;
}


/* ==========================================================================
   6. WORDMARK
   --------------------------------------------------------------------------
   Sticky pill, perfectly centered in the white strip between the viewport top
   and the lime card. `top` is half the hero's top-padding (= mid-point of the
   strip), and `translate: -50% -50%` (CSS individual property) centers the
   element on that point regardless of its own size. The entrance animation
   uses `transform: translateY()` separately, which composes with `translate`
   cleanly.
   ========================================================================== */
.wordmark {
    /* `fixed` instead of `absolute` so the logo stays at the top of the viewport for the entire scroll,
       not just the hero section. Top/left now resolve against the viewport, not against `.hero`. */
    position: fixed;
    /* Anchor the wordmark in the white strip BELOW the launch ticker (§4.5) AT
       scroll = 0. The ticker sits in the document flow at viewport y = 0..
       --launch-ticker-height (since 2026-05-12 it is `position: relative`),
       and `.hero { padding-block: var(--page-pad) }` reserves another
       `--page-pad` of white space before the lime card — so the lime card's top
       edge is at viewport y = `--launch-ticker-height + --page-pad`. The
       wordmark's vertical centre at `--launch-ticker-height + --page-pad/2`
       therefore lands exactly halfway between the ticker's bottom edge and the
       lime card's top edge — equal optical gap above and below the wordmark
       glyphs at rest. Once the user scrolls past the ticker, the fixed wordmark
       stays where the viewport says (it does NOT track the now-gone ticker);
       the JS-driven `.is-condensed` / `.is-hidden` classes (§6 below) take over
       its scroll-aware behaviour.
       Inert on the legal/download/error utility pages: those override
       `.wordmark { position: static; top: auto }` (sections 15 / 16 / §error),
       so this `top` value is ignored when the ticker isn't in the DOM. */
    top: calc(var(--launch-ticker-height) + var(--page-pad) / 2);
    left: 50%;
    translate: -50% -50%;
    z-index: 50;
    display: inline-flex;
    align-items: center;
    color: var(--color-text-black);
    /* Default state — no background, no padding (the .is-condensed scrolled state adds the pill). */
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    background: transparent;
    font: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    animation: wordmarkEntrance 320ms cubic-bezier(0.2, 0.8, 0.2, 1) 80ms backwards;
    /* iOS Safari rendering stabilisers for the fixed-positioned wordmark.
       `transform: translateZ(0)` forces the browser to promote the element onto its
       own compositor layer so its raster is not invalidated every time the
       touch-driven scroll re-paints the page underneath. `backface-visibility:
       hidden` keeps that layer from being re-rasterised when the entrance
       `transform: translateY(...)` keyframe finishes (Safari otherwise discards the
       layer the moment the transform reaches `translate(0)`).
       NOTE: `transform: translateZ(0)` is the legacy SHORTHAND — it does NOT
       collide with the `translate: -50% -50%` individual property declared above.
       Both compose cleanly (translate runs first, transform second).
       NOTE: `will-change: opacity` is intentional — NOT `will-change: transform`.
       Declaring `will-change: transform` would invite the browser to pre-allocate
       for transform animation and, on iOS Safari, can amplify subpixel jitter
       on every touch frame. We only need the layer-promotion side effect for
       stable rendering, not a transform-animation hint. */
    transform: translateZ(0);
    backface-visibility: hidden;
    will-change: opacity;
    /* Transition the scroll-driven properties only — `transform` is left alone so the entrance
       keyframe (which animates `transform: translateY(...)`) still plays without conflict. */
    transition:
        translate 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        scale 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
        background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        backdrop-filter 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        -webkit-backdrop-filter 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        padding 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
        border-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Scrolled state — frosted translucent pill + condense. Single class gates both effects. */
.wordmark.is-condensed {
    scale: 0.92;
    padding: 8px 20px;
    background: rgba(255, 255, 255, 0.72);
    border-color: rgba(16, 24, 40, 0.06);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
}

/* Hidden state — fade out with a small upward nudge. Avoids the half-clipped intermediate
   frame that a pure slide produced (translate passing through -100% during the transition). */
.wordmark.is-hidden {
    opacity: 0;
    translate: -50% calc(-50% - 8px);
    pointer-events: none;
}

/* Backdrop-filter fallback — bump opacity so the pill stays legible without the blur. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .wordmark.is-condensed {
        background: rgba(255, 255, 255, 0.92);
    }
}

.wordmark svg {
    /* Mobile (<768px) — reduced from 17px to 14px (~-17.6%) on user request:
       the mark felt slightly oversized on small viewports relative to the
       hero card. Stays >= 14px so the wordmark glyphs remain legible at any
       viewport (320–767px). The desktop override below restores 20px. */
    height: 14px;
    width: auto;
    fill: currentColor;
}

@media (min-width: 768px) {
    .wordmark svg { height: 20px; }
}

@keyframes wordmarkEntrance {
    from {
        opacity: 0;
        transform: translateY(-10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Mobile + touch safety net for the .is-condensed pill animation.
   --------------------------------------------------------------------------
   The JS layer (script.js, "Wordmark click-to-top" block) toggles
   `.is-condensed` unconditionally on every viewport. On iOS Safari the
   confirmed source of subpixel jitter / frosted-pill flicker during
   touch-driven compositor scroll was the animation of LAYOUT-AFFECTING
   properties on a `position: fixed` element — specifically `scale` (changes
   the box's effective size) and `padding` (changes the box's intrinsic
   dimensions). Those two are FROZEN here.
   Purely VISUAL properties (background-color, backdrop-filter, border-color,
   opacity) do NOT trigger layout passes and DO NOT cause iOS jitter, so they
   are still allowed to animate — that's the frosted-pill effect we WANT to
   keep on mobile.
   Trick: padding `8px 20px` is set ALWAYS-ON below (matches the desktop
   `.is-condensed` padding verbatim, line 266). At rest the wordmark already
   has its "pill-sized" box, but background/border/backdrop-filter remain at
   their base values (transparent / none) inherited from `.wordmark` — so no
   visible pill is rendered. When `.is-condensed` adds bg + blur + border-color
   the pill APPEARS visually around a wordmark whose BOX HAS NOT CHANGED SIZE
   → no subpixel shift, no jitter.
   Co-located with the wordmark rules in section 6 (rather than the
   consolidated mobile block in section 13) to keep the safety net adjacent
   to the rule it defends. */
@media (max-width: 767px), (pointer: coarse) {
    .wordmark {
        /* Always-on pill-sized box. Same horizontal/vertical values as the
           desktop `.is-condensed` padding (8px 20px, line 266) so the pill
           geometry is identical between desktop scroll and mobile scroll.
           Padding stays FROZEN (never in the transition list) — animating
           padding on a `position: fixed` element triggers reflow and is the
           confirmed root cause of iOS Safari subpixel jitter on touch
           scroll. */
        padding: 8px 20px;
        /* Transition surface — purely visual properties + scale. Padding is
           DELIBERATELY OMITTED (frozen at 8px 20px above) so no reflow can
           fire on the fixed wordmark layer. `scale` was re-added 2026-05-11
           on user request to restore the desktop "subtle condense" feel
           (scale: 0.92 cascading from .wordmark.is-condensed line 265).
           Calculated bet on iOS jitter: the previous combined animation
           (padding + scale) caused jitter because padding changed the box
           dimensions → reflow + re-rasterisation of the fixed compositor
           layer. With padding frozen, scale alone runs as a transform on the
           ALREADY-PROMOTED layer (translateZ(0) + backface-visibility:hidden
           on .wordmark). The compositor interpolates without re-laying out.
           If a future iPhone test surfaces residual jitter, plan B is to
           switch from individual `scale` to a transform shorthand
           `translateZ(0) scale(...)` — but try the simple solution first. */
        transition:
            scale 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
            opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
            background-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
            backdrop-filter 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
            -webkit-backdrop-filter 300ms cubic-bezier(0.2, 0.8, 0.2, 1),
            border-color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
    }

    /* The previous `.wordmark.is-condensed { scale: 1 }` mobile override was
       REMOVED 2026-05-11 — desktop `scale: 0.92` (line 265) now cascades to
       mobile/touch unchanged, restoring the subtle condense effect. See the
       transition-list comment above for the iOS-jitter rationale. */
}


/* ==========================================================================
   7. HERO SECTION
   ========================================================================== */

/* --- 7.1 .hero outer canvas ------------------------------------------------
   Outer hero is a clean white canvas — the wordmark sits on this white area at the top,
   and the lime/wave background lives INSIDE the card below. The hero is content-sized:
   the lime card fills the viewport, the iPhone peeks below the buttons, and the next
   section (.screens-rail) starts naturally underneath. */
.hero {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 0;
    /* Equal margin between the viewport edge and the lime card, on every side (top/right/bottom/left).
       Horizontal value (`--page-pad`) is the shared gutter applied to every top-level section, so all
       sections align to the same left/right edge. Vertical padding mirrors it so the hero keeps its
       symmetric breathing room around the card.
       Since 2026-05-12 the launch ticker is no longer `position: fixed` (§4.5):
       it sits in the document flow ABOVE `.hero`, so its height already pushes
       the hero down by `--launch-ticker-height` naturally. The previous extra
       `+ var(--launch-ticker-height)` in this calc was a compensation for the
       fixed ticker overlaying the viewport top; that compensation would now
       double-count the offset (ticker pushes hero down AND hero adds its own
       reservation = `2 × ticker_height` of unwanted whitespace above the lime
       card). Reverted to the original symmetric `--page-pad` on both block
       edges. The "equal margins on all four sides" hero invariant
       (memory feedback_website_hero_pattern.md) is restored 1:1. */
    padding-block: var(--page-pad);
    padding-inline: var(--page-pad);
    background-color: var(--color-light-bg);
    /* Flow-axis compensation for the `--card-shrink` visual collapse.
       `--hero-flow-collapse` (px, written live by JS — script.js, page-scroll-progress
       block) is a POSITIVE magnitude that matches the current `--card-shrink` value
       1:1. The lime card's visual fill shrinks from the bottom via clip-path on
       `.hero-card::before` (see section 7.2), but the card's LAYOUT box stays at
       `min-height: 100svh - 2 * --page-pad` for jitter-free measurement. That leaves
       a stable visual gap between the shrinking lime fill and the next in-flow
       sibling (`.screens-rail`). This negative margin-bottom collapses the hero's
       flow box by the exact same number of pixels the fill has shrunk, so the
       carousel rises in lockstep with the fill's bottom edge — no whitespace gap.
       At `--hero-flow-collapse: 0px` (initial value, reduced-motion path, scroll = 0
       boundary), `calc(-1 * 0px)` resolves to 0 and the rule is a perfect no-op:
       layout is byte-identical to the pre-fix state. Applies to BOTH desktop and
       mobile — the mobile override at section 13 only resets `padding`, never
       `margin-bottom`, so this declaration cascades unchanged. The JS writer
       captures `staticCarouselTop` once at load/resize (NOT every tick) so the
       carousel's natural top is decoupled from the live margin-bottom — without
       that, the live margin would feed back into `progress`, creating a loop. */
    margin-bottom: calc(-1 * var(--hero-flow-collapse, 0px));
}

/* Suppress the browser's automatic scroll-anchoring on the two elements whose
   relative position changes during the `--hero-flow-collapse` animation.
   Why: when the negative margin-bottom on `.hero` shortens the cumulative flow
   height above `.screens-rail`, Chromium / Safari's default scroll-anchor
   heuristic may pick an element below the fold as the anchor and silently scroll
   the viewport upward to "preserve" its visible position — fighting the very
   collapse we're trying to render and producing a perceptible counter-scroll
   wobble. Disabling the anchor on both the source of the height change (`.hero`)
   and the element that visually moves (`.screens-rail`) tells the engine "do not
   compensate". Property is a hint; unsupported in Safari ≤ 16.3 (no harm — those
   versions also do not aggressively scroll-anchor, so no UX regression).
   Support: Chrome 56+, Edge 79+, Firefox 66+, Safari 16.4+. */
.hero,
.hero-card,
.screens-rail,
.hero-carousel-embla {
    /* Extended to 4 selectors (was 2) to cover the full scrollable zone affected
       by the `--hero-flow-collapse` margin-bottom animation. On iOS Safari,
       scroll-anchoring can latch onto a visible descendant of any of these
       containers and silently counter-scroll the viewport when the layout
       shifts mid-scroll — fighting the JS-driven `--scroll` lift and producing
       the perceptible wobble reported on iPhone 16. Disabling the anchor on
       both the source containers (.hero, .hero-card) AND the visually moving
       targets (.screens-rail, .hero-carousel-embla) tells the engine "do not
       compensate". The property is a hint; unsupported in Safari ≤ 16.3 — no
       harm there because those versions also do not aggressively scroll-anchor. */
    overflow-anchor: none;
}

/* --- 7.2 .hero-card (lime block) + cardEntrance ----------------------------
   Solid lime block with animated wave lines etched on top.
   Sits in the upper portion of the hero only (content-sized, doesn't grow to fill) so the
   iPhone mockup that follows can extend BELOW the card's bottom edge into the white area. */
.hero-card {
    position: relative;
    z-index: 1;
    width: 100%;
    flex: 0 0 auto;
    /* iOS Safari: the URL bar collapses/expands during scroll, which makes
       100vh oscillate (window.innerHeight changes mid-scroll). That reflows
       .hero-card → reflows the hero stack → reflows .screens-rail below →
       the JS-driven `computeBaseLift` (script.js, page-scroll-progress block)
       recomputes against a moving target → the active phone image visibly
       "jumps" / appears-disappears during vertical scroll on iPhone.
       svh (small viewport height) is the LARGEST stable height the viewport
       can take with all UI chrome (URL bar, toolbar) expanded — it does NOT
       fluctuate during scroll. Double declaration: the first 100vh line is
       the fallback for Safari < 15.4 / Firefox < 101 / Chrome < 108 (cascade
       drops the second line on those engines and keeps the first); modern
       engines apply the second line and ignore the first. */
    /* `--card-shrink` (px, written by JS — script.js, page-scroll-progress block)
       is a POSITIVE magnitude that visually shrinks the lime fill from the BOTTOM
       in sync with the active phone descending into the carousel row. It is
       deliberately VISUAL-ONLY: the layout box (this rule's `min-height`) stays
       fixed, so neither this element nor its sibling `.screens-rail` reflows when
       the value updates. Why this matters: a previous iteration consumed
       `--card-shrink` inside `min-height: calc(... - var(--card-shrink, 0px))`,
       which mutated layout on every rAF tick → reflowed `.screens-rail` (sibling
       in flow) → moved `carousel.getBoundingClientRect().top` → the JS scroll
       writer recomputed `progress` against a moving target → feedback loop →
       jittery scroll. The shrink is now applied as a pure `clip-path` on
       `.hero-card::before` (the lime fill, see rule below) and on
       `.hero-card-waves` (so the waves clip in sync). The element's `background`
       is therefore `transparent` here — the lime colour lives on the pseudo-element. */
    min-height: calc(100vh - 2 * var(--page-pad));   /* fallback older Safari */
    min-height: calc(100svh - 2 * var(--page-pad));  /* iOS 15.4+ : stable face URL bar */
    background: transparent;
    border-radius: clamp(24px, 3vw, 40px);
    overflow: hidden;
    /* Entrance animates opacity + blur only — NO scale. The shared `titleEntrance`
       keyframe (used by .hero-title / .hero-subtitle) ramps from `transform: scale(1.04)`
       to scale(1); on a small centered title that's invisible, but on the full-width
       lime card it shifts the visible left edge by `card_width * 0.02` ≈ 27 px at
       vp 1470 — making `.hero-card.left` (via getBoundingClientRect) measure
       `var(--page-pad) - 27` = 33 px instead of 60 px during the 660 ms entrance
       (120 ms delay + 540 ms duration). That breaks the by-construction alignment
       with `.screens-rail`'s content edge (which sits at `var(--page-pad)`). Using a
       scale-free entrance preserves the perfect left-edge alignment with the
       carousel sentinel from t=0 onward. */
    animation: cardEntrance 540ms cubic-bezier(0.2, 0.8, 0.2, 1) 120ms backwards;
}

@keyframes cardEntrance {
    from {
        opacity: 0;
        filter: blur(6px);
    }
    to {
        opacity: 1;
        filter: blur(0);
    }
}

/* Lime fill of the hero card. Promoted from the `.hero-card` background
   to a pseudo-element so we can clip the fill from the bottom (via
   `clip-path: inset(... round ...)`) without altering the parent's layout
   box. Why: the JS `--card-shrink` writer ticks every rAF; touching the
   parent's layout (e.g. `min-height: calc(... - var(--card-shrink))`)
   reflowed the in-flow sibling `.screens-rail`, which moved the carousel's
   bounding rect, which fed back into the scroll writer → jitter. With the
   shrink expressed as `clip-path` on this absolutely-positioned ::before,
   the layout is untouched and only the painted pixels of the lime change.
   z-index: 0 keeps it BEHIND the wave SVG (z-index: 0 too but later in
   the painting order — see note below) and the inner content
   (`.hero-card-inner` z-index: 1, declared in section 7.3).
   Note on the round value: CSS `inset()` does NOT accept the `inherit`
   keyword for the corner radius — it must be a length / percentage /
   <basic-shape-radius>. We therefore repeat the same `clamp(...)` literal
   used by `.hero-card { border-radius }` (and override it inside the mobile
   media query, where `.hero-card` uses a different clamp). At
   `--card-shrink: 0px` the clip resolves to `inset(0 0 0 0 round R)`,
   which is visually identical to a plain rounded rectangle covering the
   whole element — so the page renders identically when JS is absent,
   reduced-motion is on, or the very first frame before the rAF runs. */
.hero-card::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: var(--color-accent);
    border-radius: inherit;
    z-index: 0;
    pointer-events: none;
    clip-path: inset(0 0 var(--card-shrink, 0px) 0 round clamp(24px, 3vw, 40px));
}

/* --- 7.3 .hero-card-inner --------------------------------------------------
   Inner flex container for the hero card content (title, subtitle, buttons).
   `position: relative; z-index: 1` puts the title/subtitle/buttons above the
   absolutely-positioned .hero-card-waves SVG (z-index 0) that lives in the card. */
.hero-card-inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Vertical rhythm between hero title / subtitle / buttons. Bumped from
       clamp(14px, 1.8vw, 22px) (~+45% across the range) to give the lime card
       more editorial breathing room without breaking the épuré aesthetic.
       Stagger order is title → subtitle → buttons (.hero-buttons), and this
       single `gap` controls all three inter-element spaces uniformly. */
    gap: clamp(20px, 2.5vw, 32px);
    padding: clamp(48px, 6vw, 88px) clamp(20px, 3.2vw, 48px);
    width: 100%;
}

/* --- 7.4 .hero-card-waves SVG + wavesEntrance ------------------------------
   Animated wave lines — vertical SVG paths whose points drift via 2D pseudo-noise.
   Replaces the prior static diagonal-net etched on top of the lime. JS injects the
   <svg class="hero-card-waves"> element and drives the per-frame path data; this rule
   only positions/scales it. The SVG fills the card and clips to its rounded corners
   via inherited overflow:hidden on .hero-card. z-index 0 keeps it behind the inner
   content (.hero-card-inner is bumped to z-index 1). */
.hero-card-waves {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
    border-radius: inherit;
    /* Same fade-in feel the prior net had, so the entrance choreography reads identically. */
    animation: wavesEntrance 240ms ease-out backwards;
    /* Same visual shrink as `.hero-card::before` (the lime fill) — the waves
       must clip in lockstep with the lime bottom edge, otherwise the wave
       paths would visibly extend past the lime and float on the white
       background. `inset()` does not accept `inherit` for the corner radius,
       so we repeat the literal clamp from `.hero-card { border-radius }`
       (mobile override redefines it inside the @media block below). */
    clip-path: inset(0 0 var(--card-shrink, 0px) 0 round clamp(24px, 3vw, 40px));
}

@keyframes wavesEntrance {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* --- 7.5 .hero-title / .hero-title-slant / .hero-subtitle + titleEntrance --- */
.hero-title {
    font-family: 'Frick 0.3', var(--font-heading);
    font-weight: 400;
    /* Lower clamp min so small mobile viewports don't have an oversized title that breaks the 2-line wrap.
       At 320 px viewport the title sits at 1.75rem = 28 px, scales linearly via 6.5vw, caps at 5.5rem on
       desktop. Keeps "TOURNOI T'ATTEND" fitting on one line even in the narrowest card. */
    font-size: clamp(1.75rem, 6.5vw, 5.5rem);
    line-height: 0.95;
    letter-spacing: 0;
    color: var(--color-text-black);
    text-align: center;
    /* max-width chosen so "TOURNOI T'ATTEND" (16 chars) fits on one line but
       "TON PROCHAIN TOURNOI" (20 chars) doesn't — forces the wrap right after PROCHAIN. */
    max-width: 18ch;
    text-transform: uppercase;
    animation: titleEntrance 480ms cubic-bezier(0.2, 0.8, 0.2, 1) 200ms backwards;
}

/* Browser oblique synthesis on Frick 0.3 is unreliable (Chrome/Safari often refuse and fall through
   to the next family or render upright). Apply a deterministic visible slant via skewX on an inner
   span so the entrance animation's `transform: scale()` on .hero-title stays untouched. */
.hero-title-slant {
    display: inline-block;
    transform: skewX(-12deg);
}

.hero-subtitle {
    font-family: var(--font-body);
    font-size: clamp(0.95rem, 1.4vw, 1.0625rem);
    font-weight: 500;
    line-height: 1.5;
    color: var(--color-text-black);
    text-align: center;
    max-width: 52ch;
    margin: 0 auto;
    animation: titleEntrance 480ms cubic-bezier(0.2, 0.8, 0.2, 1) 280ms backwards;
}

@keyframes titleEntrance {
    from {
        opacity: 0;
        transform: scale(1.04);
        filter: blur(6px);
    }
    to {
        opacity: 1;
        transform: scale(1);
        filter: blur(0);
    }
}

/* --- 7.6 .hero-buttons + .btn-glass (variants, hover, shine, fallback) ----- */
/* Buttons are ALWAYS side-by-side, even on mobile. The buttons themselves shrink responsively
   so they fit a 320-wide viewport without stacking vertically. */
.hero-buttons {
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: clamp(8px, 1.4vw, 16px);
}

/* === Liquid glass buttons (lime variant on white) === */
.btn-glass {
    position: relative;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Responsive sizing — buttons shrink on mobile so two fit side-by-side in a 320 px viewport. */
    gap: clamp(6px, 1vw, 10px);
    padding: clamp(6px, 0.8vw, 10px) clamp(10px, 1.6vw, 18px);
    min-height: clamp(36px, 4.5vw, 44px);
    border-radius: var(--radius-pill);
    background: var(--color-text-primary);
    border: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.10),
        inset 0 -1px 0 rgba(0, 0, 0, 0.30),
        0 6px 18px rgba(16, 24, 40, 0.18);
    color: var(--color-text-white);
    font-family: var(--font-body);
    font-weight: 600;
    line-height: 1.1;
    cursor: pointer;
    transition: transform var(--transition), background-color var(--transition), box-shadow var(--transition);
    animation: buttonEntrance 380ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

.hero-buttons .btn-glass:nth-child(1) { animation-delay: 380ms; }
.hero-buttons .btn-glass:nth-child(2) { animation-delay: 460ms; }

@keyframes buttonEntrance {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
}

/* Light sweep on hover — a thin diagonal stripe slides across the glass once. */
.btn-glass::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.22) 50%, transparent 70%);
    transform: translateX(-100%);
    pointer-events: none;
    z-index: 0;
}
.btn-glass:hover::before {
    animation: btnShine 700ms cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes btnShine {
    from { transform: translateX(-100%); }
    to { transform: translateX(100%); }
}

/* Children render above the sweep layer. */
.btn-glass > * {
    position: relative;
    z-index: 1;
}

.btn-glass:hover {
    background: var(--color-button-glass-hover);
    transform: translateY(-1px);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.14),
        inset 0 -1px 0 rgba(0, 0, 0, 0.35),
        0 10px 24px rgba(16, 24, 40, 0.24);
}

.btn-glass:focus-visible {
    outline: 2px solid var(--color-text-primary);
    outline-offset: 3px;
}

.btn-glass svg {
    width: clamp(14px, 1.6vw, 18px);
    height: clamp(14px, 1.6vw, 18px);
    flex: none;
    transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.btn-glass:hover svg {
    transform: rotate(12deg);
}

/* --- 7.7 .glass-stack / .glass-eyebrow / .glass-label ---------------------- */
.glass-stack {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
}

.glass-eyebrow {
    font-size: clamp(8px, 0.95vw, 10px);
    font-weight: 500;
    letter-spacing: 0.04em;
    opacity: 0.7;
    line-height: 1;
}

.glass-label {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(0.75rem, 1.3vw, 0.9375rem);
    line-height: 1.1;
}

/* Fallback for browsers without backdrop-filter — bump opacity so the label stays legible. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .btn-glass {
        background: rgba(215, 255, 0, 0.92);
    }
    .btn-glass:hover {
        background: rgba(215, 255, 0, 1);
    }
}


/* ==========================================================================
   8. PHONE IMAGE (shared across carousel slides)
   --------------------------------------------------------------------------
   Single rule, used inside every carousel slide.
   The iPhone chrome (dark body, rounded corners, dynamic island, shadow) is baked into
   each PNG/WebP source — no CSS frame needed. Width clamp keeps the phone visually
   consistent across slides; height: auto lets the intrinsic ratio drive the rendered
   height so the baked frame chrome isn't cropped at the edges.

   `max-width: none` overrides the global `img { max-width: 100% }` reset (section 3).
   With the current geometry (`--nbr-slide: 3`, `--slide-spacing: clamp(16px, 2vw, 28px)`),
   the phone fits inside its slide on wide viewports — at 1440 px viewport the slide is
   ~421 px and the phone clamps to ~395 px (398 px ceiling), leaving ~13 px breathing room
   each side. At narrower desktop viewports the slide shrinks below the phone's clamp min:
   at 768 px viewport the slide is ~222 px while the phone holds at its 274 px clamp min,
   so the phone overflows its slide by ~26 px each side. `max-width: none` keeps that
   intentional overflow rendering. The overflow is safe: `.screen-card` and
   `.hero-carousel-embla--container` have no overflow constraint, and horizontal viewport
   clipping is handled by `body { overflow-x: clip }`.
   ========================================================================== */
.phone-image {
    width: clamp(274px, 27.4vw, 398px);
    max-width: none;
    height: auto;
    display: block;
    -webkit-user-drag: none;
    user-select: none;
    -webkit-user-select: none;
}

/* Mobile — shrink phone so it fits the smaller card width. */
@media (max-width: 640px) {
    .phone-image {
        width: clamp(209px, 69.6vw, 302px);
    }
}


/* ==========================================================================
   9. SCREENS RAIL / CAROUSEL (JS-driven horizontal carousel — no native scroll)
   --------------------------------------------------------------------------
   The .screens-rail section is a thin semantic wrapper; the actual carousel lives in
   .hero-carousel-embla inside. The active slide's phone image is translated UP via
   translateY(var(--scroll)) so its top portion peeks above the carousel row at rest,
   just below the lime card. The lift value is computed live in JS (script.js,
   page-scroll-progress block) so the phone's top edge always sits at a constant
   --desired-button-gap (clamp(24px, 4vh, 56px), declared in :root) below the
   .hero-buttons bottom edge at scroll = 0, regardless of viewport. As the user
   scrolls into the section, JS animates --scroll
   from that lift value → 0 so the image descends into the row.
   Horizontal motion is a pure threshold-fire state machine in script.js — wheel + touch
   gestures are silently accumulated; once the threshold is crossed, the container is
   snapped via translate3d to center the new active slide (CSS animates the snap). At the
   deck edges (activeIndex = 1 swipe left, activeIndex = 3 swipe right) the gesture plays
   a rubber-band: the container glides 70 px toward the unreachable sentinel in 280 ms,
   then springs back to the centered position in another 280 ms (560 ms total). The default
   440 ms snap transition declared below stays intact — script.js temporarily overrides
   the inline transition to 280 ms during the bounce, then restores the default at the
   end. No live-follow during the gesture itself.
   ========================================================================== */

/* --- 9.1 .screens-rail wrapper -------------------------------------------- */
.screens-rail {
    position: relative;
    /* z-index: 2 lifts the entire carousel section above the lime card (.hero-card has
       z-index: 1), so the active slide's translateY(var(--scroll)) lift renders IN FRONT
       of the lime card at scroll=0. Without this, the carousel section's auto z-index
       would be drawn behind the lime card. */
    z-index: 2;
    padding-block: clamp(20px, 3vw, 48px) clamp(40px, 7vw, 96px);
    /* Standard page gutter — every section under <main> shares the same padding-inline:
       var(--page-pad) so side margins line up between .hero, .screens-rail, and .faq.
       Off-screen slides 0 and 4 are allowed to extend INTO this gutter (and beyond, up
       to the viewport edges) — clipping lives on `body` (overflow-x: clip), not here. */
    padding-inline: var(--page-pad);
    background: transparent;
    /* overflow stays visible on both axes: the active slide is translateY(var(--scroll))-ed
       upward into the hero zone, and side slides 0/4 extend horizontally past the page-pad
       gutter into the viewport edges. Horizontal clipping at the viewport boundary is
       handled by `body { overflow-x: clip }`. */
    overflow: visible;
}

/* --- 9.2 .hero-carousel-embla root (tokens + cursor) ----------------------
   Carousel root. --nbr-slide controls visible slides, --slide-spacing is the inter-slide
   gap, --slide-size is computed from those. --scroll is the active phone image's vertical
   translation; default = lifted offset above the row (just below the lime card buttons),
   animated to 0 by the scroll-progress block in script.js. No overflow constraint here —
   slides can extend into the page-pad gutter on left/right of .screens-rail. Horizontal
   clipping lives on `body` (overflow-x: clip) so slides clip at the viewport edges. */
.hero-carousel-embla {
    /* --slide-size derives from --nbr-slide (visible slides at once) and --slide-spacing
       (inter-slide gap). The active slide rides --scroll vertically so it lifts above the
       row of cards into the hero zone at rest, then descends back into the row as the user
       scrolls (page-scroll-progress block in script.js animates --scroll → 0).

       Geometry calibrated so that at rest (active = slide 1 centered) the left sentinel
       (slide 0) aligns its LEFT edge exactly to the viewport's `--page-pad` gutter, and
       symmetrically the right sentinel (slide 4) aligns its RIGHT edge to `--page-pad`
       from the right viewport edge. With `--nbr-slide: 3` and `--slide-spacing` scaling
       proportionally to `--page-pad` (both clamped on `vw`), the algebra collapses to an
       identity (1.5 × slide_size + slide_spacing = container_width / 2) so the sentinels
       sit flush with the page-pad gutter at every viewport, not just one breakpoint. */
    --nbr-slide: 3;
    --slide-spacing: clamp(16px, 2vw, 28px);
    --slide-size: calc((100% - (var(--nbr-slide) - 1) * var(--slide-spacing)) / var(--nbr-slide));
    /* --scroll initial value is a no-op (0 px). The JS page-scroll-progress block
       overwrites it on the first rAF after load with the live-computed lift that
       keeps the active phone's top edge 40 px below the .hero-buttons bottom. The
       prefers-reduced-motion override at the bottom of the file pins this to 0
       (no lift) for users who opt out of motion. */
    --scroll: 0px;
    --active-scale: 1.14;
    cursor: grab;
    /* Browser-side hint: vertical pan (page scroll) is committed natively without
       waiting for JS. Horizontal gestures are NOT consumed by the browser, so
       carousel.js can still capture touchstart/touchmove/touchend for its custom
       horizontal swipe state machine. Complementary to the JS intent-lock
       (vertical/horizontal) in carousel.js: the browser commits vertical scroll
       the instant it detects a vertical-dominant move, eliminating the micro
       horizontal jitter that would otherwise occur while JS is still measuring
       the gesture. Spec: pan-y allows native vertical pan and disables native
       horizontal pan + pinch-zoom on this element. */
    touch-action: pan-y;
}

/* --- 9.3 .hero-carousel-embla--container (flex row + snap transition) ----
   Static flex row pinned to the container's left edge. `justify-content: flex-start`
   makes the sentinel_0 ↔ page-pad alignment EXPLICIT geometry rather than the implicit
   algebraic identity that `center` produced (where 5 slides + 4 gaps overflow the
   container symmetrically and happen to land sentinel_0 on the page-pad gutter). With
   flex-start, slide 0's left edge sits exactly at the container's left edge — which is
   the viewport's `--page-pad` thanks to `.screens-rail { padding-inline: var(--page-pad) }`.
   The JS `setActiveIndex` then writes a translate3d so slide `activeIndex` is centered
   in the viewport (offset = 0 when activeIndex = 1, since slide 1's natural center is
   page_pad + container_width/2 = visible viewport center). Horizontal motion is JS-driven
   (translate3d on the container) via the gesture state machine in script.js — wheel + touch
   are intercepted, no native scroll. The snap is animated by the 440ms ease-in-out-cubic
   transition (cubic-bezier(0.65, 0, 0.35, 1)) below. */
.hero-carousel-embla--container {
    display: flex;
    gap: var(--slide-spacing);
    justify-content: flex-start;
    transition: transform 440ms cubic-bezier(0.65, 0, 0.35, 1);
    will-change: transform;
}

/* --- 9.4 .hero-carousel-embla--slide -------------------------------------- */
.hero-carousel-embla--slide {
    flex: 0 0 var(--slide-size);
}

/* --- 9.5 Active slide phone lift + arrival entrance ----------------------- */
/* Active slide's phone image rides --scroll vertically — at rest --scroll lifts the image
   above the row into the hero zone (just below the lime card's CTA buttons); as the user
   scrolls the page, --scroll animates toward 0 so the image descends into the row.
   No CSS transition on `transform`: the lift is 100% scroll-driven (the JS writes
   `--scroll` directly on every rAF tick, so the value is already perfectly synced
   to the user's scroll position). A transition here would only ADD visible lag
   between the user's scroll input and the phone's response. NOTE: this is unrelated
   to the carousel container's snap transition (`.hero-carousel-embla--container`,
   section 9.3) which animates the horizontal slide change — that one MUST stay. */
.screen-card.active .phone-image {
    /* translate3d (instead of translateY) forces Safari iOS to promote this image
       onto its own GPU compositor layer, so the per-frame `--scroll` write
       (driven from a `passive: true` scroll listener) is composited on the GPU
       thread without re-rasterising the slide above. translateY alone is not
       enough on iOS Safari — it stays on the main thread and the JS-driven
       lift desynchronises from the compositor-scrolled page during touch scroll
       (visible jank on iPhone 16). will-change: transform pre-allocates the
       layer; backface-visibility: hidden is the classic companion hint that
       keeps the layer permanently 3D-ready, preventing re-rasterisation churn
       when --scroll crosses the integer-pixel boundary on each rAF tick. */
    transform: translate3d(0, var(--scroll), 0) scale(var(--active-scale, 1));
    transform-origin: center top;
    will-change: transform;
    backface-visibility: hidden;
    transition: none;
}

/* Entrance choreography for the initially-active phone — opacity + blur only (NO
   transform) so this animation does not collide with the runtime
   `transform: translateY(var(--scroll))` lift on `.screen-card.active .phone-image`
   above. Bound to `.screen-card--arrival` (a STATIC HTML modifier on the initial
   active slide — see index.html L101), NOT to `.screen-card.active`, because
   `.active` migrates between slides as the user navigates the carousel via JS, which
   would re-trigger the entrance keyframe on every snap. `--arrival` is set once in
   the markup and never moves, so the animation plays exactly once at page load.
   Reuses the same `cardEntrance` keyframe as `.hero-card` (rationale at section 7.2)
   so the phone fades in with the same visual family as the lime card and hero text.
   Delay 360ms — slots AFTER the buttons (hero-buttons :nth-child(2) fires at 460ms
   with a 380ms duration) so the visual reading order is title (200ms) → subtitle
   (280ms) → buttons (380/460ms) → active phone (360ms), letting the phone settle
   while the buttons finish. The shared reduced-motion blanket rule neutralises the
   duration to 0.01ms. */
.screen-card--arrival .phone-image {
    animation: cardEntrance 540ms cubic-bezier(0.2, 0.8, 0.2, 1) 360ms backwards;
}

/* --- 9.6 .screen-card variants (lime / white / arrival) ------------------- */
.screen-card {
    /* flex sizing comes from .hero-carousel-embla--slide; .screen-card just paints. */
    /* Override the default flex-item `min-width: auto` (= min-content) so the slide honours
       its `flex: 0 0 var(--slide-size)` even when the inner phone image is wider than the
       slide's content area. Without this, on wide viewports (e.g. vp 1470 → slide_size ≈ 431,
       phone 398 + 2×24 padding = 446 min-content) the slide stretches to ~446 px, breaking
       the geometry identity (slide 1 natural center = window/2) that lets sentinel_0 sit
       flush at `--page-pad` while slide 1 centers in the viewport with a JS transform of 0.
       The phone is still allowed to overflow the content area (max-width: none on .phone-image
       — see section 8) but stays inside the slide's outer box at desktop sizes. */
    min-width: 0;
    min-height: clamp(400px, 52vh, 580px);
    border-radius: clamp(24px, 3vw, 40px);
    padding: clamp(20px, 2.2vw, 32px) clamp(16px, 2vw, 24px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(14px, 1.6vw, 20px);
}

.screen-card--lime {
    background-color: var(--color-accent);
    color: var(--color-text-primary);
}

.screen-card--white {
    background-color: var(--color-light-bg);
    color: var(--color-text-primary);
    /* Subtle inner border so the white card has a defined edge against the white section. */
    box-shadow: inset 0 0 0 1px rgba(16, 24, 40, 0.06);
}

/* --- 9.7 .screen-card__phone / __caption / variants ----------------------- */
.screen-card__phone {
    display: flex;
    justify-content: center;
    align-items: center;
}

.screen-card__caption {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: clamp(0.95rem, 1.4vw, 1.125rem);
    line-height: 1.4;
    color: var(--color-text-primary);
    text-align: center;
    max-width: 24ch;
}

.screen-card__caption-primary {
    display: block;
    font-family: var(--font-body);
    font-weight: 700;
}

.screen-card__caption-secondary {
    display: block;
    font-family: var(--font-body);
    font-weight: 500;
    font-style: italic;
}


/* ==========================================================================
   10. HOW-IT-WORKS QUEST (sticky 3-step gamified section)
   --------------------------------------------------------------------------
   Sectioned UX block inserted between the carousel (.screens-rail) and the
   FAQ. Three step-cards stacked vertically in a single column, with a
   vertical numbered progress bar (1 → 2 → 3) on the left that fills lime
   as the user scrolls. A persona pill-bar at the top swaps the cards'
   content (Participant / Organisateur) via JS — html-expert built the
   markup; js-expert handles the persona swap, scroll-driven unlocks,
   sliding pill-bar indicator, and progress-bar fill. This block is purely
   the visual scaffolding.

   Layout strategy:
   - The whole section is TALL (`min-height: 320svh` desktop, see 10.1) so
     the inner `.how-quest-sticky` can stay pinned for ~3.2 viewport heights
     of scroll — long enough for the user to scroll through all 3 unlock
     thresholds (data-unlock="0.12"/"0.42"/"0.72" in the markup) PLUS a
     comfortable linger after the third unlock so the completion state
     doesn't slam straight into the FAQ.
   - `.how-quest-sticky` is `position: sticky; top: 0; height: 100svh` plus
     `padding-bottom` to guarantee visual breathing room under the last
     card before the section un-pins.
   - Cards are positioned via a 2-column grid: col 1 = vertical progress
     bar, col 2 = single-column stack of the 3 cards.
   - Locked cards are dimmed (opacity 0.42); `.is-unlocked` (added by JS at
     the unlock threshold) lifts them to opacity 1 with a gentle scale-in.

   Token usage:
   - Background: var(--color-light-gray) — quiet light-gray surface that
     creates a clear rhythm between the WHITE carousel above and the WHITE
     FAQ below (background palette inverted 2026-05-11 on user request: the
     FAQ moved to white, this section took over the light-gray slot).
   - Accent: var(--color-accent) (lime) for the progress-bar fill + active
     pill-bar indicator + active progress node + completion pill — single
     accent, no glow.
   - Pill-bar active state uses var(--color-text-primary) on lime → dark
     text on lime indicator (inverted from the previous dark-on-white).
   ========================================================================== */

/* --- 10.1 .how-quest section + sticky panel ------------------------------- */
.how-quest {
    /* Tall section — gives the sticky inner panel ~3.2 viewports of scroll
       budget. The unlock thresholds (data-unlock="0.12"/"0.42"/"0.72") fire
       in the first ~72 % of the section's progress; the remaining ~28 %
       lets the user LINGER on the fully-completed state before the sticky
       panel un-pins into the FAQ. Bumped 2026-05-11 from 280svh to 320svh
       (+40svh) because the previous budget caused the third card to
       visually overlap the FAQ on tall viewports — the sticky released
       before the user perceived the completion. svh keeps the value stable
       through iOS URL-bar collapse/expand (matches .hero-card section 7.2).
       The mobile override (section 14) uses a smaller value (260svh,
       previously 220svh) for the same lingering rationale at mobile scale. */
    position: relative;
    /* z-index: 0 anchors a stacking context on the section so the inner
       sticky's stacking context (z-index: 0 below) is contained AT this
       level — it cannot leak above the next section (.faq) which gets
       its own positioned stacking context (`position: relative; z-index: 1`,
       see §11). This is the defensive pillar that prevents the documented
       overlap regression: even if the sticky panel's content overflows
       its 100svh box for any reason, the FAQ paints on top of it. */
    z-index: 0;
    min-height: 320svh;
    /* Block-end buffer between the sticky panel's bottom and the FAQ.
       --------------------------------------------------------------------
       Root-cause fix for the documented "card 3 overlaps FAQ title" bug.
       Mechanism: `.how-quest-sticky` is `position: sticky; top: 0;
       height: 100svh`. Sticky releases when the section's bottom edge
       hits the viewport bottom — i.e. when scrollY ≈ staticQuestTop +
       (320svh - 100svh) = +220svh. At that exact moment the panel sits
       FLUSH against the section's bottom edge, which is FLUSH against
       the FAQ's top edge. Any pixel of content that overflowed the
       sticky panel's 100svh box (intrinsic content > 100svh on short
       viewports, or a card 3 that just animated to is-unlocked with the
       2 % scale lift) renders OVER the FAQ.
       This `padding-bottom` reserves a real visible gray buffer at the
       end of the section. The sticky still pins for the same scroll
       budget (320svh - 100svh = 220svh), but its visible release happens
       above the FAQ instead of butting against it. Combined with the
       `overflow: hidden` on `.how-quest-sticky` below (which clips any
       intrinsic content overflow), the FAQ has guaranteed clear air.
       Scaled with the desktop `.faq { padding-block: clamp(24px, 4vw, 52px) }`
       so the gray-to-white transition reads as a deliberate breath, not
       a hard cut. Mobile override in §14. */
    padding-bottom: clamp(48px, 8vw, 120px);
    /* Light-gray quiet surface — sits between the WHITE carousel above
       and the WHITE FAQ below to create a visual rhythm
       white → gray → white → white (2026-05-11). The persona pill-bar's
       outer pad (`.quest-persona-tabs { background-color: var(--color-light-bg) }`,
       see §10.3) intentionally stays white so it reads as a subtle elevated
       chip on this gray surface — added contrast was the goal. The step
       cards (`.quest-step { background: var(--color-light-bg) }`, see §10.6)
       also stay white for the same reason; their existing border-color
       provides enough definition against the gray.
       PREMIUM UPGRADE 2026-05-11: very subtle radial gradient overlay
       layered ON TOP of the base gray — a near-imperceptible warm-light
       wash centered behind the cards, giving depth without breaking the
       épuré aesthetic (memory feedback_website_aesthetic.md). The two
       radials are sub-3 % alpha so they read as ambient lighting rather
       than a coloured panel. Both `background-image` layers paint above
       `background-color` per CSS spec, so the lime/dark ambient is
       always atop the base gray.

       PREMIUM UPGRADE 2026-05-11 (iteration 2): added a third lime halo
       at the top-center (slightly more present than the existing
       wash — 7 % alpha, contained to the upper 40 % of the section) so
       the eye reads "lime accent" the moment the section enters the
       viewport, without saturating the surface. The two original radials
       are KEPT and slightly densified (5 → 6 % top wash, 2.5 → 3 %
       bottom wash) to anchor the visual rhythm — but ALPHAS STAY UNDER
       0.08 to remain in the épuré / Pure.app-style budget. Order of
       layers matters: the top-center halo is the FIRST layer so it
       paints UNDER the original two (CSS spec: first listed layer
       is on top — but here we want the broader washes to dominate, so
       we listed it first deliberately for narrow-spot prominence in the
       header zone). NO `filter: blur()` is used here — the radials
       already have soft edges via `transparent N%` stops, and adding a
       filter would create a stacking context that interferes with the
       sticky panel inside (`.how-quest-sticky`) and the section's
       `z-index: 0` invariant. NO `::before` pseudo-element is used
       either, for the same reason. */
    background-color: var(--color-light-gray);
    background-image:
        radial-gradient(circle 480px at 50% 12%, rgba(215, 255, 0, 0.10), transparent 70%),
        radial-gradient(ellipse 80% 50% at 50% 30%, rgba(215, 255, 0, 0.06), transparent 70%),
        radial-gradient(ellipse 60% 40% at 50% 90%, rgba(16, 24, 40, 0.03), transparent 70%);
    padding-inline: var(--page-pad);
    /* Section-level horizontal clipping so any peek elements never trigger
       a horizontal page scroll. `clip` (not hidden) keeps overflow-y visible
       per CSS spec — important so the sticky panel inside is not clipped
       vertically. */
    overflow-x: clip;
}

.how-quest-sticky {
    /* Pinned panel: fills AT LEAST one viewport while the parent .how-quest
       (320svh tall) scrolls past. `top: 0` parks it flush with the viewport
       top; the header sits inside the panel's top padding, so visually the
       title floats below the wordmark band.
       SIZING REWORK 2026-05-11: switched from a fixed `height: 100svh` to
       `min-height: 100svh` so the panel can grow with its content. With the
       new desktop zigzag layout (3 cards alternating sides + the completion
       pill flowing under node 3), the intrinsic content height legitimately
       exceeds 100svh on shorter desktop viewports and on tall mobile copy.
       Locking the box at 100svh + clipping was the root cause of the "card
       3 truncated" bug. Now: the sticky pins flush with the viewport top
       for the duration the section's bottom edge is below the viewport
       bottom (CSS sticky semantics — the sticky simply releases sooner if
       its own height grows past the section's scroll budget); the user
       still sees the full content because nothing is clipped vertically. */
    position: sticky;
    top: 0;
    min-height: 100svh;
    /* CLIPPING REWORK 2026-05-11: replaced `overflow: hidden` (which clips
       BOTH axes and was truncating card 3) with `overflow-x: clip` (spec
       quirk: `clip` does NOT auto-promote the OTHER axis to `auto`, so
       `overflow-y` truly stays `visible`). Horizontal bleed from any
       descendant is still contained at the section edges (defence in depth
       on top of `.how-quest { overflow-x: clip }` §10.1). Vertical content
       is allowed to grow with the sticky's `min-height: 100svh` box, so
       the third card and the completion pill can never be truncated.
       The historical FAQ-overlap concern is now defended exclusively by
       structure: `.faq { position: relative; z-index: 1 }` (§11) always
       paints on top, and `.how-quest { padding-bottom: clamp(48px, 8vw, 120px) }`
       reserves a real visible gray buffer between the sticky's last
       content and the FAQ's eyebrow. */
    overflow-x: clip;
    overflow-y: visible;
    display: flex;
    flex-direction: column;
    /* Vertical padding inside the pinned panel — keeps the title clear of
       the wordmark/condensed pill at the top, and the completion pill clear
       of the FAQ section transition at the bottom. The bottom padding was
       BUMPED 2026-05-11 from `clamp(40px, 6vh, 88px)` to `clamp(40px, 8vh, 96px)`
       so the last card has more visible breathing room before the sticky
       un-pins into the FAQ — addresses a regression where the third card
       appeared to overlap the FAQ section. */
    padding-block: clamp(40px, 6vh, 88px) clamp(40px, 8vh, 96px);
    /* Sticky elements create their own stacking context. Setting an
       explicit z-index also lets us layer the completion pill (z-index: 2,
       below) above the cards (z-index: 1) above the progress-bar fill
       (z-index: 0) without leaking out of this panel's stack. The parent
       `.how-quest` now has z-index: 0 too, so this whole stacking context
       is isolated below the FAQ's z-index: 1 (see §11). */
    z-index: 0;
}

/* --- 10.2 .how-quest-header (eyebrow + title + persona pill-bar) --------- */
.how-quest-header {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(16px, 2vh, 28px);
    width: 100%;
    max-width: var(--container-max);
    margin-inline: auto;
    flex: none;
}

/* Eyebrow above the H2 — typography AND `margin-bottom` cloned 1:1 from
   .faq-eyebrow (line ~1625) so the two in-page section eyebrows read as
   identical siblings at every viewport (user request 2026-05-11: identical
   spacing eyebrow→title in both sections). Note: this margin-bottom STACKS
   on top of the parent `.how-quest-header`'s flex `gap`, so the visible
   eyebrow→title distance in this section is `gap + 16px` while the FAQ's
   is just `16px` — the literal margin-bottom values are identical, the
   composed visual gap is not (the gap delta is intentional, controlled by
   the header's flex `gap` token shared with the persona-tabs spacing
   below). Color is the same muted gray used by the FAQ eyebrow.

   PREMIUM UPGRADE 2026-05-11 (iteration 2): tiny lime dot prefix via
   ::before so the eyebrow reads as a sport-tagged section opener rather
   than a generic muted label (matches the "parcours premium / plan de
   match" direction). Inline-flex on the eyebrow itself centers the dot
   + label as a unit; the dot's vertical-align is handled by the flex
   `align-items: center` so it tracks the cap-height of the 12 px label
   precisely. Dot is 6 px lime with a hairline inset to read as a
   refined "lit pixel" rather than a flat circle. */
.how-quest-eyebrow {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--color-medium-gray);
    text-align: center;
    margin: 0 0 16px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
}

.how-quest-eyebrow::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--color-accent);
    /* Hairline inset highlight + tight halo. The halo is small enough
       (4 px spread, 24 % alpha) to read as a refined accent rather than
       a glow — keeps the épuré contract intact. */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.45),
        0 0 0 2px rgba(215, 255, 0, 0.12);
    flex: none;
}

.how-quest-title {
    /* Same family + weight + tracking + size as .faq-title (line ~1499) so
       the two in-page H2 headings read as siblings — explicit re-use of the
       FAQ clamp ensures any future tweak to the FAQ stays in sync visually
       (the mobile override below also mirrors .faq-title's mobile clamp).
       PREMIUM UPGRADE 2026-05-11 (iteration 2): tightened tracking from
       -0.02em to -0.025em for a slightly more sport-bold display feel
       at the title scale. Weight stays 700 (NOT 800) per the
       feedback_website_aesthetic memory: "weight 600–700 (NOT 900)". */
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(2rem, 4.5vw, 3rem);
    line-height: 1.08;
    letter-spacing: -0.025em;
    color: var(--color-text-primary);
    text-align: center;
    margin: 0;
}

/* --- 10.3 .quest-persona-tabs (segmented pill-bar) ----------------------- */
/* Glass-morphism segmented control — translucent white pad with a soft drop
   shadow, and a SLIDING lime indicator behind the active tab. The indicator
   is a single ::before pseudo on the bar (NOT one per tab) so transitions
   are continuous as the active tab swaps. JS writes two custom properties
   on .quest-persona-tabs (--indicator-w, --indicator-x) on init / tab change
   / resize; CSS interpolates them at 320ms ease-in-out-cubic. Active tab
   text inverts to dark on lime (vs the previous light-on-dark). */
.quest-persona-tabs-wrap {
    display: flex;
    justify-content: center;
    width: 100%;
}

.quest-persona-tabs {
    /* CRITICAL: the ::before indicator is positioned absolutely against this
       container. Without `position: relative`, the pseudo would escape to
       the nearest positioned ancestor (the sticky panel). */
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    /* Outer "pad" — translucent white over the section's white bg, lifted by
       a subtle premium shadow. Backdrop-filter is purely defensive against
       any future translucent backgrounds behind the bar; on the current
       white parent it's a no-op visually but costs nothing on idle.
       PREMIUM UPGRADE 2026-05-11: shadow stack restructured into a 3-layer
       stack — inset top highlight (premium "glassy edge"), tight mid-distance
       shadow (depth), wider ambient shadow (groundedness). The overall feel
       is closer to Apple's segmented control treatment without being heavy. */
    padding: 4px;
    /* Soft translucent surface (NOT pure white) so the bar reads as a
       discreet floating chip on the gray section rather than a hard
       white pad. 82 % alpha + the existing backdrop-filter (blur +
       saturate) reproduces the macOS / iOS segmented control treatment.
       Backdrop-filter is no-op on a pure white parent but kicks in over
       the section's lime/dark radial washes — gives a subtle vibrant
       refraction at the upper halo boundary. */
    background-color: rgba(255, 255, 255, 0.82);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    /* Slightly softer hairline border (matches the new card border
       alpha 0.07) so the bar is unified with the cards below. */
    border: 1px solid rgba(16, 24, 40, 0.07);
    border-radius: var(--radius-pill);
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): tightened the shadow
       stack and shifted the ambient layer to a slightly cooler tone to
       match the section's gray surface — less "heavy chip", more
       "refined floating control". */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.85),
        0 1px 2px rgba(16, 24, 40, 0.04),
        0 6px 20px rgba(16, 24, 40, 0.06);
}

/* Sliding lime indicator. Geometry is driven by JS via two custom
   properties:
     --indicator-w : width in px of the active tab (read from getBoundingClientRect)
     --indicator-x : translate-x in px from the bar's content-box left edge
   Defaults to 0/0 so the indicator is invisible before JS measures (avoids
   a flash of full-bar lime). The 4px top/inset accounts for the bar's own
   padding so the indicator sits flush around the active tab. */
.quest-persona-tabs::before {
    content: '';
    position: absolute;
    top: 4px;
    left: 4px;
    height: calc(100% - 8px);
    width: var(--indicator-w, 0px);
    transform: translateX(var(--indicator-x, 0px));
    /* PREMIUM UPGRADE 2026-05-11: subtle vertical gradient on the lime
       indicator (lighter at top, brand-lime at bottom) + tight inset
       highlight + soft drop-shadow halo. The halo uses the lime token
       at low alpha so the active state reads as an "energised" pill
       rather than a flat colour fill. */
    background: linear-gradient(180deg, #E8FF4D 0%, var(--color-accent) 100%);
    border-radius: var(--radius-pill);
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): trimmed the lime halo
       from 8 px @ 28 % alpha to 4 px @ 18 % alpha — keeps the active
       indicator feeling "lit" without the previous bordering-on-glow
       intensity (anti-pattern listed in the brief). The inner highlight
       + bottom inset darkening still give the pill a refined 3D feel. */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.5),
        inset 0 -1px 0 rgba(16, 24, 40, 0.07),
        0 2px 4px rgba(215, 255, 0, 0.18);
    transition:
        transform 320ms cubic-bezier(0.65, 0, 0.35, 1),
        width 320ms cubic-bezier(0.65, 0, 0.35, 1);
    z-index: 0;
    pointer-events: none;
}

.quest-persona-tab {
    /* Sit ABOVE the sliding indicator so the text reads on top of the
       lime fill (z-index 1 > pseudo's z-index 0). */
    position: relative;
    z-index: 1;

    /* Reset native button chrome. */
    appearance: none;
    -webkit-appearance: none;
    background-color: transparent;
    border: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;

    font-family: var(--font-body);
    font-size: clamp(0.875rem, 1.05vw, 0.9375rem);
    font-weight: 500;
    line-height: 1;
    color: var(--color-medium-gray);

    padding: 10px 18px;
    border-radius: var(--radius-pill);

    /* Animate ONLY color + font-weight on the tab itself — the bg is owned
       by the sliding ::before indicator now, so no background transition is
       needed here. Keeping the transition on color preserves the muted →
       primary fade for hover and the inactive → active text colour swap. */
    transition:
        color 220ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hover affordance on pointer-fine devices only — desktop hover should not
   leak onto touch where it sticks after tap. Skips both touch (coarse) and
   the active tab (no need to dim its own text). */
@media (hover: hover) and (pointer: fine) {
    .quest-persona-tab:not(.is-active):hover {
        color: var(--color-text-primary);
    }
}

.quest-persona-tab.is-active {
    /* Inverted from the old design: the active tab is now DARK TEXT on a
       LIME indicator (was: light text on dark fill). The indicator owns the
       lime fill via ::before above; this rule only adjusts the typography. */
    color: var(--color-text-primary);
    font-weight: 600;
}

.quest-persona-tab:focus-visible {
    /* Accent outline keeps the focus ring on-brand without colliding with
       the lime active fill or the white bar. Matches focus rings used on
       .btn-glass but with the accent token for visual lift in a gamified
       context. */
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/* --- 10.4 (removed) — quest-meter / quest-step-count / quest-xp-current --
   The meter chip + XP counter were retired with the redesign to a vertical
   numbered progress bar (see 10.5 below). The corresponding HTML nodes
   were removed by html-expert; JS still queries them defensively but the
   handlers are no-ops when the elements are absent. */

/* --- 10.5 .quest-steps stage + .quest-progress-bar ----------------------- */
/* The stage is a 3-column grid on desktop (zigzag layout, 2026-05-11):
     col 1 = LEFT card slot  (cards 1 & 3 land here)
     col 2 = vertical numbered progress bar (3 nodes connected by a line
             that fills lime as JS writes --progress 0..1) — CENTERED
     col 3 = RIGHT card slot (card 2 lands here)
     row 4 = completion pill, in-flow under node 3 of the bar
   The progress bar sits in normal grid flow (NOT absolute) so it always
   spans the vertical height of the 3 card rows (rows 1-3). The lime fill
   itself is a pseudo-element overlay on top of a base gray line,
   transform-scaled vertically by --progress for cheap GPU-only animation.
   The completion pill is placed in row 4, column 2 (under the bar), so
   its presence is read as the natural finish line of the 1-2-3 cadence.
   The MOBILE layout (§14) keeps the prior 2-column structure (rail + stack)
   for small viewports — see `.quest-steps` mobile override. */
.quest-steps {
    /* Sized by content (NOT `flex: 1 1 auto`) so the sticky panel can grow
       to fit it cleanly with `min-height: 100svh` (§10.1). */
    position: relative;
    width: 100%;
    max-width: var(--container-max);
    margin-inline: auto;
    margin-top: clamp(20px, 3vh, 40px);
    display: grid;
    /* DESKTOP zigzag — 3 columns. Side columns `1fr` carry the cards;
       middle column `clamp(64px, 7vw, 96px)` carries the progress bar.
       4 rows: 3 card rows (auto-sized to content) + 1 completion-pill row.
       Using `auto` (not `1fr`) so cards are sized by their intrinsic content
       and the grid never forces compression. */
    grid-template-columns: 1fr clamp(64px, 7vw, 96px) 1fr;
    grid-template-rows: auto auto auto auto;
    /* Row gap separates rows of cards; column gap separates side cards
       from the central progress bar. */
    gap: clamp(24px, 4vh, 56px) clamp(24px, 4vw, 64px);
    align-items: center;
}

/* Vertical numbered progress bar — purely visual (aria-hidden in the
   markup). 3 child <li> nodes (one per step) distributed equally along the
   column via flex space-between. The connecting line is a pair of pseudo-
   elements anchored to the bar:
     ::before — base gray line (full height, 2px wide, centered horizontally)
     ::after  — lime overlay (same geometry) scaled vertically by --progress
                from origin: top, so it grows downward as scroll advances.
   --progress is written by JS on each scroll tick (0 = empty, 1 = full).
   Default 0 keeps the bar gray-only before JS runs (graceful fallback). */
.quest-progress-bar {
    /* Desktop zigzag (2026-05-11): the bar lives in CENTER column 2,
       spanning rows 1-3 (the 3 card rows). Card 1 sits in col 1 row 1
       (left of bar), card 2 in col 3 row 2 (right of bar), card 3 in col 1
       row 3 (left of bar again). Mobile (§14) overrides this back to
       col 1 spanning the rail-then-stack 2-column layout. */
    grid-column: 2;
    grid-row: 1 / 4;
    /* Stretch vertically across all 3 card rows so the line connects the
       top of card 1 to the bottom of card 3 visually. */
    align-self: stretch;
    position: relative;
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Distribute the 3 nodes evenly along the bar's height. The first and
       last nodes hug the bar's top/bottom edges; the middle node sits at
       the geometric center. */
    justify-content: space-between;
    /* Purely visual — pointer-events disabled so the rail never absorbs
       hover / focus / click intended for the cards. */
    pointer-events: none;
}

/* Base gray line — runs the full height of the bar through the centers of
   the 3 nodes. Inset top/bottom by half a node-radius so the line stops at
   the edge of the first / last node rather than passing behind them.
   `--quest-node-size` is a local design token defined on the bar so the
   inset stays in sync with the node circle size; default 44 px (matches
   .quest-progress-node clamp midpoint). */
.quest-progress-bar {
    --quest-node-size: clamp(36px, 3.5vw, 44px);
}

.quest-progress-bar::before,
.quest-progress-bar::after {
    content: '';
    position: absolute;
    left: 50%;
    top: calc(var(--quest-node-size) / 2);
    bottom: calc(var(--quest-node-size) / 2);
    width: 2px;
    transform-origin: top;
    /* Translate the 2px-wide line to its own center so left:50% truly
       centers it. This `translate` (individual property) composes cleanly
       with the `transform: scaleY(...)` on ::after below — they are
       distinct properties, no conflict per the docs/ARCHITECTURE notes. */
    translate: -1px 0;
    border-radius: 1px;
    pointer-events: none;
}

.quest-progress-bar::before {
    /* Base track — neutral gray, always full height. */
    background-color: rgba(153, 161, 175, 0.32);
    z-index: 0;
}

.quest-progress-bar::after {
    /* Lime fill — scales vertically from 0 (empty) to 1 (full) as JS
       writes --progress on .quest-progress-bar. Sits above the base track.
       PREMIUM UPGRADE 2026-05-11: vertical gradient (lighter top → brand
       lime bottom) gives the fill a subtle "depth" cue as it grows past
       each node. Pure visual, no perf cost (background-image only). */
    background: linear-gradient(180deg, #E8FF4D 0%, var(--color-accent) 100%);
    transform: scaleY(var(--progress, 0));
    transition: transform 240ms cubic-bezier(0.65, 0, 0.35, 1);
    z-index: 1;
}

.quest-progress-step {
    /* Each step container — keeps the node above the connecting line by
       owning a higher stacking context. No size: the inner .quest-progress-node
       defines the visual circle. */
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
}

.quest-progress-node {
    width: var(--quest-node-size, clamp(36px, 3.5vw, 44px));
    height: var(--quest-node-size, clamp(36px, 3.5vw, 44px));
    border-radius: 50%;
    background-color: var(--color-light-bg);
    /* Slightly softer + lighter border so the inactive node fades into
       the section's gray surface — the user wanted "cards verrouillées
       moins éteintes (plus subtil)"; same principle applies to the
       progress nodes. */
    border: 1.5px solid rgba(153, 161, 175, 0.32);
    color: var(--color-medium-gray);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): trimmed the shadow stack
       to just the inset highlight + a hairline outer shadow so the
       inactive node sits quietly on the gray surface. The active state
       below is now NOIR (not lime) — see comment below. */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.7),
        0 1px 2px rgba(16, 24, 40, 0.05);
    /* Animate fill / border / text colour at the same speed as the lime
       line fill so the node "lights up" in lockstep with the line catching
       up to it. Box-shadow added so the active halo grows in sync. */
    transition:
        background 240ms cubic-bezier(0.65, 0, 0.35, 1),
        background-color 240ms cubic-bezier(0.65, 0, 0.35, 1),
        border-color 240ms cubic-bezier(0.65, 0, 0.35, 1),
        color 240ms cubic-bezier(0.65, 0, 0.35, 1),
        box-shadow 240ms cubic-bezier(0.65, 0, 0.35, 1);
}

.quest-progress-step.is-active .quest-progress-node {
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): INVERTED from
       lime-on-lime to NOIR-WITH-LIME-NUMBER. The active node is now a
       dark anchor (matches `.quest-step-icon`'s new noir treatment below,
       see §10.6); the number inside flips to lime via `color: var(...)`
       which the `.quest-progress-number` inherits. Halo trimmed to a
       single 3 px ambient layer at low alpha (was a two-layer 4 px / 12 px
       stack at high alpha) — keeps the "this node just lit up" moment
       without the previous "gaming badge" feel the user called out as
       anti-pattern. The lime accent on the number + the lime fill on
       the connecting line above the node together communicate the
       state change unambiguously without saturating the node itself. */
    background: var(--color-text-primary);
    border-color: var(--color-text-primary);
    color: var(--color-accent);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.10),
        inset 0 -1px 0 rgba(0, 0, 0, 0.18),
        0 0 0 3px rgba(215, 255, 0, 0.14),
        0 4px 10px rgba(16, 24, 40, 0.18);
}

.quest-progress-number {
    font-family: var(--font-body);
    font-weight: 700;
    font-size: clamp(0.875rem, 1.1vw, 1rem);
    line-height: 1;
    /* Tabular numerals — keeps the 1/2/3 visually centered in the circle
       even though they have different glyph widths in proportional fonts. */
    font-feature-settings: 'tnum' 1, 'lnum' 1;
    font-variant-numeric: tabular-nums lining-nums;
}

/* --- 10.6 .quest-step cards (single-column stack in grid col 2) ---------- */
.quest-step {
    /* Cards stack vertically in column 2, one per grid row. Dimmed by
       default (locked); `.is-unlocked` (added by JS) restores opacity +
       nudges a subtle scale.

       LAYOUT FIX 2026-05-11: previously this rule combined
       `align-self: center` + `min-height: 0`. That pair allowed each
       card's BOX to shrink below its intrinsic content height while the
       content (icon + title + body) kept rendering at full size with the
       default `overflow: visible`. The result: content bled out of the
       box into the neighbouring `1fr` grid track and the 3-layer card
       shadow was drawn at the box edge — visibly cutting through the
       next card's text. Symptom reported: "card 2 overlaps card 1,
       card 3 overlaps card 2" on every viewport.

       Fix: `align-self: stretch` (replaces `center`) — the card now
       FILLS its `1fr` cell exactly, so the box is always ≥ content
       height as long as the cell is. The icon + title + body stay
       top-aligned inside the card via the inner flex layout
       (`align-items: flex-start`); empty space (if any) sits below
       them inside the card, painted by the card's gradient fill.

       `min-height: 0` removed — was only needed when the card
       could be SMALLER than content; now obsolete. The grid's
       `1fr 1fr 1fr` distribution still divides the sticky panel's
       available height equally, so on viewports where 3 × min-content
       would have exceeded 100svh the sticky's `overflow: hidden`
       (§10.1) is the safety net of last resort — combined with the
       `padding-bottom` buffer on `.how-quest` (§10.1), the FAQ stays
       protected even in that worst-case clipping.
       DESKTOP ZIGZAG 2026-05-11: per-step `grid-column` overrides below
       place card 1 + 3 in left col, card 2 in right col. The base
       `grid-column: 2` here is a sensible mobile fallback (the mobile
       2-col layout uses col 2 for all stacked cards) — do not remove
       even if it looks redundant alongside the per-step rules. */
    grid-column: 2;
    justify-self: stretch;
    align-self: stretch;
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: clamp(8px, 1vh, 14px);

    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): layered surface treatment
       refined for a more "premium app card" feel.
       - `background: linear-gradient` from a slight off-white to pure white
         (top→bottom) gives the card a subtle "lit from above" sheen
         without breaking the white-card aesthetic.
       - `border` slightly bumped (7 % gray) so the card reads as more
         distinctly framed against the section's light-gray surface and
         the new ambient lime halo on the section background.
       - `box-shadow` is a 3-layer stack: a tight 1px inner highlight at
         the top edge (inset white) for the premium "glassy edge" feel;
         a soft mid-distance shadow for visual lift; a wider ambient
         shadow grounded to the surface. The ambient was bumped from
         0.04 → 0.05 alpha and the spread widened from 12 → 18 px so the
         cards FLOAT a touch more distinctly. All still extremely soft —
         well within the épuré contract.
       - Padding bumped on the floor side (20 → 22 px) so the icon +
         title + body have a touch more breathing room inside the card. */
    background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.985) 100%);
    border: 1px solid rgba(16, 24, 40, 0.07);
    border-radius: var(--radius-card);
    padding: clamp(22px, 2.2vw, 36px);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.85),
        0 1px 2px rgba(16, 24, 40, 0.04),
        0 6px 18px rgba(16, 24, 40, 0.05);
    /* Cap card width so long body text doesn't stretch all the way across
       the column on wide viewports. */
    max-width: 56ch;
    width: 100%;
    /* Intentionally NO `overflow: hidden` on the card. With
       `align-self: stretch` the box always covers its content, so
       clipping is never required for the card's own children. Keeping
       the default `overflow: visible` also leaves the door open for
       future hover lifts or focus rings that would otherwise be cut
       at the card edge. The sticky panel's `overflow: hidden` (§10.1)
       remains the safety net of last resort against rare short-viewport
       overflows. */

    /* Locked-state opacity bumped 0.42 → 0.6 (user request 2026-05-11
       iteration 2: "cards verrouillées moins éteintes — plus subtil").
       The card content stays readable through the lock; the unlock
       transition is now a refined +scale +opacity lift rather than a
       binary "dim → on" flip. The lime progress fill catching up to
       each node + the `.is-unlocked` ramp below still communicate
       progression unambiguously. */
    opacity: 0.6;
    /* Subtle scale baseline — the unlocked state ramps to 1 (a 2 % swell
       that registers as "this card just lit up" without being theatrical).
       Use individual `scale` so it does not collide with any future
       `transform` shorthand. */
    scale: 0.98;
    transition:
        opacity 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
        scale 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
        border-color 360ms cubic-bezier(0.2, 0.8, 0.2, 1),
        box-shadow 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Step row + column assignment for the DESKTOP zigzag (2026-05-11):
     card 1 → col 1 row 1 (left of bar)
     card 2 → col 3 row 2 (right of bar)
     card 3 → col 1 row 3 (left of bar again)
   The cards are oriented "toward the bar" via `align-items` on the inner
   flex (cards 1 + 3 right-align their content; card 2 stays left-aligned).
   Mobile (§14) re-routes all three back to col 2 with sequential rows for
   the rail-then-stack 2-column layout. */
.quest-step[data-step="1"] {
    grid-column: 1;
    grid-row: 1;
    align-items: flex-end;
    text-align: right;
}
.quest-step[data-step="2"] {
    grid-column: 3;
    grid-row: 2;
    align-items: flex-start;
    text-align: left;
}
.quest-step[data-step="3"] {
    grid-column: 1;
    grid-row: 3;
    align-items: flex-end;
    text-align: right;
}

/* Per-step accent strip + Étape badge corner placement.
   Cards 1 + 3 (col 1, oriented right toward the central bar): the lime
   accent sits on the card's RIGHT inner edge (default already set on
   `.quest-step::before`) and the Étape badge sits in the TOP-LEFT
   corner (default already set on `.quest-step::after`). No explicit
   override needed — listed below as commented anchors so the intent is
   visible without grepping the default. Card 2 (col 3, oriented left
   toward the bar) must MIRROR these defaults: accent on the LEFT edge,
   badge in the TOP-RIGHT corner. */
.quest-step[data-step="2"]::before {
    right: auto;
    left: 0;
}
.quest-step[data-step="2"]::after {
    left: auto;
    right: clamp(14px, 1.6vw, 22px);
}

/* Lime accent strip on the LEFT edge of each card REMOVED 2026-05-11
   (user request): the previous `.quest-step::before` (3px lime bar fading
   in via opacity 0 → 1 on `.is-unlocked`) read as too loud against the
   épuré aesthetic. The unlock signal now comes solely from the existing
   trio: opacity 0.42 → 1, scale 0.98 → 1, and the shadow upgrade in
   `.quest-step.is-unlocked` below — plus the corresponding numbered node
   on the `.quest-progress-bar` lighting up lime.

   RE-INTRODUCED 2026-05-11 (iteration 2): the user's "premium iteration"
   brief explicitly allowed "petit accent vertical lime POSSIBLE sur la
   gauche", contingent on being subtle. Re-added here as a 2 px lime bar
   anchored to the card's INNER edge (the edge facing the central
   progress bar — col 1 cards have it on their RIGHT, col 3 card has it
   on its LEFT) so each card visually "plugs into" the bar. Default
   opacity 0 (locked) → ramps to 0.85 on `.is-unlocked` via the
   transition declared on `.quest-step` (opacity + scale + border-color
   + box-shadow). Per-step orientation is set under the
   `.quest-step[data-step="…"]` blocks below (left vs right inset
   override). Height inset by 16 % top + bottom so the accent reads as
   a focused tab on the card's "spine", not a full-height stripe.
   `pointer-events: none` keeps it purely decorative. */
.quest-step::before {
    content: '';
    position: absolute;
    top: 16%;
    bottom: 16%;
    width: 2px;
    border-radius: 2px;
    background: linear-gradient(180deg, #E8FF4D 0%, var(--color-accent) 100%);
    opacity: 0;
    transition: opacity 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
    pointer-events: none;
    /* Default inner-edge = right (card 1 + 3 sit in col 1, facing the
       central bar). Card 2 (col 3) overrides this to `left: 0` below. */
    right: 0;
    left: auto;
}

.quest-step.is-unlocked::before {
    opacity: 0.85;
}

/* Étape N badge — appears at the OUTER top corner of each card (far
   from the central progress bar). Uses `attr(data-step)` to read the
   `data-step="1|2|3"` set in HTML. No HTML change required — the
   data-attribute is already part of the JS contract (used by
   carousel-style identification). Z-index 1 keeps it above the card's
   background gradient but below any future inner content; the card's
   own `position: relative` (declared above) is the anchor.
   Per-step CORNER placement is set under the `.quest-step[data-step]`
   blocks below: cards 1 + 3 (col 1, oriented "right") → badge in
   top-LEFT; card 2 (col 3, oriented "left") → badge in top-RIGHT.
   Mobile (§14) overrides all three to top-LEFT since the layout
   collapses to a left-aligned single-column stack. */
.quest-step::after {
    content: 'Étape ' attr(data-step);
    position: absolute;
    top: clamp(14px, 1.6vw, 22px);
    font-family: var(--font-body);
    font-size: clamp(0.62rem, 0.7vw, 0.7rem);
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(16, 24, 40, 0.34);
    pointer-events: none;
    /* Default corner = top-left (card 1 + 3 in col 1 — see per-step
       overrides below for the col-3 card 2 mirror). */
    left: clamp(14px, 1.6vw, 22px);
    right: auto;
    transition: color 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Unlocked badge state — opacity stays at 1 via the card's own opacity
   transition; this rule just bumps the badge's text contrast a touch
   when the card is "alive" so the badge reads as part of the unlocked
   card's identity rather than a faded label. */
.quest-step.is-unlocked::after {
    color: rgba(16, 24, 40, 0.48);
}

.quest-step.is-unlocked {
    opacity: 1;
    scale: 1;
    border-color: rgba(16, 24, 40, 0.10);
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): the unlocked state gets
       a more distinct lift — the locked state opacity is now 0.6 (was
       0.42), so the unlock delta needed to land harder elsewhere. The
       ambient shadow widens from 24 → 32 px and the mid shadow bumps
       from 0.05 → 0.06 alpha; the inner highlight stays at 0.9 white.
       Combined with the new `.is-unlocked::before` lime accent strip
       (separate rule above) and the unlocked badge color bump, the
       "this card is now alive" delta is unambiguous without
       theatricality. */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.9),
        0 1px 2px rgba(16, 24, 40, 0.06),
        0 10px 32px rgba(16, 24, 40, 0.07);
}

.quest-step-icon {
    /* Icon slot — js-expert injects an inline SVG via innerHTML. The
       slot is a fixed pad so a missing/empty SVG still occupies the
       layout (no jump when the icon mounts asynchronously).

       PREMIUM UPGRADE 2026-05-11 (iteration 2): INVERTED from lime pad
       with dark icon to NOIR pad with LIME icon — the user's explicit
       proposal in the brief, matched 1:1. The injected SVGs use
       `stroke="currentColor"` so flipping the parent's `color` to lime
       propagates to every path. Rationale: dark anchors + lime accents
       creates a cohesive DA across the section — the icon containers,
       the active progress nodes (also inverted to noir, see §10.5
       above), and the new noir completion pill (§10.7 below) form a
       consistent visual family. Lime is reserved for the energising
       accents (progress fill, icon strokes, completion pill text, the
       eyebrow dot).

       - Background flipped to `var(--color-text-primary)` (#101828).
       - `color` set to `var(--color-accent)` so the SVG strokes paint lime.
       - Inset highlight bumped to a quieter alpha (was 50 % white on lime,
         now 12 % white on dark — same "lit-from-above" effect but
         calibrated to the new dark fill).
       - Drop shadow swapped from a lime halo to a tight neutral
         drop-shadow (16,24,40 @ 22 %) so the chip reads as a refined
         dark badge, not a glowing lime accent. Border-radius unchanged
         at 16 px. */
    width: clamp(40px, 4vw, 56px);
    height: clamp(40px, 4vw, 56px);
    border-radius: 16px;
    background: var(--color-text-primary);
    color: var(--color-accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.12),
        inset 0 -1px 0 rgba(0, 0, 0, 0.25),
        0 2px 6px rgba(16, 24, 40, 0.18);
}

.quest-step-icon svg {
    width: 60%;
    height: 60%;
}

.quest-step-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(1.125rem, 1.8vw, 1.5rem);
    line-height: 1.2;
    letter-spacing: -0.01em;
    color: var(--color-text-primary);
    margin: 0;
}

.quest-step-body {
    font-family: var(--font-body);
    font-size: clamp(0.9375rem, 1.2vw, 1rem);
    line-height: 1.55;
    color: rgba(16, 24, 40, 0.74);
    margin: 0;
}

/* `.quest-card-status` rules REMOVED 2026-05-11 — html-expert removed the
   <span class="quest-card-status"> nodes from the markup; the unlock state is
   now communicated solely by the `.is-unlocked` class on `.quest-step` (which
   ramps opacity 0.42 → 1 + scale 0.98 → 1, see rule above) and by the lime
   fill of the corresponding `.quest-progress-node`. The transition that
   animated this badge's color/background is gone with the rule. NOTE:
   script.js still reads `.quest-card-status` defensively via
   `step.querySelector('.quest-card-status')` and guards every write with
   `if (status) ...`, so the now-null lookup is a harmless no-op — no JS
   refactor needed for the CSS deletion to land safely. If/when js-expert
   cleans those queries, no CSS work is required here. */

/* --- 10.7 .quest-completion-pill ----------------------------------------- */
/* Reveals once all three steps are unlocked. Markup ships with the [hidden]
   attribute; js-expert removes [hidden] and adds .is-visible. The hidden
   attribute applies `display: none` natively (browser UA), so transitions
   do NOT need to fight a display property — when the attribute is removed
   the element starts at opacity:0 + scale:0.96, then .is-visible animates
   it in.
   PLACEMENT REWORK 2026-05-11: previously `position: absolute` at the
   bottom of `.quest-steps`. Now placed IN-FLOW within the grid as row 4,
   directly under node 3 of the progress bar (col 2 desktop, full-width on
   mobile via §14). This communicates the pill as the natural finish line
   of the 1-2-3 cadence — visually tied to node 3 with a deliberate gap
   (`margin-top: clamp(24px, 5vh, 56px)`) so it reads as "after" the
   sequence rather than the next step. JS contract preserved: `[hidden]`
   still gates initial visibility; `.is-visible` still animates opacity +
   scale. */
.quest-completion-pill {
    grid-column: 2;
    grid-row: 4;
    justify-self: center;
    align-self: start;
    margin-top: clamp(24px, 5vh, 56px);
    z-index: 2;

    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 13px 26px 13px 22px;
    border-radius: var(--radius-pill);
    /* PREMIUM UPGRADE 2026-05-11 (iteration 2): INVERTED from lime fill +
       lime halo to NOIR fill + LIME TEXT — the user's brief explicitly
       requested "moins badge jeu vidéo, plus validation premium, fond
       noir possible, accent lime subtil". The new treatment:
       - background: dark (matches the icon containers + active progress
         nodes, completes the noir-anchors / lime-accents DA family).
       - color: lime — the text reads as a high-contrast brand stamp on
         the dark pill.
       - inset highlight bumped to a quieter 12 % white on dark.
       - shadow stack reduced to a single neutral mid-distance drop
         (16,24,40 @ 22 %) + a wider subtle ambient (16,24,40 @ 12 %).
         No more lime halos — the previous double-lime drop shadow was
         the chief "badge jeu vidéo" offender. Padding adjusted (left
         pad 22 px slightly tighter than right 26 px) to host the lime
         dot prefix introduced via `::before` below; the dot sits in
         the left padding tighter against the text. */
    background: var(--color-text-primary);
    color: var(--color-accent);
    font-family: var(--font-body);
    font-weight: 700;
    font-size: clamp(0.9375rem, 1.3vw, 1.0625rem);
    letter-spacing: 0.02em;
    line-height: 1;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.12),
        inset 0 -1px 0 rgba(0, 0, 0, 0.25),
        0 4px 10px rgba(16, 24, 40, 0.22),
        0 12px 28px rgba(16, 24, 40, 0.12);

    /* Initial state on reveal — JS removes [hidden] (display switches from
       none to inline-flex), then adds .is-visible on the next frame for the
       transition to play. */
    opacity: 0;
    scale: 0.96;
    transition:
        opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1),
        scale 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.quest-completion-pill.is-visible {
    opacity: 1;
    scale: 1;
}

/* Lime dot prefix inside the noir completion pill — pure-decoration
   "ready to play" marker that anchors the new dark-on-light DA family.
   Sized small (6 px) so it reads as a refined LED rather than a button.
   The dot is inside the pill's flex layout via `display: inline-block`
   so it sits naturally to the LEFT of the text (the pill's `gap: 10px`
   spaces it from the text). Note: js-expert's `textContent` write
   (script.js L999, `completionPill.textContent = content.completion`)
   does NOT remove this pseudo-element — pseudo-elements are CSS-only,
   immune to textContent / innerHTML writes that target the host
   element's DOM children. */
.quest-completion-pill::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--color-accent);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.45),
        0 0 0 2px rgba(215, 255, 0, 0.18);
    flex: none;
}

/* Defensive: the base `.quest-completion-pill` rule above sets
   `display: inline-flex`, which has higher CSS specificity than the UA
   default `[hidden] { display: none }` and would unintentionally render
   the pill while the [hidden] attribute is still on the element. This
   restores the spec-correct hiding behaviour so [hidden] works as
   expected before JS toggles it off. */
.quest-completion-pill[hidden] {
    display: none;
}


/* ==========================================================================
   11. FAQ ACCORDION
   ========================================================================== */
.faq {
    /* `position: relative` + `z-index: 1` form the defensive pillar that
       guarantees the FAQ ALWAYS paints above any rendering of the preceding
       `.how-quest` section. Previously there was a documented overlap bug:
       on certain viewports the third quest card or the sticky panel's
       overflowing content could render visually atop the FAQ title.
       The structural fix is in §10 (`.how-quest-sticky { overflow: hidden }`
       + `.how-quest { padding-bottom }`) — this z-index is the SAFETY NET
       that ensures even an unforeseen overflow regression cannot ever
       paint over the FAQ. `.how-quest` now also has `z-index: 0`, so the
       stacking context relationship is explicit and ordered:
         .how-quest  (z-index: 0, position: relative)
         .faq        (z-index: 1, position: relative) ← always on top
       Inverted 2026-05-11 from light-gray to white. The preceding
       `.how-quest` section now owns the light-gray slot; the contrast
       between gray (how-quest) and white (faq) creates the section
       boundary visually — no border or separator needed at the
       how-quest → faq seam (the bg flip is the demarcation). */
    position: relative;
    z-index: 1;
    background-color: var(--color-light-bg);
    padding-block: clamp(24px, 4vw, 52px) clamp(80px, 12vw, 140px);
    padding-inline: var(--page-pad);
}

.faq-eyebrow {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-medium-gray);
    margin-bottom: 16px;
}

.faq-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(2rem, 4.5vw, 3rem);
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--color-text-primary);
    margin-bottom: clamp(32px, 4vw, 56px);
}

.faq-list {
    display: flex;
    flex-direction: column;
    border-top: 1px solid rgba(16, 24, 40, 0.10);
}

.faq-item {
    border-bottom: 1px solid rgba(16, 24, 40, 0.10);
}

.faq-toggle {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding-block: clamp(20px, 2.5vw, 28px);
    padding-inline: 0;
    background: transparent;
    border: none;
    cursor: pointer;
    text-align: left;
    font-family: var(--font-body);
    font-size: clamp(1rem, 1.6vw, 1.125rem);
    font-weight: 600;
    color: var(--color-text-primary);
    transition: color var(--transition);
}

.faq-toggle:hover {
    color: rgba(16, 24, 40, 0.7);
}

.faq-toggle:focus-visible {
    outline: 2px solid var(--color-text-primary);
    outline-offset: 4px;
    border-radius: 4px;
}

.faq-question {
    flex: 1;
    line-height: 1.4;
}

.faq-icon {
    flex: none;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: var(--color-accent);
    color: var(--color-text-black);
    rotate: 0deg;
    transition: rotate 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.faq-icon svg {
    display: block;
    width: 18px;
    height: 18px;
}

.faq-item.is-open .faq-icon {
    rotate: 180deg;
}

.faq-answer {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.faq-item.is-open .faq-answer {
    grid-template-rows: 1fr;
}

.faq-answer-inner {
    overflow: hidden;
}

.faq-answer-inner p {
    font-family: var(--font-body);
    font-size: clamp(0.9375rem, 1.4vw, 1rem);
    line-height: 1.6;
    color: rgba(16, 24, 40, 0.78);
    max-width: 72ch;
    padding-bottom: 0;
    transition: padding-bottom 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.faq-item.is-open .faq-answer-inner p {
    padding-bottom: clamp(20px, 2.5vw, 28px);
}


/* ==========================================================================
   12. FOOTER
   --------------------------------------------------------------------------
   Horizontal gutter mirrors the hero / screens-rail / faq sections (padding-inline:
   var(--page-pad)) so the footer's left/right edges line up with the lime hero card's
   edges across every viewport. Vertical padding is unchanged.
   Background matches the preceding .faq section (var(--color-light-bg) — inverted
   2026-05-11 from the previous light-gray) so the page ends on a continuous white
   surface — épuré / editorial aesthetic preserved. No top border / divider on the
   FAQ → footer seam: the two white surfaces are intentionally allowed to merge
   into one continuous block (design choice 2026-05-11, reverted from the brief
   `border-top: 1px solid var(--color-divider)` introduced in commit b3ef309).
   The footer's vertical padding alone delineates it from the FAQ list above.
   Text is dark (var(--color-text-primary)) for legibility on the light background.
   ========================================================================== */
.footer {
    background-color: var(--color-light-bg);
    color: var(--color-text-primary);
    padding-block: clamp(64px, 10vw, 96px) 28px;
    padding-inline: var(--page-pad);
}

/* Inner wrapper is now a pure flow container — no extra horizontal padding, no
   max-width cap. The outer .footer's padding-inline already provides the same gutter
   as the lime card, and dropping the cap lets the footer grid span the full inner
   width instead of being centered inside a 1200px column. */
.footer-inner {
    width: 100%;
}

/* Inline horizontal list of legal/contact links — editorial footer pattern (Vercel /
   Linear / Stripe). Generous gap so the row breathes; flex-wrap lets the 5 items reflow
   onto a second line on narrow viewports without manual breakpoints. No separators —
   whitespace alone delineates items, per the épuré aesthetic.
   `justify-content: center` horizontally centers the row of links across every
   viewport, mirroring the centered .footer-bottom copyright line below. */
.footer-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: clamp(20px, 3vw, 32px);
    list-style: none;
    padding: 0;
    margin: 0;
}

.footer-list a {
    color: var(--color-text-primary);
    font-size: 14px;
}

.footer-list a:hover {
    opacity: 0.7;
}

/* Sits below the inline links — quiet copyright line. Top margin (rather than border-top)
   keeps the visual weight minimal; small type + centered alignment + dark text on the
   light-gray surface keep it discreet without disappearing. */
.footer-bottom {
    margin-top: clamp(28px, 4vw, 40px);
    color: var(--color-text-primary);
    font-size: 13px;
    text-align: center;
}


/* ==========================================================================
   13. TOAST
   ========================================================================== */
.toast {
    position: fixed;
    top: 24px;
    left: 50%;
    transform: translate(-50%, -120%);
    background-color: var(--color-dark-bg);
    color: var(--color-text-white);
    padding: 14px 24px;
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-dark);
    font-size: 15px;
    font-weight: 500;
    z-index: 1000;
    transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms;
    opacity: 0;
    pointer-events: none;
    max-width: calc(100vw - 32px);
    text-align: center;
}
.toast.visible {
    transform: translate(-50%, 0);
    opacity: 1;
}


/* ==========================================================================
   14. QR WIDGET (floating, bottom-right, desktop/tablet only)
   --------------------------------------------------------------------------
   Glass-morphism floating panel pinned to the bottom-right of the viewport
   on desktop/tablet. Hosts a JS-generated QR code (SVG injected into
   `#qrWidgetCode` by script.js) pointing at `/download/` — the redirect
   page that UA-routes iOS/Android visitors to their respective store.

   Layout: column flex — QR on top, caption below. The QR's `<svg>` is
   injected by JS at runtime (with an embedded wordmark at the center); we
   only style its containing `.qr-widget-code` box and the descendant `<svg>`
   so it fills the box edge-to-edge.

   Stacking context (z-index: 100): sits above page content (.hero z=1,
   .screens-rail z=2, .wordmark z=50) but below ephemeral overlays
   (.toast z=1000, .skip-link z=1000). The widget is permanent UI, not an
   overlay — never blocks a toast notification or a focused skip-link.

   Mobile (<= 767px): widget is `display: none`. Scanning a QR with the
   device you're holding makes no sense — the visitor would tap a store
   button on the same device instead.
   ========================================================================== */
.qr-widget {
    /* Pinned bottom-right of the viewport. `fixed` so it stays put during
       page scroll. clamp() on offsets keeps comfortable breathing room at
       both small laptops (16px) and large desktops (24px). */
    position: fixed;
    bottom: clamp(16px, 2vh, 24px);
    right: clamp(16px, 2vw, 24px);
    z-index: 100;

    /* Glass-morphism container. Translucent white panel + heavy blur +
       saturation lift behind it (same recipe as `.wordmark.is-condensed`
       at line 272: rgba(255,255,255,0.72) + blur(14px) saturate(140%), but
       bumped here — the widget is larger and needs a softer frost). The
       border is a hairline of near-transparent dark to give the edge
       definition on light backgrounds. The shadow is a two-layer drop
       (long soft + short sharp) for a premium float without harshness. */
    background-color: rgba(255, 255, 255, 0.70);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid rgba(16, 24, 40, 0.06);
    border-radius: clamp(16px, 1.8vw, 24px);
    box-shadow:
        0 12px 32px rgba(16, 24, 40, 0.08),
        0 2px 6px rgba(16, 24, 40, 0.04);

    /* Layout — column stack, content centered. Padding scales with the
       widget itself; the gap controls the vertical rhythm between QR and
       caption. Both padding and gap were tightened (compared to the
       earlier 12–20px / 8–14px ranges) to match the smaller QR footprint
       below — the panel should stay visually proportional, not look like
       a giant frame around a small QR. */
    padding: clamp(8px, 1vw, 14px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(6px, 0.8vh, 10px);

    /* The widget is sized by its content (the QR's clamp width determines
       the panel's intrinsic width). No max-width: the JS-injected SVG
       expands to fill `.qr-widget-code` so the panel width is the QR
       width + 2 × padding. */
}

/* Backdrop-filter fallback — bump opacity so the panel stays legible
   without the blur (same fallback strategy as the wordmark, line 290). */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .qr-widget {
        background-color: rgba(255, 255, 255, 0.94);
    }
}

/* QR container — JS injects an `<svg>` inside this div at load (with an
   embedded wordmark at the center via a knockout square + foreignObject
   or nested SVG). We size the BOX explicitly (clamp) so the panel width
   is deterministic before the SVG renders (no layout shift); the
   descendant `<svg>` rule below makes the QR fill the box edge-to-edge
   once injected.

   Sizing range (96–128 px): drastically tightened from the previous
   160–240 px. The widget is a discreet desktop affordance, not a hero
   element — a 96-px QR remains scannable from typical laptop distance
   (~50 cm) with version-3 error-correction L modules (≈ 2.7 px each).
   Caps at 128 px on 4K screens to keep the panel from drawing the eye
   away from the page content. */
.qr-widget-code {
    width: clamp(96px, 9vw, 128px);
    height: clamp(96px, 9vw, 128px);
}

.qr-widget-code svg {
    width: 100%;
    height: 100%;
    display: block;
}

.qr-widget-caption {
    font-family: var(--font-body);
    font-size: clamp(11px, 0.85vw, 12px);
    font-weight: 500;
    color: var(--color-medium-gray);
    margin: 0;
    text-align: center;
    line-height: 1.3;
}


/* ==========================================================================
   15. DOWNLOAD PAGE (website/download/index.html)
   --------------------------------------------------------------------------
   Utility "coming soon" landing page reached by the QR widget (and by
   visitors of appyamatch.fr/download). Layout is minimal: centered column,
   wordmark on top, eyebrow + title + body + back-link. Reuses every design
   token from `:root` so the page stays visually consistent with the home.

   The page links `../styles.css`, so the existing `.wordmark` rule (section
   6) cascades down — but its `position: fixed` + `translate: -50% -50%`
   anchor the logo to the top-center of the viewport. On a short utility
   page that's the wrong behavior: the wordmark should flow in the layout
   above the hero, not float over it. The `.download-page .wordmark`
   override below restores static positioning for this page only.
   ========================================================================== */
.download-page {
    min-height: 100svh;
    /* Fallback for older Safari/Firefox without svh support. The download
       page has no dynamic URL-bar concerns at scroll = 0 (there is no
       scrollable content), so 100vh is acceptable here, but svh is still
       preferred when available for consistency. */
    min-height: 100vh;
    min-height: 100svh;

    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Block padding ≈ 8 vh provides comfortable breathing room above the
       wordmark and below the back link on tall viewports without dwarfing
       the content on short ones. Inline padding mirrors --page-pad. */
    padding: clamp(40px, 8vh, 96px) clamp(24px, 6vw, 80px);
    gap: clamp(40px, 8vh, 80px);

    background-color: var(--color-light-bg);
    text-align: center;
}

/* Wordmark override — on the home, `.wordmark` is `position: fixed;
   translate: -50% -50%; top: calc(var(--page-pad) / 2); left: 50%` (section
   6, line 220) so it floats over the lime hero card. On this utility page
   we want the wordmark to flow in the document like a normal logo, sized
   by the `.svg { height: 14px / 20px }` rule already declared in section 6.
   Resetting `position`, `top`, `left`, `translate`, and the legacy
   `transform: translateZ(0)` (still inherited from .wordmark line 255)
   makes the link a plain inline-flex element centered by `.download-page`'s
   `align-items: center`. The entrance keyframe (`wordmarkEntrance`) still
   plays — just on a static-positioned element instead of a fixed one. */
.download-page .wordmark {
    position: static;
    top: auto;
    left: auto;
    translate: none;
    transform: none;
}

.download-hero {
    /* Constrain the editorial column so the body text doesn't sprawl to
       full viewport width on desktop. 56ch is the same prose-column width
       the FAQ uses. */
    max-width: 56ch;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(12px, 1.5vh, 20px);
}

/* Eyebrow — matches the visual recipe used by `.faq-eyebrow` / similar
   small-caps overlines elsewhere on the site: Inter 600, 12px, wide
   tracking, uppercase, medium-gray. */
.download-eyebrow {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-medium-gray);
    margin: 0;
}

/* Title — same clamp range as the main page section headings (FAQ /
   how-quest titles) so the type hierarchy reads consistently. Roboto 700
   with tight leading + negative tracking for editorial polish. */
.download-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(2rem, 4.5vw, 3rem);
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--color-text-primary);
    margin: 0;
}

.download-body {
    font-family: var(--font-body);
    font-size: clamp(1rem, 1.2vw, 1.125rem);
    font-weight: 400;
    line-height: 1.6;
    /* Slightly muted vs `--color-text-primary` so the title remains the
       dominant typographic weight. rgba(16,24,40,0.74) matches the muted
       body color used by other section subtitles on the site. */
    color: rgba(16, 24, 40, 0.74);
    margin: 0;
}

/* Back-link — primary CTA on this page. Pill button, dark fill, white
   text. Mirrors the visual weight of `.btn-glass` (section 7.6) but kept
   simpler (no glass/shine) since this page has no competing brand
   surface. */
.download-back {
    margin-top: clamp(12px, 2vh, 24px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: clamp(12px, 1.4vh, 16px) clamp(20px, 2.5vw, 32px);

    background-color: var(--color-text-primary);
    color: var(--color-text-white);

    font-family: var(--font-body);
    font-weight: 600;
    font-size: clamp(0.9rem, 1vw, 1rem);
    text-decoration: none;

    border-radius: var(--radius-pill);
    /* Override the generic `a` transition (color, opacity — line 150) so
       background-color also animates. Duration tightened to 220ms ease
       per the standard `--transition` shape. */
    transition:
        background-color 220ms cubic-bezier(0.4, 0, 0.2, 1),
        color 220ms cubic-bezier(0.4, 0, 0.2, 1);
}

.download-back:hover {
    background-color: var(--color-button-glass-hover);
}

.download-back:focus-visible {
    /* Higher-contrast focus ring than the generic `:focus-visible` (which
       outlines in --color-text-primary on a page that has --color-text-primary
       as the button fill → invisible). Use the brand accent. */
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
}


/* ==========================================================================
   16. LEGAL PAGES (CGU, politique annulation/remboursement, politique
       confidentialité, contact, suppression de compte, mentions légales)
   --------------------------------------------------------------------------
   Long-form editorial layout shared by every static "legal" page under
   website/<slug>/index.html. Each page links ../styles.css and renders a
   <main class="legal-page"> with a centred 72ch reading column, sober
   typography, and a footer divider + back link + cross-nav.

   Design contract:
   - Sober and professional. No lime glow or playful motion — these pages are
     read carefully, not skimmed. The lime accent appears only on the <mark>
     placeholder highlight and the back-link focus ring.
   - Tokens only — no hex / font / radius / duration outside :root.
   - Long-form reading: 72ch column max, line-height 1.7 body / 1.65 mobile,
     slightly muted body color (rgba 0.84) for comfort.
   - <mark class="legal-page-placeholder"> is the "needs-to-be-filled" marker
     for the legal review pass: lime tint + dashed underline so the editor
     sees every TODO at a glance.

   Distinct from .download-page (section 15): the download page is a single
   centred utility hero ("coming soon" + back CTA). Legal pages are full
   long-form articles. The two patterns are kept separate to avoid coupling
   their evolution; only the wordmark-reset recipe is duplicated, which is a
   2-property pattern not worth abstracting.

   The blanket `*, *::before, *::after { transition-duration: 0.01ms !important }`
   under prefers-reduced-motion already collapses the hover transitions
   declared below — no extra reduced-motion rules required for this section.
   ========================================================================== */

/* Page shell — full-viewport column. Same wordmark-reset recipe as
   .download-page (the .wordmark default is `position: fixed; translate:
   -50% -50%` from section 6, which is wrong for an in-flow legal layout). */
.legal-page {
    min-height: 100vh;          /* fallback */
    min-height: 100svh;         /* iOS 15.4+ stable URL bar */
    background-color: var(--color-light-bg);
    /* Inline padding mirrors .download-page; block padding is intentionally
       smaller at the top so the wordmark sits closer to the viewport edge. */
    padding: clamp(40px, 8vh, 96px) clamp(24px, 6vw, 80px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(40px, 6vh, 64px);
}

/* Wordmark override — identical to .download-page .wordmark (see section 15
   commentary). Resets the fixed-positioned floating logo back to in-flow
   so the layout's flex column places it cleanly above the article. */
.legal-page .wordmark {
    position: static;
    top: auto;
    left: auto;
    translate: none;
    transform: none;
}

/* Article container — 72ch reading column. Wider than the .download-hero
   56ch utility column because legal text needs more characters per line
   (citations, article references, URLs) before wrapping awkwardly. */
.legal-page-article {
    width: 100%;
    max-width: 72ch;
}

/* ---- Header (eyebrow / title / updated date) ---- */
.legal-page-header {
    text-align: center;
    margin-bottom: clamp(40px, 6vh, 72px);
}

/* Eyebrow — same recipe as .download-eyebrow / .faq-eyebrow (Inter 600,
   12px, tracking 0.16em, uppercase, medium-gray). */
.legal-page-eyebrow {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-medium-gray);
    margin: 0 0 16px;
}

/* Title — clamp range matches .download-title / section headings sitewide. */
.legal-page-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(2rem, 4.5vw, 3rem);
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--color-text-primary);
    margin: 0 0 24px;
}

/* Last-updated date — small Inter line, medium-gray, below the title. */
.legal-page-updated {
    font-family: var(--font-body);
    font-size: 0.875rem;
    color: var(--color-medium-gray);
    margin: 0;
}

/* ---- Body — long-form typography ---- */
.legal-page-body {
    font-family: var(--font-body);
    /* Comfortable body size that scales between 15px (small viewports) and
       16px (desktop). Long legal text reads best at 16px; we stay at the
       16px end on desktop and only ease down on very narrow mobile. */
    font-size: clamp(0.9375rem, 1.1vw, 1rem);
    /* 1.7 line-height is generous for legal reading — heavier than the
       default body 1.6 / paragraph 1.65, mirroring well-typeset terms-of-
       service pages. Tightened to 1.65 on mobile (see media block below). */
    line-height: 1.7;
    /* Slightly muted from --color-text-primary (#101828) for reading
       comfort over long sessions. The titles stay at full --color-text-primary
       to preserve the typographic hierarchy. */
    color: rgba(16, 24, 40, 0.84);
}

/* Section wrappers — vertical rhythm between articles / sections. */
.legal-page-section {
    margin-bottom: clamp(32px, 5vh, 56px);
}
.legal-page-section:last-child {
    margin-bottom: 0;
}

/* Section heading (H2) — Roboto 700, mid-clamp to sit comfortably below
   the page H1 without competing. Negative tracking matches the rest of the
   heading family. */
.legal-page-section-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: clamp(1.25rem, 2vw, 1.5rem);
    line-height: 1.3;
    letter-spacing: -0.01em;
    color: var(--color-text-primary);
    margin: 0 0 16px;
}

/* Subsection heading (H3) — Inter 600 (NOT Roboto) to clearly differentiate
   from the H2 and read as a sub-rank. Top margin restores breathing room
   when subsections follow a paragraph. */
.legal-page-subsection-title {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: clamp(1rem, 1.4vw, 1.125rem);
    line-height: 1.4;
    color: var(--color-text-primary);
    margin: 24px 0 12px;
}

/* Paragraphs inside the body. Override the global `p { line-height: 1.65 }`
   so the section-wide 1.7 line-height applies uniformly. */
.legal-page-body p {
    margin: 0 0 16px;
    line-height: inherit;
}
.legal-page-body p:last-child {
    margin-bottom: 0;
}

/* Lists — bullets / numbers explicitly re-enabled, since the global
   `ul { list-style: none }` (section 3, line 156) strips them by default.
   Indent via padding-inline-start so RTL would still work correctly. */
.legal-page-body ul,
.legal-page-body ol {
    margin: 0 0 16px;
    padding-inline-start: 24px;
}
.legal-page-body ul {
    list-style: disc;
}
.legal-page-body ol {
    list-style: decimal;
}
.legal-page-body li {
    margin-bottom: 8px;
}
.legal-page-body li:last-child {
    margin-bottom: 0;
}
/* Nested lists — tighter spacing. */
.legal-page-body li > ul,
.legal-page-body li > ol {
    margin: 8px 0 0;
}

/* Emphasis — pull the weight up and restore full primary color so
   <strong> defined terms read clearly against the muted body text. */
.legal-page-body strong {
    font-weight: 600;
    color: var(--color-text-primary);
}

/* Inline code — emails, technical identifiers (e.g. SIRET), API names.
   Subtle gray pill, monospace stack with system fallback. The 0.9em sizing
   compensates for monospace fonts reading slightly larger optically than
   the surrounding Inter glyphs. */
.legal-page-body code {
    font-family: 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas,
                 'Liberation Mono', 'Courier New', monospace;
    font-size: 0.9em;
    background-color: rgba(16, 24, 40, 0.06);
    padding: 2px 6px;
    border-radius: 4px;
    color: var(--color-text-primary);
}

/* Inline links — underlined for clarity on long-form pages (vs. the rest
   of the site where links rely on context). Underline offset + thin weight
   keep the rule readable but unobtrusive.

   The global `a { transition: color, opacity }` (section 3, line 152)
   already covers the hover color animation — no extra transition rule. */
.legal-page-body a {
    color: var(--color-text-primary);
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
}
.legal-page-body a:hover {
    color: rgba(16, 24, 40, 0.62);
}
.legal-page-body a:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: 2px;
}
/* When a link wraps a <code> chip (e.g. mailto: contact@appyamatch.fr),
   don't double-underline the chip — the chip already has its own bg. */
.legal-page-body a > code {
    text-decoration: inherit;
}

/* ---- Tables (used in politique-annulation-remboursement) ---- */

/* Tables can overflow a 72ch column on narrow viewports. Wrap them in
   their own scroll context only if needed — but apply the recipe to the
   table itself so it stays usable inside any container. */
.legal-page-table {
    width: 100%;
    border-collapse: collapse;
    margin: 24px 0;
    font-size: 0.9375rem;
}

.legal-page-table thead {
    background-color: var(--color-light-gray);
}

.legal-page-table th,
.legal-page-table td {
    text-align: left;
    padding: 12px 16px;
    /* Subtle row divider — 8% black, the same alpha used for other dividers
       on legal pages (see footer border-top below). */
    border-bottom: 1px solid rgba(16, 24, 40, 0.08);
    vertical-align: top;
}

.legal-page-table th {
    font-family: var(--font-body);
    font-weight: 600;
    color: var(--color-text-primary);
}

.legal-page-table td {
    color: rgba(16, 24, 40, 0.84);
}

.legal-page-table tbody tr:last-child td {
    border-bottom: none;
}

/* ---- Placeholder marker — <mark class="legal-page-placeholder"> ----
   Used by the legal review pass to flag "TO BE COMPLETED" spans (SIRET,
   capital, contact email, médiateur, etc.). Must be impossible to miss:
   lime tint (45% accent), dashed underline, slightly heavier weight than
   surrounding text. Lime alpha (45%) is light enough to remain readable
   inside running paragraphs. */
.legal-page-placeholder {
    background-color: rgba(215, 255, 0, 0.45);
    color: var(--color-text-primary);
    padding: 1px 6px;
    border-radius: 4px;
    border-bottom: 1px dashed rgba(16, 24, 40, 0.4);
    font-weight: 500;
}

/* ---- Footer (back link + cross-nav) ---- */
.legal-page-footer {
    margin-top: clamp(48px, 8vh, 96px);
    padding-top: clamp(32px, 4vh, 48px);
    border-top: 1px solid rgba(16, 24, 40, 0.08);
    display: flex;
    flex-direction: column;
    gap: clamp(24px, 3vh, 32px);
}

/* Back link — text-style, NOT a pill (the back link on legal pages is a
   secondary action, not the page's primary CTA like on .download-page). */
.legal-page-back {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--color-text-primary);
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.9375rem;
    text-decoration: none;
    /* The global `a { transition: color, opacity }` covers the hover. */
}
.legal-page-back:hover {
    opacity: 0.7;
}
.legal-page-back:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
    border-radius: 2px;
}

/* Cross-nav — "Autres documents" inline list of the other legal pages. */
.legal-page-nav-title {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-medium-gray);
    margin: 0 0 12px;
}

.legal-page-nav-list {
    /* Re-assert no-bullets (the global rule already strips them, but being
       explicit here protects against any future change to the global ul). */
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: clamp(12px, 1.5vw, 20px);
}

.legal-page-nav-list a {
    color: var(--color-text-primary);
    text-decoration: none;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    font-weight: 500;
    /* Subtle underline that grows to full intensity on hover. */
    border-bottom: 1px solid rgba(16, 24, 40, 0.12);
    padding-bottom: 2px;
    transition: border-color var(--transition);
}
.legal-page-nav-list a:hover {
    border-color: var(--color-text-primary);
}
.legal-page-nav-list a:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: 2px;
}

/* ---- Mobile tightening (<= 767px) ---- */
@media (max-width: 767px) {
    .legal-page {
        /* Tighter inline gutter on small viewports; block padding scales
           naturally via the clamp's vh component. */
        padding: clamp(32px, 6vh, 48px) clamp(20px, 5vw, 32px);
    }

    /* Slightly tighter leading on mobile — 1.7 reads too airy on small
       screens, 1.65 is the sweet spot between desktop comfort and mobile
       density. */
    .legal-page-body {
        line-height: 1.65;
    }

    /* Smaller table cells so the 4-6 column tables in
       politique-annulation-remboursement stay readable without horizontal
       scroll on phones. */
    .legal-page-table {
        font-size: 0.875rem;
    }
    .legal-page-table th,
    .legal-page-table td {
        padding: 10px 12px;
    }
}


/* ==========================================================================
   16b. ERROR PAGE (404 — website/404.html)
   --------------------------------------------------------------------------
   Standalone 404 page that REUSES the hero pattern (lime card + slanted
   title + glass CTA) but WITHOUT the homepage's sticky / scroll-driven
   coupling. The 404 page intentionally loads NO JavaScript (statically
   robust) — every JS-driven CSS variable (`--scroll`, `--card-shrink`,
   `--hero-flow-collapse`) therefore stays at its `var(.., 0px)` fallback,
   which is a clean no-op in every consumer rule (verified at lines 432,
   557, 603).

   Scoping contract: every override below is rooted on `.error-page` so the
   homepage cascade is untouched. The override pattern mirrors the existing
   `.download-page` / `.legal-page` recipe (sections 15 and 16) — same
   wordmark static-positioning reset, same min-height: 100svh wrapper.

   New class introduced by this section: `.hero-eyebrow`. Visual recipe
   adapted from the existing eyebrow family (.faq-eyebrow line 2130,
   .download-eyebrow line 2520, .legal-page-eyebrow line 2671): same
   font-family / size / weight / tracking / case. Color differs: those
   eyebrows sit on white surfaces and use `--color-medium-gray`; this one
   sits on the LIME hero card so it uses `--color-text-black` for stronger
   contrast and to match the on-card title/subtitle color.
   ========================================================================== */

/* Page shell — full-viewport flex column with the wordmark at the top
   (now in normal flow, not fixed) and <main> centring the hero card. */
.error-page {
    min-height: 100vh;        /* fallback older Safari */
    min-height: 100svh;       /* iOS 15.4+ : stable face URL bar */
    display: flex;
    flex-direction: column;
    background-color: var(--color-light-bg);
}

.error-page main {
    /* Grow to fill remaining vertical space; centre the hero block. */
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(40px, 8vh, 96px) clamp(24px, 6vw, 80px);
}

/* Wordmark override — same recipe as `.download-page .wordmark`
   (section 15, line 2498) and `.legal-page .wordmark` (section 16,
   line 2647). The default wordmark (section 6, line 227) is
   `position: fixed; translate: -50% -50%; top: calc(--page-pad / 2); left: 50%`
   so it floats over the lime hero card. On the 404 utility page we want
   the wordmark to flow ABOVE the centred hero block as a normal logo;
   resetting position / top / left / translate / transform makes it a
   plain inline-flex element. The entrance keyframe (`wordmarkEntrance`)
   still plays — just on a static-positioned element. */
.error-page .wordmark {
    position: static;
    top: auto;
    left: auto;
    translate: none;
    transform: none;
    align-self: center;
    margin-top: clamp(24px, 5vh, 56px);
}

/* Hero override — the homepage `.hero` (section 7.1, line 400) carries
   `padding: var(--page-pad)` and a negative-margin-bottom collapse driven
   by `--hero-flow-collapse`. On the 404 page neither is desired: the
   `.error-page main` wrapper already supplies the centring + breathing
   room, and there's no carousel below to pull up via flow-collapse. Reset
   padding + margin-bottom; let `.error-page main` constrain the width. */
.error-page .hero {
    width: 100%;
    max-width: 720px;
    padding: 0;
    margin-bottom: 0;
}

/* Hero-card override — the homepage `.hero-card` (section 7.2, line 468)
   carries `min-height: calc(100svh - 2 * var(--page-pad))` so it fills
   the viewport with room for the active phone image to peek below. On
   the 404 page there is no carousel and no phone, so the giant min-height
   would produce an absurdly tall lime tile. Reset to `auto` so the card
   sizes to its content (eyebrow + title + subtitle + CTA). The
   `cardEntrance` keyframe + `--card-shrink` clip-path stay in effect —
   harmless no-ops because `--card-shrink` resolves to its `0px` fallback
   without JS. */
.error-page .hero-card {
    min-height: auto;
}

/* Hero-card-inner override — the homepage `.hero-card-inner` (section
   7.3, line 564) carries `padding: clamp(48px, 6vw, 88px) clamp(20px,
   3.2vw, 48px)`. On the 404 the lime card is the ONLY page surface, so
   we want a touch more horizontal generosity (no carousel below to
   compete for attention). Slightly larger inline padding; vertical
   padding inherits the same scale. */
.error-page .hero-card-inner {
    padding: clamp(48px, 8vw, 88px) clamp(32px, 6vw, 64px);
}

/* Hero-buttons override — the homepage hero has TWO buttons (App Store
   + Google Play); the existing `.hero-buttons` rule (section 7.6, line
   666) is `justify-content: center` with `gap` and side-by-side flex.
   On 404 there is exactly one button (Retour à l'accueil). The flex
   layout already handles a single child correctly — `justify-content:
   center` simply centres it. No override required for layout, but we
   explicitly pin `justify-content: center` here so a future change to
   the homepage rule cannot accidentally shift the 404 button alignment. */
.error-page .hero-buttons {
    justify-content: center;
}

/* Hero-eyebrow — small-caps overline above the title. Visual recipe
   matches the eyebrow family elsewhere on the site (.faq-eyebrow line
   2130, .download-eyebrow line 2520, .legal-page-eyebrow line 2671):
   Inter 600, 12 px, 0.16em tracking, uppercase. Color differs: the
   other eyebrows live on WHITE surfaces and use `--color-medium-gray`;
   this one sits on the LIME hero card so it uses `--color-text-black`
   to match the on-card title/subtitle color and preserve contrast.
   Centred to align with the centred title/subtitle/buttons stack inside
   `.hero-card-inner` (which has `align-items: center` — see section 7.3,
   line 568). */
.hero-eyebrow {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--color-text-black);
    margin: 0;
    text-align: center;
}


/* ==========================================================================
   17. MOBILE MEDIA QUERIES (consolidated — overrides hero + carousel + quest)
   --------------------------------------------------------------------------
   Single consolidated @media (max-width: 767px) block. Mobile carousel shows
   ~1.08 slides per viewport. The active-phone lift uses the same JS-driven
   --scroll mechanism as desktop (script.js page-scroll-progress block computes
   the live lift so the phone top sits 40 px below .hero-buttons bottom at rest,
   then animates back to 0 as the user scrolls). Token overrides at the top
   redefine --page-pad and the mobile-only CTA / hero-card-height vars. Placed
   AFTER all desktop rules so media-query specificity wins by source order for
   same-specificity selectors.
   ========================================================================== */
@media (max-width: 767px) {
    /* Reveal the inline mobile-only line breaks (see section 3 utility). */
    .br-mobile-only {
        display: inline;
    }

    :root {
        /* Reduced 2026-05-11 to widen the title's text budget on small viewports.
           The mobile `.hero-title` ("TOURNOI T'ATTEND" line, 16 caps in Frick 0.3
           with skewX(-12deg)) was wrapping to 3 lines at vp 320–375 because the
           previous floor (12px) combined with the previous --mobile-hero-card-margin
           floor (30px) and --hero-card-inner padding-inline floor (16px) left only
           ~228 px of usable text width at vp 320, while a more conservative
           Frick-0.3 cap-glyph estimate (0.58 em/glyph) requires ~242 px at the
           1.7rem floor. Trimming --page-pad to clamp(8px, 2.5vw, 12px) only
           affects the footer on mobile (since .hero / .hero-card / .screens-rail
           / .faq all switch to --mobile-hero-card-margin in this block); the
           footer's editorial gutter shrinks by 4-6 px which stays comfortably
           above the 8 px minimum for tap targets. */
        --page-pad: clamp(8px, 2.5vw, 12px);
        /* Mobile floating-tile margin — INLINE AXIS ONLY (left / right of the
           lime hero card). Decoupled 2026-05-11 from the block axis (top /
           bottom) so the horizontal column can be tightened (gain text budget
           for the hero title wrap) WITHOUT also collapsing the white strip
           above the card where the wordmark lives. The block axis lives in
           --mobile-hero-card-margin-block below.

           Inline-axis consumers (this token):
             .hero-card           → width: calc(100% - 2 * ...)
             .screens-rail        → padding-inline: var(...)
             .faq                 → padding-inline: var(...)

           Reduced 2026-05-11 from clamp(30px, 7.5vw, 60px) to widen the lime
           card by 20 px at vp 375 (and 20 px at vp 320). The previous floor
           wasted column width on a narrow viewport where every pixel of text
           budget matters for the 2-line "TON PROCHAIN / TOURNOI T'ATTEND"
           hero title wrap (Frick 0.3 caps, skewX(-12deg)). */
        --mobile-hero-card-margin: clamp(20px, 5vw, 40px);

        /* Mobile floating-tile margin — BLOCK AXIS ONLY (top / bottom of the
           lime hero card + vertical position of the wordmark in the white
           strip above the card). Split out 2026-05-11 from the inline-axis
           token above so the white strip that hosts the wordmark keeps a
           comfortable height (≥ 30 px floor) even when the inline margin is
           tightened to a smaller floor (20 px) for text-budget reasons.

           Block-axis consumers (this token):
             .hero-card           → margin-top, margin-bottom
             .hero-card           → min-height: calc(100svh - 2 * ...)
             .wordmark            → top: calc(... / 2)  (wordmark vertically
                                    centred in the white strip whose height
                                    equals this token)

           Range bumped 2026-05-11 from clamp(30px, 7.5vw, 60px) to
           clamp(40px, 10vw, 80px) (+33 % at every viewport) on user request:
           the white strip above the wordmark felt cramped at vp 320–375 even
           after the inline/block split, and the bottom margin under the lime
           card needed more air before the carousel block. The wordmark's
           `top: calc(... / 2)` re-centres automatically inside the wider
           strip; .hero-card top/bottom margins and min-height are recomputed
           symmetrically. The carousel pull-up via `--hero-flow-collapse`
           closes the lower gap during scroll, so the user-perceived effect
           is concentrated at the rest state (page top).

           Range bumped again 2026-05-11 from clamp(40px, 10vw, 80px) to
           clamp(56px, 14vw, 112px) (+40 % at every viewport) on user
           request: even after the previous bump the white strip above the
           wordmark still read as tight on small phones — the wordmark sat
           visually closer to the lime card than to the viewport top edge.
           Effective floor is now 56 px (vp 320 / 375: fluid = 14vw =
           44.8 / 52.5 px → both clamped to the 56 px floor); ceiling is
           112 px (reached at vp ≥ 800 px). All three consumer rules
           (.hero-card margin-top / margin-bottom / min-height and
           .wordmark top) recompute automatically; min-height shrinks
           symmetrically by 2 × delta but stays well above the intrinsic
           content height of the title + subtitle + buttons stack. */
        --mobile-hero-card-margin-block: clamp(56px, 14vw, 112px);
    }

    /* Mobile hero is now a floating tile: the lime card carries equal margins on
       all four sides, restoring the desktop "equal margins on all four sides"
       invariant (memory feedback_website_hero_pattern.md) at mobile scale. The
       previous full-bleed-bottom layout (margin-bottom: 0, flat bottom corners)
       has been retired in favour of true symmetric breathing room. Carousel
       remains intentionally pushed below the fold. */
    .hero {
        padding: 0;
    }

    /* Hide the floating QR widget on mobile. Scanning a QR with the same
       device you're holding makes no sense — mobile visitors will tap a
       store button (or land on /download/ directly) instead. Removing the
       widget also reclaims valuable bottom-right real estate that would
       otherwise overlap the carousel and the FAQ accordion on small
       viewports. The widget's `aria-label` is descriptive, but `display:
       none` removes it from the a11y tree entirely on mobile, which is
       correct: there is no scannable QR for mobile screen-reader users. */
    .qr-widget {
        display: none;
    }

    .hero-card {
        /* Floating-tile layout — margins on all four sides, sourced from TWO
           tokens to allow inline (left/right) and block (top/bottom) axes to
           be tuned independently:
             --mobile-hero-card-margin       → inline axis (width formula)
             --mobile-hero-card-margin-block → block axis (top/bottom + min-height)
           Decoupling rationale: the inline floor (20 px) is set tight to maximise
           text width for the 2-line hero title wrap; the block floor (30 px)
           preserves the white strip above the card where .wordmark lives, so
           the wordmark keeps a comfortable optical gap above and below.
           - width:         viewport minus 2× inline margin (centred via auto)
           - margin-inline: auto centres the card horizontally
           - margin-top / margin-bottom: equal block margin (sourced from block token)
           - min-height:    100svh minus 2× block margin so the card fits in the
                            initial viewport without forcing scroll
           - border-radius: full (was: top-rounded / bottom-flat) — bottom corners
                            are now visible against white, so they must be rounded
                            to match the top. */
        width: calc(100% - 2 * var(--mobile-hero-card-margin));
        margin-inline: auto;
        margin-top: var(--mobile-hero-card-margin-block);
        margin-bottom: var(--mobile-hero-card-margin-block);
        /* svh (small viewport height) is preferred over vh because it excludes
           the dynamic mobile URL bar, so the card height stays stable while the
           URL bar collapses/expands during scroll — no "wobble". Browser
           support: Safari 15.4+, Chrome 108+, Firefox 101+. */
        /* `--card-shrink` does NOT affect this min-height — see the desktop
           rule's comment for the full rationale. Layout box stays fixed; the
           visual shrink is applied via clip-path on `.hero-card::before` and
           `.hero-card-waves` (overridden just below to use the mobile
           border-radius literal). */
        min-height: calc(100svh - 2 * var(--mobile-hero-card-margin-block));
        border-radius: clamp(24px, 7vw, 32px);
    }

    /* Mobile override for the lime fill and the wave SVG: same `inset()`
       shrink contract as desktop, but the corner radius literal must match
       the mobile `.hero-card { border-radius }` value above (`inset()`'s
       round value cannot use `inherit`). */
    .hero-card::before,
    .hero-card-waves {
        clip-path: inset(0 0 var(--card-shrink, 0px) 0 round clamp(24px, 7vw, 32px));
    }

    /* Vertically centre the wordmark in the white strip between the ticker's
       bottom edge and the lime card's top edge. Since 2026-05-12 the launch
       ticker (§4.5) lives in the document flow at viewport y = 0..--launch-ticker-height
       and `.hero { padding: 0 }` on mobile means the lime card's top edge sits
       exactly at `viewport y = --launch-ticker-height + --mobile-hero-card-margin-block`.
       The wordmark is `position: fixed`, so its `top` is measured from the
       viewport. To land its centre at the midpoint between the ticker bottom
       (`--launch-ticker-height`) and the lime card top, the centre must sit at
       `--launch-ticker-height + --mobile-hero-card-margin-block / 2`.
       `translate: -50% -50%` (already declared in the desktop rule, section 6)
       keeps the element's centre locked to this `top` value regardless of its
       own height — so equal gap above (between ticker bottom and wordmark top
       edge) and below (between wordmark bottom edge and lime card top edge) by
       construction. Sourcing the block token (NOT the inline one) is critical:
       the strip the wordmark lives in is vertical, so the wordmark's vertical
       position must track the vertical margin. Default desktop value
       (`var(--launch-ticker-height) + var(--page-pad) / 2`) is too small here
       because mobile overrides --page-pad to a much smaller clamp than the
       card's block margin. */
    .wordmark {
        top: calc(var(--launch-ticker-height) + var(--mobile-hero-card-margin-block) / 2);
    }

    .hero-card-inner {
        /* Horizontal padding (middle value) tightened 2026-05-11 from
           clamp(16px, 5vw, 28px) to clamp(8px, 2.5vw, 16px) — frees up
           16 px of text width at vp 320 (8 px each side) so "TOURNOI
           T'ATTEND" fits on its single line in the 2-line wrap. Top
           and bottom paddings are intentionally untouched: the top pad
           preserves vertical breathing room above the title, and the
           bottom pad preserves space for the active phone mockup that
           lifts up via translateY(var(--scroll)) into this region. */
        padding:
            clamp(58px, 12vw, 82px)
            clamp(8px, 2.5vw, 16px)
            clamp(112px, 22vw, 160px);
        /* Mobile vertical rhythm — bumped ~45% from clamp(12px, 3vw, 18px)
           to mirror the desktop bump. The lime card on mobile is a floating
           tile with min-height ~88svh, so the extra spacing fits inside the
           card; if the intrinsic content height exceeds the min-height the
           card naturally grows (no clipping). */
        gap: clamp(18px, 4.5vw, 26px);
    }

    .hero-title {
        /* Two-line wrap is now forced explicitly by `<br class="br-mobile-only">`
           inserted between "Ton prochain" and " tournoi t'attend" inside the
           `.hero-title-slant` span (see index.html L46). Line 1 = "TON PROCHAIN"
           (12 chars), line 2 = "TOURNOI T'ATTEND" (16 chars).
           Bumped ~+17% on user request ("un peu plus gros"); previous clamp was
           clamp(1.6rem, 7.5vw, 2.65rem) → 25.6/28.1/42.4 px @ vp 320/375/cap.
           New clamp: clamp(1.7rem, 8.75vw, 3rem) → 27.2/32.8/48 px @ vp 320/375/cap.
           Floor 1.7rem (NOT 1.85rem the user originally suggested) is the worst-case
           safety bound — at vp 320 the available text width is the hardest budget.

           Visual budget at vp 320 (worst case, hard constraint) — REVISED 2026-05-11
           after iPhone wrap regression:
             card outer  = 100vw - 2 × --mobile-hero-card-margin
                         = 320 - 2 × clamp(20, 5vw=16, 40) = 320 - 40 = 280 px
                         (was: 260 px before the margin reduction)
             card-inner padding-x = clamp(8, 2.5vw=8, 16) = 8 px each side
                         (was: 16 px each side)
             available text width = 280 - 16 = 264 px
                         (was: 228 px — gained +36 px of text budget)
             "TOURNOI T'ATTEND" (16 visual chars in Frick 0.3 caps) — REVISED to
             0.58 em/glyph (was 0.50 em/glyph, which underestimated Frick's wide
             cap glyphs and caused the iPhone 3-line wrap regression). Conservative
             upper bound for Frick 0.3 caps including the apostrophe + space.
             Unskewed width ≈ 16 × 0.58 = 9.28 em.
             skewX(-12deg) on a line-height 0.95 box adds tan(12°) ≈ 0.213 ×
             0.95 ≈ 0.20 em of horizontal shear → skewed width ≈ 9.48 em.
             At floor 1.7rem = 27.2 px: skewed ≈ 9.48 × 27.2 = 257.8 px
             vs available 264 px → ~6 px buffer ✓ (target ≥ 8 px met within the
             measurement tolerance — the 0.58 em/glyph estimate is itself
             conservative, real Frick caps measure closer to 0.55-0.56 em).

           Per-viewport sanity check (revised skewed width @ 0.58 em/glyph vs available):
             vp 320 — 27.2 px floor      → ~258 px ≤ 264 px ✓ ( +6 px buffer)
             vp 375 — 8.75vw = 32.8 px   → ~311 px ≤ 319 px ✓ ( +8 px buffer)
             vp 414 — 8.75vw = 36.2 px   → ~343 px ≤ 354 px ✓ (+11 px buffer)
             vp 549 — 8.75vw caps at 48 px → ~455 px ≤ 477 px ✓ (+22 px buffer)
             (available = vp - 2 × clamp(20, 5vw, 40) - 2 × clamp(8, 2.5vw, 16))

           max-width raised 14ch → 18ch to match the desktop rule (line 429); the
           explicit <br> is what enforces the 2-line wrap now, max-width is just a
           safety cap for over-wide viewports inside the mobile range. */
        font-size: clamp(1.7rem, 8.75vw, 3rem);
        max-width: 18ch;
        line-height: 0.95;
    }

    .hero-subtitle {
        /* Bumped +12% on user request (2026-05-11) from clamp(0.85rem, 3.2vw, 1.05rem).
           New range: 15.2 px floor (vp ≤ ~422) → 17.3 px @ vp 480 → 18.4 px ceiling
           (vp ≥ ~512). Hierarchy under the mobile .hero-title clamp(1.7rem, 8.75vw, 3rem)
           remains preserved: subtitle / title ratio sits at ~56–60 % across the mobile
           range (15.2/27.2 ≈ 0.56 @ vp 320, 17.3/42 ≈ 0.41 @ vp 480, 18.4/48 ≈ 0.38
           at the ceiling) — subtitle is always smaller than the title.
           Two-line wrap forced explicitly by `<br class="br-mobile-only">` between
           "Compose ton équipe," and " il y a match" (index.html L47). Worst-case fit
           @ vp 320: available text width = 320 - 2 × clamp(20,5vw,40)=40 - 2 × clamp(8,2.5vw,16)=16 = 264 px.
           Longest line = "Compose ton équipe," (19 chars incl. trailing comma) at
           Inter 500 ≈ 0.55 em/glyph average → 19 × 0.55 × 15.2 px ≈ 159 px ≤ 264 px,
           ~105 px buffer. ✓ */
        font-size: clamp(0.95rem, 3.6vw, 1.15rem);
        line-height: 1.35;
    }

    /* 404-specific subtitle — drastically smaller than the homepage hero subtitle.
       The 404 copy ("Cette page n'existe pas ou a été déplacée. Reviens sur le terrain
       principal.", index.html L45) is a UTILITY message, not the page's main hook —
       it should read as light secondary text under the eyebrow + title, not compete
       with them. Roughly -25% vs the homepage mobile clamp(0.95rem, 3.6vw, 1.15rem):
       new range 12px floor (vp ≤ ~342) → 13.4px @ vp 480 → 14.4px ceiling (vp ≥ ~514).
       Tighter line-height (1.4 vs homepage 1.35 — homepage forces a 2-line wrap via
       <br class="br-mobile-only">; the 404 wraps naturally on 2 lines and benefits
       from a hair more leading at the smaller size for legibility). */
    .error-page .hero-subtitle {
        font-size: clamp(0.75rem, 2.8vw, 0.9rem);
        line-height: 1.4;
    }

    .hero-buttons {
        gap: clamp(8px, 3vw, 12px);
    }

    .btn-glass {
        padding: 7px 12px;
        min-height: 38px;
    }

    /* Mobile: hide the eyebrow entirely. The store buttons in a 320–414 px
       viewport have only ~55–80 px of label width available (after icon, gap,
       and button padding); two-line "Télécharger sur / App Store" stacks read
       cluttered at that size. Single-line label-only is cleaner and matches
       the épuré aesthetic. The `aria-label` on .btn-glass already conveys the
       full "Télécharger sur l'App Store" / "Disponible sur Google Play" copy
       to screen readers, so no semantic loss. */
    .glass-eyebrow {
        display: none;
    }

    /* Mobile: shrink the label and force single-line so "Google Play" never
       wraps inside its button. Desktop default `clamp(0.75rem, 1.3vw, 0.9375rem)`
       resolves to its 12 px floor on every mobile viewport (vw term collapses);
       at vp 320 that 12 px label "Google Play" is ~70 px wide while the inner
       label slot is only ~55 px → wrap. New range: 10 px floor (vp ≤ ~385) →
       11.2 px @ vp 430 → 12 px ceiling (vp ≥ ~462). Combined with `nowrap`,
       the label stays on one line at every viewport in the 320–767 range; if
       the geometry tightens further the button itself widens (flex children
       size to content) before the text could ever wrap. */
    .glass-label {
        font-size: clamp(10px, 2.6vw, 12px);
        white-space: nowrap;
    }

    .screens-rail {
        z-index: 2;
        margin-top: 0;
        padding-block: 0 clamp(48px, 10vw, 80px);
        /* Mobile only: align the carousel's content area to the lime hero card's
           horizontal footprint by sourcing inline padding from --mobile-hero-card-margin
           (the same token .hero-card uses for its side margins above). Result: the active
           slide width = lime card width = viewport - 2 * --mobile-hero-card-margin.
           Side slides (peers of the active) extend INTO this padding gutter and beyond,
           and are clipped at the viewport edges by `body { overflow-x: clip }`. The
           padding-inline gutter is intentionally larger than the desktop --page-pad so
           the visual peek of neighbour slides fits inside the gutter. */
        padding-inline: var(--mobile-hero-card-margin);
        background: var(--color-light-bg);
        overflow: visible;
    }

    .hero-carousel-embla {
        /* Single full-width slide on mobile (1.0, not 1.08): with --slide-size derived
           as `(100% - (n-1) * spacing) / n`, n = 1 collapses to slide_size = 100% of
           the container's content width = lime card width (since .screens-rail's
           padding-inline = --mobile-hero-card-margin matches the hero card's margins).
           The neighbour slide peeks naturally in the padding-inline gutter via the JS
           transform — no fractional `1.08` needed to surface the peek. */
        --nbr-slide: 1;
        --slide-spacing: clamp(14px, 4vw, 20px);
        --active-scale: 1.06;
        /* --scroll is written live by JS (script.js, page-scroll-progress block);
           default 0 px declared in the desktop rule remains the no-lift fallback
           if JS never runs. */
    }

    .screen-card {
        min-height: clamp(430px, 66svh, 560px);
        padding: clamp(16px, 4vw, 22px);
        border-radius: clamp(24px, 7vw, 32px);
        justify-content: flex-start;
        gap: clamp(12px, 4vw, 18px);
    }

    .phone-image {
        width: clamp(218px, 64vw, 292px);
    }

    .screen-card.active .phone-image {
        /* Mobile mirror of the desktop rule's compositor hints — see L837 for
           full rationale. translate3d + will-change + backface-visibility keep
           the active phone on a dedicated GPU layer during the iOS touch-scroll
           that drives `--scroll` per rAF tick. The desktop rule already sets
           will-change and backface-visibility (cascaded into this media query),
           so we only need to override `transform` here, but we re-declare the
           hints inline for explicitness and to make the mobile rule
           self-contained. */
        transform: translate3d(0, var(--scroll), 0) scale(var(--active-scale, 1));
        transform-origin: center top;
        will-change: transform;
        backface-visibility: hidden;
    }

    /* === Mobile overrides for the how-quest section ======================
       The desktop layout is a 2-column grid: progress bar (col 1) +
       single-column cards stack (col 2). On mobile we keep that exact
       structure but shrink both column widths and the gap to fit the
       narrower viewport. The sticky panel still pins for the duration of
       the section; min-height is reduced (260svh vs desktop 320svh) since
       the cards take less horizontal width and the unlock thresholds
       (0.12 / 0.42 / 0.72) need correspondingly less scroll budget. */
    .how-quest {
        /* Align the section's horizontal padding with the lime card / carousel /
           FAQ left/right edges (same token used by .screens-rail and .faq above). */
        padding-inline: var(--mobile-hero-card-margin);
        min-height: 260svh;
        /* Mobile mirror of the desktop block-end buffer (§10.1). Smaller
           clamp than desktop because the FAQ's mobile padding-block is
           also smaller (`var(--mobile-hero-card-margin)`); we just need
           a real visible gray strip between the sticky's last visible
           content and the FAQ's "FAQ" eyebrow. The desktop padding-bottom
           cascades into mobile by default, but we override here to use
           a smaller mobile-appropriate clamp. */
        padding-bottom: clamp(40px, 10vw, 80px);
    }

    .how-quest-sticky {
        /* Tighter vertical padding so all 3 cards + header + completion pill
           fit in 100svh on smaller phones. Bottom padding preserved (sourced
           from the desktop clamp's range) so the last card has breathing
           room before the section un-pins into the FAQ.
           NOTE: `overflow: hidden` is inherited from the desktop rule and is
           especially important on mobile — short-viewport phones (iPhone SE,
           568pt) are where the 100svh constraint is tightest and the sticky
           is most likely to need clipping to fit. */
        padding-block: clamp(24px, 5vh, 48px) clamp(32px, 7vh, 72px);
    }

    .how-quest-header {
        gap: clamp(12px, 2vh, 20px);
    }

    /* `.how-quest-eyebrow` keeps its desktop `margin-bottom: 16px` on mobile
       — matches `.faq-eyebrow` (no mobile override) so the two in-page
       eyebrows have identical literal margin values at every viewport. The
       previous `margin-bottom: 8px` mobile override was REMOVED 2026-05-11
       (user request: identical spacing eyebrow→title in both sections). */

    .how-quest-title {
        /* Match the mobile .faq-title clamp (line ~1630) so both H2s read at
           the same scale on mobile — coherent in-page heading hierarchy. */
        font-size: clamp(1.25rem, 5.5vw, 2rem);
    }

    .quest-persona-tab {
        /* Slightly tighter padding on small screens so the two-tab bar fits
           comfortably without wrapping. Font size unchanged from desktop
           (the desktop clamp's floor of 0.875rem already targets mobile). */
        padding: 9px 14px;
    }

    .quest-steps {
        /* MOBILE: keep the historic 2-column layout (rail in col 1,
           cards stacked in col 2). The desktop zigzag (3-col with bar
           in the center) is intentionally desktop-only — on a narrow
           viewport it would force cards into ~1/3 of the width and
           hurt readability. Row 4 reserved for the in-flow completion
           pill (placement override below). */
        grid-template-columns: clamp(40px, 10vw, 56px) 1fr;
        grid-template-rows: auto auto auto auto;
        gap: clamp(12px, 2vh, 20px) clamp(12px, 3vw, 20px);
        margin-top: clamp(12px, 2vh, 24px);
    }

    /* MOBILE: re-route the progress bar back to col 1, spanning the 3 card
       rows (rows 1-3) — undoes the desktop col-2 placement. Also defines
       the smaller mobile node-circle size token. */
    .quest-progress-bar {
        grid-column: 1;
        grid-row: 1 / 4;
        /* Smaller node circle on mobile to keep the rail visually slim. */
        --quest-node-size: clamp(28px, 8vw, 36px);
    }

    /* MOBILE: undo the desktop zigzag per-step placement; stack all 3 cards
       in col 2, sequential rows 1-3. Reset the desktop right-align overrides
       so card text reads left-aligned in the narrow column. */
    .quest-step[data-step="1"],
    .quest-step[data-step="2"],
    .quest-step[data-step="3"] {
        grid-column: 2;
        align-items: flex-start;
        text-align: left;
    }
    .quest-step[data-step="1"] { grid-row: 1; }
    .quest-step[data-step="2"] { grid-row: 2; }
    .quest-step[data-step="3"] { grid-row: 3; }

    /* MOBILE: with all cards in col 2 left-aligned (rail in col 1 to
       the LEFT of the cards), the lime accent strip should sit on the
       card's LEFT inner edge (facing the rail) and the Étape badge in
       the TOP-RIGHT corner (far from the rail). This OVERRIDES the
       desktop defaults (right-edge accent + top-left badge for cards
       1/3, and the per-step mirror for card 2). All three cards get the
       same orientation on mobile since they all sit in the same column. */
    .quest-step[data-step="1"]::before,
    .quest-step[data-step="2"]::before,
    .quest-step[data-step="3"]::before {
        right: auto;
        left: 0;
    }
    .quest-step[data-step="1"]::after,
    .quest-step[data-step="2"]::after,
    .quest-step[data-step="3"]::after {
        left: auto;
        right: clamp(14px, 4vw, 22px);
    }

    /* MOBILE: completion pill spans both columns (rail + cards) on row 4
       so it reads as a centered finish line below the entire 1-2-3 stack.
       Smaller margin-top than desktop because mobile vertical rhythm is
       tighter overall. */
    .quest-completion-pill {
        grid-column: 1 / -1;
        grid-row: 4;
        justify-self: center;
        margin-top: clamp(20px, 4vh, 40px);
    }

    .quest-progress-number {
        font-size: clamp(0.75rem, 3.2vw, 0.875rem);
    }

    .quest-step {
        /* Allow the card to breathe to the full column width on narrow
           viewports; remove the desktop max-width cap since the column
           itself is already constrained by the lime-card alignment. */
        max-width: none;
        padding: clamp(16px, 4vw, 22px);
    }

    /* Mobile only: align the FAQ content area to the lime hero card's horizontal
       footprint by sourcing inline padding from --mobile-hero-card-margin (the same
       token .hero-card and .screens-rail use above). Result: the FAQ content edges
       sit on the same vertical column as the lime card and the active carousel
       slide — a coherent left/right grid all the way down. The desktop --page-pad
       (16-24px on mobile) was visibly narrower than the lime card / carousel
       gutter, breaking that column. Padding-block also sources from the same token
       so all four FAQ paddings are identical (top/right/bottom/left), mirroring the
       lime hero card's equal-on-all-sides margin invariant. Footer is intentionally
       NOT touched here. */
    .faq {
        padding-inline: var(--mobile-hero-card-margin);
        padding-block: var(--mobile-hero-card-margin);
    }

    /* Mobile FAQ heading — shrink so it stays strictly smaller than the mobile
       .hero-title (now `clamp(1.7rem, 8.75vw, 3rem)` = 27.2–48px after the
       2026-05-11 +17 % bump). Without this override, the desktop rule
       `clamp(2rem, 4.5vw, 3rem)` collapses to its 32px floor on every mobile
       viewport (4.5vw < 32px until vp ≈ 711), which at vp 320–365 exceeds the
       live mobile hero-title floor (27.2px) — a clear hierarchy inversion (the
       FAQ section heading visually outranks the page H1).
       Current range targets ~63–73% of the mobile hero-title at every viewport:
         vp 320 — FAQ 20px (floor)        vs hero 27.2px (floor)  ≈ 73% ✓
         vp 375 — FAQ 20.6px (5.5vw)      vs hero 32.8px (8.75vw) ≈ 63% ✓
         vp 549 — FAQ 30px (5.5vw)        vs hero 48px (cap)      ≈ 63% ✓
         vp 582 — FAQ 32px (cap)          vs hero 48px (cap)      ≈ 67% ✓
       Stays strictly < hero-title at every viewport in the 320–767 range, so
       the +17% bump on hero-title preserves the hierarchy without touching the
       FAQ clamp. Desktop rule (`.faq-title` base at clamp(2rem, 4.5vw, 3rem)
       max 48px vs hero-title max 88px ≈ 55%) is already comfortably under
       hero-title and is intentionally untouched. */
    .faq-title {
        font-size: clamp(1.25rem, 5.5vw, 2rem);
    }

    /* Mobile footer CTAs — shrink ~15-20% on user request. Desktop default
       `.footer-list a { font-size: 14px }` felt slightly oversized on small
       viewports relative to the surrounding whitespace and the editorial
       footer aesthetic. New range: 11px floor (vp ≤ ~366) → 11.25px @ vp375
       → 12px ceiling (vp ≥ 400). Stays >= 11px so the links remain readable
       at the smallest 320 px viewport. The gap is also tightened slightly
       (was clamp(20px, 3vw, 32px) — collapsed to its 20 px floor on every
       mobile viewport) so the row of 5 links feels visually condensed to
       match the smaller type. Copyright line (.footer-bottom) gets its own
       smaller clamp below so it sits strictly under the CTA size at every
       mobile viewport (visual hierarchy: legal CTAs > copyright). */
    .footer-list {
        gap: clamp(14px, 3vw, 20px);
    }

    .footer-list a {
        font-size: clamp(11px, 3vw, 12px);
    }

    /* Mobile copyright line — must read strictly SMALLER than the .footer-list
       CTAs above (which sit at clamp(11px, 3vw, 12px) = 11–12px on mobile).
       Without this override the desktop rule `.footer-bottom { font-size: 13px }`
       leaves the copyright LARGER than the CTAs on every mobile viewport — a
       hierarchy inversion (a quiet legal line outranking the live link list).
       Range: 10px floor (vp ≤ ~400) → 2.5vw mid → 11px ceiling (vp ≥ ~440).
       Per-viewport check vs CTAs:
         vp 320 — copyright 10px (floor)        vs CTAs 11px (floor)     ✓ smaller
         vp 375 — copyright 10px (floor)        vs CTAs 11.25px (3vw)    ✓ smaller
         vp 440 — copyright 11px (ceiling)      vs CTAs 12px (ceiling)   ✓ smaller
         vp 600 — copyright 11px (ceiling)      vs CTAs 12px (ceiling)   ✓ smaller
       Stays >= 10px so the line remains legible on the smallest viewport. */
    .footer-bottom {
        font-size: clamp(10px, 2.5vw, 11px);
    }
}


/* ==========================================================================
   18. CURSOR TRAIL (volleyball emoji + canvas comet — desktop only)
   --------------------------------------------------------------------------
   Custom cursor decoration: a rigid volleyball emoji (.cursor-emoji) follows
   the system pointer in real time, with a tapered lime "comet" trail painted
   behind it on a full-viewport <canvas class="cursor-trail">. Both DOM nodes
   live at the end of <body> (see index.html ~line 384–385) and are driven
   by script.js (mousemove handler + rAF canvas-draw loop + element position
   write).

   Hard product constraints we enforce in CSS:
     1. The system cursor MUST stay visible (we never set `cursor: none`
        anywhere in the codebase — see grep audit on `cursor:` declarations,
        all five existing usages set `pointer` or `grab`, none set `none`).
        The emoji is a decorative *companion* to the OS cursor, not a
        replacement for it.
     2. Neither node may intercept clicks: `pointer-events: none` on both.
        The canvas covers the entire viewport (100vw × 100vh) so without
        this guard every click on the page would land on the canvas.
     3. The emoji must NOT have a `transition` on `transform` / `translate`.
        The user explicitly rejected any easing on the cursor follow — JS
        writes the new `translate3d(x, y, 0)` on every `mousemove` and the
        emoji must paint at the new position on the next frame with zero
        interpolation. The only transitioned property is `opacity` (120ms),
        used for show/hide on `mouseenter`/`mouseleave` of the document.
     4. Hidden on devices with no cursor concept (`pointer: coarse`) and
        on small viewports (`max-width: 767px`) — same breakpoint family
        used for the QR widget and other desktop-only UI.
     5. Hidden under `prefers-reduced-motion: reduce` — the comet trail is
        a non-essential animation; users who opted out of motion get a
        clean, undecorated pointer.

   Stacking: trail at z-index 9998, emoji at 9999. The pair sits ABOVE
   permanent UI (`.qr-widget` z=100, `.wordmark` z=50, `.screens-rail` z=2,
   `.hero-card` z=1) and ABOVE ephemeral overlays (`.toast`, `.skip-link`
   both at z=1000). Justification: the cursor decoration must follow the
   pointer no matter what is currently on screen — including when a toast is
   visible or the skip-link is focused. Because both nodes are
   `pointer-events: none`, sitting above interactive overlays is purely
   visual: clicks still reach the underlying skip-link/toast normally. The
   trail is faint (lime fade rendered by the canvas) and the emoji is small
   (20 px), so they never visually obscure the overlays in any meaningful
   way. Emoji at 9999 (one above the trail) so the volleyball always paints
   in front of its own comet tail.
   ========================================================================== */
.cursor-trail {
    /* Full-viewport canvas pinned to the viewport edges. JS sets
       `canvas.width = innerWidth * dpr` and `canvas.height = innerHeight * dpr`
       (DPR scaling for sharp lines on retina), then resets the CSS box back
       to 100vw × 100vh via these declarations so the bitmap upscales
       exactly to the viewport. */
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    /* Never intercept clicks — the canvas covers the full viewport. */
    pointer-events: none;
    /* Below the emoji (9999), above all permanent UI and overlays. See
       header comment for stacking rationale. */
    z-index: 9998;
}

.cursor-emoji {
    /* Rigidly anchored to the viewport top-left; JS writes the live
       position via `translate3d(x + offsetX, y + offsetY, 0)` on every
       `mousemove`. The initial off-screen translate prevents a flash at
       (0, 0) before the first pointer event arrives. */
    position: fixed;
    top: 0;
    left: 0;
    /* Initial off-screen position so the emoji doesn't paint at (0, 0)
       on first paint. JS overrides this on the first `mousemove`. NOTE:
       we use `translate` (the individual property) NOT `transform: translate(...)`
       so JS can write either property without conflicting with this initial
       value — modern JS uses the shortcut `style.transform = ...` per the
       spec, so we use `transform` here as the initial too. Critically, we
       do NOT add `transform` to the transition list below — the emoji must
       follow the cursor with zero lag. */
    transform: translate3d(-100px, -100px, 0);
    /* Decorative — never intercept clicks. */
    pointer-events: none;
    /* Above the canvas trail (9998). See header for stacking justification. */
    z-index: 9999;
    /* Discrete sizing — clamp inside a tight 18–22 px range. The emoji is a
       companion to the OS cursor, not a feature in itself; it must read as
       playful but never compete with content typography. line-height: 1
       collapses the line box so the emoji glyph's bounding box matches the
       font-size exactly, keeping the JS offset math (centering the emoji
       on the cursor tip) trivial. */
    font-size: clamp(18px, 1.4vw, 22px);
    line-height: 1;
    /* Prevent text-selection caret from appearing if the user happens to
       click-drag through the emoji's box (defensive — pointer-events: none
       already prevents this in modern browsers, but the CSS is cheap). */
    user-select: none;
    -webkit-user-select: none;
    /* GPU compositor hint — the emoji is repositioned every mousemove
       (~120 Hz on modern trackpads). will-change: transform promotes it to
       its own compositor layer so the position writes never trigger a
       paint, only a composite. Same pattern used on .phone-image
       (line ~931) and .wordmark (line ~259, with `opacity` instead of
       `transform`). */
    will-change: transform;
    /* ONLY opacity is transitioned — used by JS to fade the emoji in/out
       on document `mouseenter` / `mouseleave` (window blur, cursor leaves
       to a system menu, etc.). 120 ms ease keeps the fade snappy. NEVER
       add `transform`/`translate` to this list — it would create the lag
       the user explicitly rejected. */
    transition: opacity 120ms ease;
    opacity: 1;
}

/* Hide on coarse-pointer devices (touch/stylus) and small viewports.
   Same breakpoint pair used at line 346 (.wordmark mobile pill safety
   net): `(max-width: 767px), (pointer: coarse)`. Both conditions hide
   the cursor decoration: there is no "cursor" on touch devices, and the
   small-viewport check covers non-touch laptops with narrow windows
   that we want to treat as mobile. `display: none` removes both nodes
   from the layout tree entirely — JS still attaches mousemove listeners
   harmlessly (the writes are no-ops on display:none nodes). */
@media (max-width: 767px), (pointer: coarse) {
    .cursor-trail,
    .cursor-emoji {
        display: none;
    }
}


/* ==========================================================================
   19. REDUCED MOTION
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    .hero-card-waves { animation: none; opacity: 1; }
    /* Active slide's phone image sits flat in the row — no lift. The JS
       page-scroll-progress block early-returns under prefers-reduced-motion,
       so it never overwrites these values. */
    .hero-carousel-embla {
        --scroll: 0;
        --active-scale: 1;
    }

    /* Quest section under reduced-motion:
       - All step cards are immediately at their unlocked visual state
         (full opacity + scale 1) so users who opted out of motion still
         see the full content without waiting for scroll-driven unlocks.
         The blanket `transition-duration: 0.01ms` above already kills the
         opacity/scale ramp; pinning the values here makes the first paint
         already-correct in case JS hasn't run yet.
       - Completion pill is shown statically when JS removes [hidden].
       - The persona pill-bar's sliding indicator and the progress-bar's
         lime fill have explicit transitions (320ms / 240ms ease-in-out-cubic)
         that the blanket rule above also collapses to ~0ms — so swapping
         persona / scrolling past unlock thresholds snaps instantly without
         the smooth slide / grow. No additional rule is needed: those
         pseudo-element transitions are caught by `*::before, *::after` in
         the universal selector above. */
    .quest-step {
        opacity: 1;
        scale: 1;
    }
    .quest-completion-pill {
        opacity: 1;
        scale: 1;
    }

    /* Cursor trail (§18) — fully hidden under reduced-motion. The comet
       fade and the constantly-moving emoji are non-essential decorations;
       users who opted out of motion get a clean OS cursor with no
       companion. `display: none` also lets the JS mousemove handler
       short-circuit cheaply (writes to a display:none element are
       no-ops), though the JS side is also expected to early-return on
       `matchMedia('(prefers-reduced-motion: reduce)').matches` — same
       pattern as the page-scroll-progress block. */
    .cursor-trail,
    .cursor-emoji {
        display: none;
    }

    /* Launch ticker (§4.5) — kill the horizontal scroll animation and
       collapse the duplicated track to a single, statically-centred copy
       of the message. Hides the 2nd/3rd/4th spans (would otherwise queue
       horizontally with the gap and look like a static repetition). The
       blanket `* { animation-duration: 0.01ms }` rule above already
       reduces the keyframe to a near-instant snap; the explicit
       `animation: none` here removes it entirely (cleaner — no risk of a
       residual frame at -50% before the 0.01ms shortcut takes effect),
       and `transform: none + min-width: 0` lets the row sit naturally.
       `justify-content: center` re-centres the surviving first span. */
    .launch-ticker {
        justify-content: center;
    }
    .launch-ticker__track {
        animation: none;
        transform: none;
        min-width: 0;
        /* Reset the seamless-loop padding (only meaningful when the
           animation runs); leaving it would shift the lone surviving
           span off-centre under `justify-content: center`. */
        padding-right: 0;
    }
    .launch-ticker__track span:not(:first-child) {
        display: none;
    }
}
