/* ==========================================================================
   Neptune App Manager — cinematic landing.

   Scroll architecture: each <section class="act"> is a tall scroll track whose
   .act__pin is position:sticky, so the viewport holds one frame while progress
   runs 0..1. That single progress value drives both the DOM reveals here and
   the WebGL stage in scene.js — no animation library, no scroll hijacking.

   Palette is Neptune's own, taken from the brand portal (abyss/deep navy,
   rose accents, blood-red titles); the yellow ticker is kept. Type is League Gothic
   for display, Inter for body, Space Mono for chrome, Amiri for Arabic.
   ========================================================================== */

/* Titr Bold carries Arabic only -- no Latin glyphs -- so the unicode-range keeps
   it off the Latin runs inside mixed FR/AR lines, which fall through to Amiri. */
@font-face {
  font-family: 'Titr';
  src: url("fonts/titr_bold.fb45c0e2f697.ttf") format('truetype');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+060C, U+061B, U+061F, U+0600-06FF, U+0750-077F, U+08A0-08FF,
                 U+FB50-FDFF, U+FE70-FEFF;
}

:root {
  --abyss: #020b16;
  --deep: #05213d;
  --deep-2: #0b2e52;
  --deep-3: #163a64;
  --accent: #94a3b8;        /* Neptune --rose: eyebrows, markers, rules */
  --accent-warm: #e7b4b4;   /* Neptune's Arabic accent colour */
  --info: #1d4ed8;          /* Neptune --glow-hi */
  --blue: #1d4ed8;
  /* The page's one solid-fill colour: the CTA, the auth submit, the pricing
     badge, the skip link. Was #f5e663, a full lemon yellow; it is now the same
     cream the first pitch card is painted in, so the loudest element on the
     page and the section that carries the pitch are the same colour. Both come
     from that lemon -- this is it at 18% over white -- which is why the page
     still reads warm rather than neutral. */
  --cta: #FDFBE3;
  --cream: #f8fafc;
  --cream-soft: rgba(241, 245, 249, 0.76);
  --cream-dim: rgba(241, 245, 249, 0.52);
  --hair: rgba(248, 250, 252, 0.14);
  --hair-soft: rgba(248, 250, 252, 0.07);

  /* Titles: a glossy dark blood red. The bright top stop is doing the legibility
     work -- a flat dark red on this navy would sit under 2:1 contrast. */
  --blood-hi: #f4685a;
  --blood-mid: #c62828;
  --blood: #98181b;
  --blood-deep: #6b1013;

  --font-display: 'League Gothic', 'Archivo Black', Impact, sans-serif;
  --font-body: 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono: 'Space Mono', 'JetBrains Mono', ui-monospace, monospace;
  --font-ar: 'Titr', 'Amiri', 'David Libre', Georgia, serif;
  --font-logo: 'Lobster', cursive;

  --bar-h: 34px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Ground colour behind the hardcoded scrims. Lets the gradients in .block
     and .topbar::before follow the theme without restating them. */
  --ground-rgb: 2, 11, 22;
  --ink: #ffffff;

  /* Text that sits ON a --cta fill (solid buttons, the pricing badge, the auth
     submit, the password ticks). It was hardcoded #0b1220 at five call sites,
     which is correct only while the fill is light: swap it for the light
     theme's navy and the pair becomes navy-on-navy. Measured at the worst site,
     "Se connecter ->": 1.15:1. On the cream fill this reads 17.9:1. */
  --on-cta: #0b1220;
  /* Text on a --blood-hi fill (the error badge). Near-black reads only while
     that stop is the bright coral; on light it is #701111. */
  --on-blood: #2a0407;

  /* The raised-surface veil: cards, inputs, ghost buttons. A white 5% wash is
     a DARK-page device -- over #F8FAFC it is invisible, so those surfaces lost
     their fill entirely and were left holding a 1px border. */
  --veil: rgba(255, 255, 255, 0.05);

}

/* ==========================================================================
   LIGHT THEME — Neptune design system
   ==========================================================================
   Values are taken verbatim from the App Manager's token block in
   brand_portal_dashboard.html, which is the design system of record:

     --paper   #FFFFFF   --ink   #0F172A   --accent-blue #05213D
     --paper-2 #F8FAFC   --ink-2 #334155   --accent-red  #701111
     --surface-lighter #F1F5F9   --muted #64748B   --border-subtle #E2E8F0

   The convention is the dashboard's too: `data-theme` on <html>, persisted
   under the same localStorage key, so a visitor who picks a theme on either
   surface gets it on the other.

   Structural note: this file's :root still holds the DARK values, because the
   landing is a dark-first design -- the WebGL stage, the clay wordmark and the
   blood titles were all authored against #020b16. Light is the overlay. Which
   theme a first-time visitor GETS is decided in neptune.js, not here, so the
   two concerns stay separate.

   Measured on the ground each one actually paints on:
     ink 17.06:1   ink-2 9.90:1   muted 4.55:1
     accent-blue 15.54:1   accent-red 11.29:1        (all AA)
   ========================================================================== */
:root[data-theme="light"] {
  --abyss: #F8FAFC;         /* --paper-2, the dashboard's own --bg */
  --deep: #FFFFFF;          /* --paper: raised card surfaces */
  --deep-2: #F1F5F9;        /* --surface-lighter */
  --deep-3: #E2E8F0;        /* --border-subtle */
  --ground-rgb: 248, 250, 252;
  --ink: #0F172A;

  --cream: #0F172A;                      /* --ink,   17.06:1 */
  --cream-soft: #334155;                 /* --ink-2,  9.90:1 */
  --cream-dim: #64748B;                  /* --muted,  4.55:1 */
  --hair: #E2E8F0;                       /* --border-subtle */
  --hair-soft: #EFF3F8;

  --accent: #64748B;        /* --muted; the dark theme's slate is 1.9:1 here */
  --accent-warm: #701111;   /* --accent-red */
  --info: #05213D;          /* --accent-blue */
  --blue: #05213D;
  /* The design system has no lemon. Its primary button is
     `background: var(--accent-blue); color: #fff` (dashboard line 243), so the
     landing's solid fill becomes exactly that. */
  /* The cream fill belongs to the dark ground. Over #F8FAFC it is four points
     away from the page itself, so a cream button with dark text would read as a
     disabled field rather than as the page's one call to action. The light
     theme keeps the design system's own primary (--accent-blue) instead. */
  --cta: #05213D;
  --on-cta: #FFFFFF;        /* 16.26:1 on #05213D */
  --on-blood: #FFFFFF;      /* 11.82:1 on #701111 */
  --veil: #FFFFFF;          /* --paper: a raised card is WHITE on this theme,
                               not a wash of the ink colour */

  /* The ramp keeps its SHAPE -- bright stop first, dark last -- rebuilt around
     --accent-red #701111 as the anchor. Inverting it was tried first and made
     the gloss sweep read as a smear: the sheen is a LIGHT band, so on an
     inverted ramp it landed on the lightest end and vanished.
     Stops measure 11.29 / 8.95 / 6.91 / 4.68:1. */
  --blood-hi: #701111;
  --blood-mid: #8A1A1A;
  --blood: #A52626;
  --blood-deep: #C04A3D;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--abyss);
  color: var(--cream);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* .sky and .stage (the fixed WebGL background/canvas pair) were removed with
   the 3D scene. This replaces the ambient depth the always-rendering canvas
   used to give the whole page -- not just the hero -- with a CSS-only
   starfield: eight radial-gradient dots tiled and drifting, no image
   request, no JS, no WebGL context. Dark theme only (--abyss is the dark
   ground; light theme has no equivalent and stars would just be visible
   grey specks on white). */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(1.6px 1.6px at 12% 22%, rgba(248, 250, 252, 0.55) 50%, transparent 100%),
    radial-gradient(1.2px 1.2px at 27% 68%, rgba(248, 250, 252, 0.4) 50%, transparent 100%),
    radial-gradient(1.6px 1.6px at 41% 14%, rgba(148, 163, 184, 0.5) 50%, transparent 100%),
    radial-gradient(1.2px 1.2px at 58% 77%, rgba(248, 250, 252, 0.35) 50%, transparent 100%),
    radial-gradient(1.6px 1.6px at 73% 33%, rgba(248, 250, 252, 0.45) 50%, transparent 100%),
    radial-gradient(1.2px 1.2px at 86% 61%, rgba(148, 163, 184, 0.4) 50%, transparent 100%),
    radial-gradient(1.6px 1.6px at 94% 9%, rgba(248, 250, 252, 0.35) 50%, transparent 100%),
    radial-gradient(1.2px 1.2px at 6% 88%, rgba(248, 250, 252, 0.3) 50%, transparent 100%);
  background-repeat: repeat;
  background-size: 340px 340px;
  opacity: 0.55;
  animation: starDrift 140s linear infinite;
}
@keyframes starDrift { to { background-position: 340px 680px; } }
@media (prefers-reduced-motion: reduce) { body::before { animation: none; } }
:root[data-theme="light"] body::before { display: none; }

/* .act--galaxy carries its own name for a reason -- a soft radial glow
   behind the stat reveal, denser than the ambient field, so the section
   still reads as its own moment instead of a plainer continuation of
   .act--story right above it. */
.act--galaxy { position: relative; }
.act--galaxy::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60% 45% at 50% 38%, rgba(148, 163, 184, 0.14), transparent 70%),
    radial-gradient(1.8px 1.8px at 20% 30%, rgba(248, 250, 252, 0.6) 50%, transparent 100%),
    radial-gradient(1.4px 1.4px at 35% 60%, rgba(248, 250, 252, 0.45) 50%, transparent 100%),
    radial-gradient(1.8px 1.8px at 50% 20%, rgba(148, 163, 184, 0.55) 50%, transparent 100%),
    radial-gradient(1.4px 1.4px at 65% 70%, rgba(248, 250, 252, 0.4) 50%, transparent 100%),
    radial-gradient(1.8px 1.8px at 80% 40%, rgba(248, 250, 252, 0.5) 50%, transparent 100%),
    radial-gradient(1.4px 1.4px at 90% 75%, rgba(148, 163, 184, 0.4) 50%, transparent 100%);
  background-repeat: repeat;
  background-size: 480px 480px;
}
:root[data-theme="light"] .act--galaxy::before { display: none; }

main { position: relative; z-index: 2; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 99;
  background: var(--cta);
  color: var(--on-cta);
  padding: 10px 16px;
  font: 700 13px/1 var(--font-mono);
}
.skip-link:focus { left: 8px; top: 8px; }

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

/* --- ticker ------------------------------------------------------------- */

.marquee {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 40;
  height: var(--bar-h);
  /* Same ground as the page and the tarifs block. The old hairline was a black
     25% line, invisible now that the bar is no longer yellow, so it becomes a
     faint light rule -- otherwise the band has no edge at all against the page
     behind it. */
  background: var(--abyss);
  color: var(--ink);
  display: flex;
  align-items: center;
  overflow: hidden;
  border-bottom: 1px solid var(--hair);
}
.marquee__track {
  display: flex;
  flex: none;
  gap: 2.5rem;
  padding-right: 2.5rem;
  white-space: nowrap;
  animation: marquee 26s linear infinite;
  font: 700 12.5px/1 'Archivo Black', var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
@keyframes marquee { to { transform: translateX(-50%); } }
.is-still .marquee__track { animation-play-state: paused; }

/* --- top bar ------------------------------------------------------------ */

.topbar {
  position: fixed;
  top: var(--bar-h); left: 0; right: 0;
  z-index: 39;
  /* 1fr | auto | 1fr, so the middle column is centred on the viewport whatever
     the wordmark and the button measure. space-between put "Tarifs" wherever
     the two ends happened to leave room. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
  /* 0.55rem, not 0.85: the CTA's 44px tap target now sets the content height,
     so the old padding would have pushed the bar (and the scrim it casts, at
     210% of its height) 14px further down the page. */
  padding: 0.55rem clamp(1rem, 4vw, 2.5rem);
}

/* Copy scrolls UNDER this bar: every act is a sticky pin that releases at the
   end of its track, so its content rides up behind the header. With no
   background at all the two collided -- on a phone "PLUS DE DONNÉES SUR VOS
   CLIENTS" landed directly on the wordmark and the nav. This scrim fades out
   below the bar, so text disappears behind it instead of through it, without
   drawing a hard edge across the page.
   Negative z-index keeps it behind the wordmark and links; the bar's own
   z-index:39 contains it, so it cannot fall behind the page itself. */
.topbar::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 210%;
  background: linear-gradient(180deg,
    var(--abyss) 0%,
    var(--abyss) 46%,
    rgba(var(--ground-rgb), 0) 100%);
  pointer-events: none;
  z-index: -1;
}

/* Third grid slot: toggle + CTA share it so the middle column stays centred on
   the viewport. The CTA's 44px tap target sets the bar's content height, and
   the scrim above runs to 210% of that -- so the toggle is capped at the same
   44px and cannot grow the bar (or push the scrim down over the first benefit
   card, which is what happened the last time this bar got taller). */
.topend {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: clamp(0.5rem, 2vw, 1rem);
}

/* No container, matching "Se connecter": what marks it is the icon alone. A
   filled pill here was rejected on the CTA for being too loud, and a second
   one beside it would be louder still. The 44px box is the tap target; nothing
   paints it until hover. */
.themetoggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--cream-soft);
  cursor: pointer;
  transition: color 0.2s var(--ease), background-color 0.2s var(--ease);
}
.themetoggle svg { width: 19px; height: 19px; display: block; }
.themetoggle:hover { color: var(--cream); background: var(--hair-soft); }
.themetoggle:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 2px;
}
.wordmark {
  font-family: var(--font-logo);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  color: var(--cream);
  text-decoration: none;
  text-shadow: 0 0 26px rgba(148, 163, 184, 0.35);
  line-height: 1;
  /* The bar is a grid now, so this sits in a 1fr column instead of taking its
     natural width. Without nowrap the lockup broke onto two lines the moment
     that column fell under its 144px. */
  white-space: nowrap;
  /* Lobster ships a single 400 cut -- it is a display script, there is no bold
     to select. font-weight:700 would hand it to the browser's synthetic bold,
     which smears a script's thin connecting strokes; a hairline stroke in the
     text's own colour thickens the same outline cleanly and keeps the counters
     open at the 15px the smallest phones use. */
  -webkit-text-stroke: 0.4px currentColor;
}
/* "App Manager" was 76% cream with no glow, which read as a subtitle hanging
   off the name. It is part of the lockup, so it now takes the same colour, the
   same halo and the same weight as "Neptune" -- only the size still separates
   them. */
.wordmark span {
  font-family: var(--font-logo);
  font-size: 0.62em;
  color: inherit;
  letter-spacing: 0;
  text-transform: none;
  margin-left: 0.42rem;
}
.topnav {
  grid-column: 2;
  justify-self: center;
  display: flex;
  align-items: center;
  gap: clamp(0.6rem, 2vw, 1.4rem);
}
.topnav a {
  font: 700 13px/1 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cream-soft);
  text-decoration: none;
  transition: color 0.2s;
}
.topnav a:hover { color: var(--accent-warm); }

/* No box at all. A solid lemon pill made the bar shout over a page whose whole
   register is quiet, and a bordered ghost pill was just a nav link wearing a
   container. What separates this from "Tarifs" is weight and a rule, not a
   shape: full --cream against --cream-soft, and a hairline that thickens and
   warms on hover. The 44px min-height is kept purely as the touch target --
   nothing paints it. */
.topcta {
  grid-column: 3;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  padding: 0 0.1rem;
  min-height: 44px;
  color: var(--cream);
  font: 700 13px/1 var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  position: relative;
  transition: color 0.25s var(--ease);
}
.topcta::after {
  content: '';
  position: absolute;
  left: 0.1rem;
  right: 0.1rem;
  /* Half the line-box plus the cap height, so the rule sits on the baseline
     rather than at the bottom of the 44px tap target. */
  top: calc(50% + 0.62em);
  height: 1px;
  background: currentColor;
  transition: height 0.25s var(--ease), opacity 0.25s var(--ease);
  opacity: 0.55;
}
.topcta:hover { color: var(--accent-warm); }
.topcta:hover::after { height: 2px; opacity: 1; }

/* --- act scaffolding ---------------------------------------------------- */

.act { position: relative; }
.act__pin {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: calc(var(--bar-h) + 4.5rem) clamp(1rem, 5vw, 3rem) 3rem;
  text-align: center;
}
/* position:relative + explicit z-index so this unambiguously paints above
   .hero-video-wrap. Without it, a positioned sibling with z-index:auto
   (the video wrap) paints AFTER non-positioned in-flow content in the CSS
   stacking order -- regardless of which comes first in the DOM -- so the
   video was rendering on top of the hero text, not behind it. This is what
   made the hook (and, to a lesser extent visually, the lede/CTA) unreadable
   in Safari: not a timing bug, a stacking one. */
.act__inner { width: min(1080px, 100%); position: relative; z-index: 1; }

/* --- act 1: hero -------------------------------------------------------- */

/* Scroll-track lengths.
   These are how far you scroll, not how big anything looks: each act's pin
   holds one frame while progress runs 0..1 over (height - 100vh). They were
   long enough that the hero held "BIENVENUE CHEZ Neptune" static for ~208vh
   before the story could start, which read as a dead gap between the two.
   Trimmed by roughly a third; every beat inside is a fraction of the act, so
   the choreography is unchanged, just tighter per pixel scrolled. */
/* No scroll track any more. This used to be 175vh so the pinned hero could
   hold still while the hook crossfaded into the 3D wordmark -- that crossfade
   is gone (heroBeat2 is display:none, is-out is a no-op), so paintHero drives
   nothing visible. What the track still did was cost ~93vh of scrolling where
   the page visibly did NOT move: the pin is content-sized now, so its stuck
   budget (track height - pin height) grew instead of shrinking. height:auto
   leaves position:sticky in place -- the containing block the video wrap
   depends on is unchanged -- but gives it nowhere to stick, so the hero
   scrolls away with the page from the first pixel. */
.act--hero { height: auto; }

/* The hero splits the frame in two: copy in the upper half, devices in the
   lower half. Centred in the pin (the default for every other act) the hook
   sat exactly where the old phone and the two hero devices live, so the words
   were printed over a beige plastic slab. Nothing here shrinks the models --
   they are meant to be big and to run off the edges; the copy simply moves out
   of their way. */
/* Trimmed from 6.5rem/3rem: this pin now carries the full permanent stack
   (badges, welcome block and CTAs included, not just the hook), and real
   browser chrome (bookmarks bar, tab strip) eats far more of a window's
   height than the raw viewport suggests -- 4.5rem is the least top padding
   that still clears .topbar's own rendered height (measured ~61.6px, on
   top of the marquee --bar-h already adds), with a small margin. */
.act--hero .act__pin {
  align-items: start;
  padding-top: calc(var(--bar-h) + 4.5rem);
  padding-bottom: 1.75rem;
}
/* The hook can use more of the width now that it is not fighting a model for
   the middle of the screen: fewer, longer lines read better than a narrow
   stack of three. */
.act--hero .hook { max-width: 30ch; }
/* Hook, store badges, lede, welcome block and CTAs stack in one column at
   every width now -- this used to be mobile-only (desktop relied on the
   hook/heroBeat2 crossfade below instead), but the hero is now the same
   permanent stack everywhere, just sized for the room each width has. */
.act--hero .act__inner { display: flex; flex-direction: column; align-items: center; }
.hero-stack { order: 1; }
.hero-stores { order: 2; }
.hero-lede { order: 3; }
.hero-welcome-mobile { order: 4; }
.hero-actions { order: 5; }
/* Every other act's pin wants min-height:100svh, filling the screen while
   its sticky "hold one frame" story beat plays. The hero is no longer that
   -- it's a fixed stack whose real height depends on its content, and
   forcing a full-screen floor here just strands "Défilez" (and, on short
   windows, the CTAs) below the fold with nothing under them. Hero-only:
   every other act keeps its full-screen hold. */
.act--hero .act__pin { min-height: 0; }

/* --- marker: animated highlighter stroke ---------------------------------- *
 * A fixed fill, independent of --cta (which flips to navy in the light theme
 * for the button system) -- this is a literal highlighter mark, not a semantic
 * accent, so it stays the same colour in both themes on purpose. It is the
 * cream the CTA and the first pitch card use, so the page has one warm fill
 * rather than three.
 *
 * The word ON the mark is darkened with it. It used to be --cream (near white)
 * over #F5E663, which measured about 1.5:1 -- legible only because the script
 * face is heavy and enormous. Over the cream it would have been invisible, so
 * this is a contrast fix that the recolour forced rather than an aesthetic
 * choice: a highlighter has dark text on it.
 * -------------------------------------------------------------------------- */
/* NOT --on-cta. That token flips with the theme because the CTA's fill does
   (cream on dark, navy on light) -- but this mark's fill does not: it is a
   fixed cream in both themes, so its ink has to be fixed too. Bound to it, the
   word came out white-on-cream on the light theme, which is invisible.
   The ink arrives WITH the sweep rather than before it. The stroke is drawn by
   `markIn` after a 0.5s delay; a word already set in dark ink during that delay
   is dark-on-navy on the dark theme, i.e. invisible until the highlighter
   catches up. Running the colour over the same window means the word is always
   legible against whatever is actually behind it. */
.marker {
  position: relative;
  white-space: nowrap;
  display: inline-block;
  color: var(--cream);
  animation: markerInk 0.5s 0.5s var(--ease) forwards;
}
@keyframes markerInk { to { color: #0F172A; } }
.marker .stroke {
  position: absolute;
  left: -0.06em; right: -0.06em; bottom: 0.02em; top: 0.1em;
  z-index: -1;
  background: #FDFBE3;
  clip-path: polygon(0 8%, 100% 0, 100% 92%, 0 100%);
  transform: scaleX(0) rotate(-1deg);
  transform-origin: left center;
  animation: markIn 0.5s 0.5s var(--ease) forwards;
}
@keyframes markIn { to { transform: scaleX(1) rotate(-1deg); } }
@media (prefers-reduced-motion: reduce) {
  /* The mark is painted immediately here, so the ink must be too. */
  .marker { animation: none; color: #0F172A; }
  .marker .stroke { animation: none; transform: scaleX(1) rotate(-1deg); }
}

/* --- hero-welcome-mobile --------------------------------------------------- *
 * Used to be mobile-only: desktop kept #heroBeat2's scroll-triggered
 * crossfade and its 3D clay wordmark instead, which needed the room a wide
 * viewport gives it. Now shown at every width -- permanent, plain-styled
 * like .hook rather than the clay treatment, with the marker doing the
 * emphasis instead -- so the hero is the same stack everywhere. #heroBeat2
 * is retired below (display:none unconditionally) so the two never say the
 * same thing twice.
 * -------------------------------------------------------------------------- */
.hero-welcome-mobile { display: block; text-align: center; margin-top: 1rem; }
.hero-stores { display: block; width: 240px; height: auto; margin: 0.75rem auto 0; }
/* The source badges are Apple/Google's black pill artwork -- reads fine on
   the light-theme page, all but invisible once the surrounding hero is
   dark too (black pill on a near-black video/abyss backdrop). No spare
   background chip (would fight the "video shows through" hero treatment):
   inverting the image itself turns the black pill white with dark
   logo/text, which is legible against a dark ground and close to Apple's
   own alternate white badge in effect, without needing a second asset. */
:root[data-theme="dark"] .hero-stores,
:root:not([data-theme="light"]) .hero-stores {
  filter: invert(1) hue-rotate(180deg);
}
:root[data-theme="light"] .hero-stores { filter: none; }
/* Lobster (the wordmark's own script face) instead of the display face --
   the loudest thing in the hero, matching the treatment the brand name
   gets in the nav lockup. The clamp's own upper bound (6.3rem) is already
   reached by ~520px wide, so the same rule holds this size steady across
   tablet and desktop without a second, desktop-specific value. */
.hero-welcome-line {
  font-family: var(--font-logo);
  font-size: clamp(4.5rem, 19.5vw, 6.3rem);
  line-height: 1.05;
  margin: 0;
  color: var(--cream);
}
.hero-welcome-line + .hero-welcome-line { margin-top: 0.3rem; }

/* --- hero-video ------------------------------------------------------------ *
 * Takes the welcome block's slot when a film is configured. Three things ask
 * for the click, in descending loudness: rings pulsing out of the play button,
 * a sheen crossing the poster, and the card's slow float. All three stop under
 * reduced motion / .is-still, where the play button alone carries it.
 * -------------------------------------------------------------------------- */
.hero-video {
  order: 4;
  display: block;
  width: min(420px, 86vw);
  margin: clamp(1rem, 2.4vw, 1.5rem) auto 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--cream);
  cursor: pointer;
  animation: heroFurnitureIn 0.7s var(--ease) 0.36s both;
}
.hero-video__frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: 18px;
  overflow: hidden;
  border: 2px solid var(--cta);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45), 0 0 0 6px rgba(253, 251, 227, 0.12);
  transition: scale 0.35s var(--ease), box-shadow 0.35s var(--ease);
  animation: heroVideoFloat 5s ease-in-out 1.1s infinite;
}
/* `scale`, not `transform`: the float animation owns transform and would
   override a hover value written to the same property. */
.hero-video:hover .hero-video__frame,
.hero-video:focus-visible .hero-video__frame {
  scale: 1.03;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5), 0 0 0 8px rgba(253, 251, 227, 0.22);
  animation-play-state: paused;
}
.hero-video:focus-visible { outline: 2px solid var(--cta); outline-offset: 8px; border-radius: 22px; }
/* maxresdefault (16:9) only exists for videos uploaded in HD. When changing
   LANDING_VIDEO_ID, check that URL answers 200 before deploying. */
.hero-video__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: brightness(0.78);
  transition: filter 0.35s var(--ease);
}
.hero-video:hover .hero-video__poster { filter: brightness(0.92); }
.hero-video__sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.35) 50%, transparent 60%);
  transform: translateX(-100%);
  animation: heroVideoSheen 4.5s ease-in-out 1.6s infinite;
}
/* Fixed cream fill and navy glyph, like .marker: it sits on the poster, not
   on the page ground, so neither theme's tokens apply. */
.hero-video__play {
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(56px, 16%, 72px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #FDFBE3;
  color: #0F172A;
}
.hero-video__play svg { width: 46%; height: 46%; margin-left: 6%; }
.hero-video__play::before,
.hero-video__play::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid #FDFBE3;
  animation: heroVideoRing 2.2s ease-out infinite;
}
.hero-video__play::after { animation-delay: 1.1s; }
.hero-video__label {
  display: block;
  margin-top: 0.8rem;
  font: 700 12px/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
}
@keyframes heroVideoFloat {
  0%, 100% { transform: translateY(0) rotate(0); }
  50% { transform: translateY(-6px) rotate(-0.6deg); }
}
@keyframes heroVideoSheen {
  0% { transform: translateX(-100%); }
  35%, 100% { transform: translateX(100%); }
}
@keyframes heroVideoRing {
  from { transform: scale(1); opacity: 0.85; }
  to { transform: scale(2.1); opacity: 0; }
}
.is-still .hero-video__frame,
.is-still .hero-video__sheen,
.is-still .hero-video__play::before,
.is-still .hero-video__play::after { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .hero-video { animation: none; }
  .hero-video__frame,
  .hero-video__sheen,
  .hero-video__play::before,
  .hero-video__play::after { animation: none; }
  .hero-video__sheen,
  .hero-video__play::before,
  .hero-video__play::after { display: none; }
}
.is-still .hero-video__sheen,
.is-still .hero-video__play::before,
.is-still .hero-video__play::after { display: none; }

/* The film itself. Dark in both themes: it is a cinema, not a page surface. */
.has-video-open { overflow: hidden; }
.video-dialog {
  width: min(960px, 94vw);
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  background: none;
  color: #f8fafc;
  overflow: visible;
}
.video-dialog::backdrop { background: rgba(2, 11, 22, 0.88); backdrop-filter: blur(6px); }
.video-dialog[open] { animation: videoDialogIn 0.35s var(--ease); }
@keyframes videoDialogIn {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: none; }
}
.video-dialog__inner { position: relative; }
/* Capped by height too, so the CTA under the film stays on screen on a short
   or landscape viewport. */
.video-dialog__player {
  aspect-ratio: 16 / 9;
  width: min(100%, calc((100svh - 190px) * 16 / 9));
  margin: 0 auto;
  border-radius: 16px;
  overflow: hidden;
  background: #000;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
}
.video-dialog__player iframe { width: 100%; height: 100%; border: 0; display: block; }
.video-dialog__close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: 0 0 0.5rem auto;
  padding: 0;
  border: 1px solid rgba(248, 250, 252, 0.3);
  border-radius: 50%;
  background: rgba(2, 11, 22, 0.6);
  color: inherit;
  cursor: pointer;
}
.video-dialog__close svg { width: 20px; height: 20px; }
.video-dialog__close:focus-visible { outline: 2px solid #FDFBE3; outline-offset: 3px; }
.video-dialog__cta {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.75rem 1.25rem;
  margin-top: 1rem;
}
.video-dialog__cta p { margin: 0; font-size: 1rem; }
/* The dark theme's CTA pair, pinned: the dialog is dark on the light theme
   too, where --cta is navy and would vanish into the backdrop. */
.video-dialog__cta .btn--solid { background: #FDFBE3; color: #0b1220; }
@media (prefers-reduced-motion: reduce) {
  .video-dialog[open] { animation: none; }
}

/* Both hero beats occupy the same grid cell -- harmless now that beat 2 is
   always hidden below, but left as-is rather than restructured. */
.hero-stack { display: grid; }
.hero-stack > * { grid-area: 1 / 1; align-self: center; }

.hero-beat {
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
}
.hero-beat--1 { opacity: 1; }
/* The hook now stays up permanently at every width, matching mobile:
   "Bienvenue chez Neptune" no longer lives in this crossfade at all, it is
   .hero-welcome-mobile's own permanent block further down the stack, so
   there is nothing left for the hook to hand off to. JS still toggles
   is-out on scroll (unchanged -- other values are derived off the same
   fraction), it just no longer has anything to do here. */
.hero-beat--1.is-out { opacity: 1; transform: none; }
/* The scroll-triggered 3D clay wordmark is retired in favour of
   .hero-welcome-mobile's permanent treatment -- kept in the DOM/JS rather
   than removed (its word-reveal timing feeds other values) but never
   shown, at any width. */
.hero-beat--2 { display: none; }

/* The hook is French now, so it takes the display face and reads left to
   right. It used to be a Tunisian-Arabic line in Amiri/Titr at direction:rtl
   with only its Latin run promoted to the display face; 18ch was sized for
   Arabic and is too tight for the French sentence. */
/* TEST: every scroll/JS-tied visibility rule stripped from .hook -- no
   opacity:0 base, no transition, no .is-in dependency, nothing keyed to
   NP.acts.hero or is-out. Permanently opacity:1 from first paint, full
   stop. This isolates whether the disappearing-text bug lives in that
   machinery at all, per your instruction to remove it and test. If the
   text still vanishes with this in place, the cause is elsewhere (most
   likely heroBeat2 fading in on top of it in the shared grid cell, or a
   Safari-specific rendering quirk with -webkit-background-clip: text --
   neither of which this change touches). */
.hook {
  /* Archivo Black, not the display face. League Gothic ships a single 400 cut
     (three unicode subsets, all weight 400 -- see google-fonts.css), so asking
     it for bold gets synthetic bolding: the browser smears the outline, which
     on a condensed face reads as a rendering fault rather than as weight.
     Archivo Black IS the black cut, it is already loaded and preloaded for the
     benefits act, and it is the page's own heavy display voice. It is a much
     wider face, so the line-height comes down and the measure with it. */
  font-family: 'Archivo Black', var(--font-display), sans-serif;
  font-weight: 400;
  /* Sized against the face, not against the old one. Measured on a real render
     (fonts only load from the real origin, not from a file:// copy), Archivo
     Black runs about 0.52em per character here against League Gothic's 0.40 --
     so the same nominal size is a third wider. A straight 1.4x came out at
     103px, which wrapped "Passez au next level" onto two lines and left the
     headline occupying half the hero.
     4.8rem is the largest size that still holds that line in one piece inside
     the 1080px measure with room to spare, and against the old 4.6rem of a much
     narrower face it still reads as a clear step up in both size and weight.
     max-width goes back to none: a ch-based cap is measured in the FACE's own
     character width, so it was re-breaking the line that the size already
     fits. */
  font-size: clamp(2.1rem, 5.4vw, 4.8rem);
  line-height: 1.06;
  max-width: none;
  margin: 0 auto;
  direction: ltr;
  text-wrap: balance;
  opacity: 1;
}
.hook--sub {
  font-size: clamp(1.7rem, 4.2vw, 3.7rem);
  margin-top: 0.3rem;
}

.display {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(3.2rem, 12.5vw, 11rem);
  line-height: 0.86;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: var(--accent);
  margin: clamp(0.6rem, 2vw, 1.2rem) 0;
  text-shadow: 0 0 60px rgba(148, 163, 184, 0.25);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 0.28em;
}
.display .w { display: inline-block; overflow: hidden; }
.display .w i {
  display: inline-block;
  font-style: normal;
  transform: translateY(105%);
  transition: transform 0.75s var(--ease);
}
/* Gated on the beat, not on the act entering -- otherwise the words would have
   already slid up behind the hook and beat 2 would arrive pre-animated. */
.hero-beat--2.is-on .display .w i,
.is-in .block .display .w i { transform: none; }

/* --- clay: the Neptune wordmark as modelling putty ---------------------- *
 * Three stacked layers of the same glyphs:
 *   ::before  a darker body, offset down, giving the extruded thickness
 *   the text  a soft radial ramp (highlight top-left -> shadow bottom-right)
 *   ::after   fine grain, so the surface reads matte rather than glassy
 * Rounded, very heavy letterforms do most of the work -- the shading alone
 * cannot make a condensed grotesque look like putty.
 * ------------------------------------------------------------------------ */

.display--clay {
  position: relative;
  /* fit-content, not block: scene.js scales the 3D wordmark to this element's
     rect, and a full-width block box would size it to the column, not the word. */
  display: block;
  width: fit-content;
  margin-inline: auto;
  font-family: 'Baloo 2', 'Fredoka', var(--font-display);
  font-weight: 800;
  font-size: clamp(3rem, 11vw, 9.5rem);
  line-height: 1.02;
  letter-spacing: -0.005em;
  text-transform: none;
  isolation: isolate;
  background-image:
    /* travelling gloss, so the putty reads wet-shiny rather than matte */
    linear-gradient(100deg,
      transparent 36%,
      rgba(255, 232, 226, 0.42) 48%,
      transparent 60%),
    /* tight specular bloom, top-left, where the light hits the bulge */
    radial-gradient(38% 44% at 27% 14%,
      rgba(255, 238, 234, 0.92) 0%,
      rgba(255, 202, 194, 0.42) 42%,
      rgba(255, 190, 180, 0) 72%),
    /* body ramp: lit blood red rolling to a deep shaded underside */
    radial-gradient(112% 140% at 30% 12%,
      #ffb9ac 0%,
      #fa8874 15%,
      #ef5a4e 32%,
      #d13a33 52%,
      #b02024 72%,
      #8e161a 88%,
      #6f1014 100%);
  background-size: 240% 100%, 100% 100%, 100% 100%;
  background-position: 160% 0, 0 0, 0 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* Ambient occlusion under the form, not a hard cast shadow. */
  filter:
    drop-shadow(0 6px 10px rgba(0, 0, 0, 0.32))
    drop-shadow(0 22px 38px rgba(90, 10, 12, 0.38));
  /* Held back until the Arabic welcome has landed -- see .is-pop below. */
  opacity: 0;
  animation: none;
}

/* Just enough body behind the lit surface to suggest thickness. A deep hard
   extrusion reads as letterpress; clay wants a shallow, soft underside. */
.display--clay::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #7d1418;
  -webkit-text-fill-color: #7d1418;
  background: none;
  text-shadow:
    0 2px 0 #741216, 0 4px 0 #681014, 0 6px 0 #5c0e12,
    0 9px 6px rgba(42, 4, 6, 0.6),
    0 14px 16px rgba(0, 0, 0, 0.42);
}

/* Matte grain over the surface only. */
.display--clay::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
  background-size: 140px 140px;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  opacity: 0.22;
  mix-blend-mode: multiply;
}

/* Pop: overshoot, settle. Fires only on .is-pop, which trails .is-on -- the
   Arabic welcome lands first, then Neptune arrives. The gloss sweep starts
   after the pop has settled. */
.hero-beat--2.is-pop .display--clay {
  animation:
    clayPop 0.86s var(--ease) both,
    claySheen 7s ease-in-out 0.95s infinite;
}
@keyframes claySheen {
  0%        { background-position: 160% 0, 0 0, 0 0; }
  55%, 100% { background-position: -60% 0, 0 0, 0 0; }
}
@keyframes clayPop {
  0%   { opacity: 0; transform: scale(0.52) translateY(30px) rotate(-3deg); }
  55%  { opacity: 1; transform: scale(1.09) translateY(0) rotate(1.2deg); }
  76%  { transform: scale(0.965) rotate(-0.6deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}
.is-still .hero-beat--2.is-pop .display--clay { animation: none; opacity: 1; }

/* Once the real extruded wordmark is in the scene, the CSS one steps aside but
   keeps its layout box -- scene.js positions the 3D word from that box. */
.has-word3d .display--clay {
  visibility: hidden;
  animation: none;
}

/* The hook's Latin run is a title too, so it takes the display face rather
   than the Amiri fallback Titr leaves it on. Condensed, so it needs a nudge up
   in size to sit level with the Arabic beside it. */
.hook-lat {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.18em;
  letter-spacing: 0.005em;
  direction: ltr;
  display: inline-block;
  vertical-align: baseline;
}

/* "Bienvenue chez", the line that hands off to the clay wordmark. Inherits
   .display (League Gothic, uppercase, ltr) and only resets the scale, which
   .display sets far larger than this line should be. Was .display--ar, an
   Amiri right-to-left line, until the copy went fully French. */
.display--intro {
  font-size: clamp(2rem, 6.5vw, 4.6rem);
  line-height: 1.25;
  gap: 0 0.22em;
}

.lede {
  max-width: 46ch;
  margin: 0 auto;
  font-size: clamp(0.98rem, 1.5vw, 1.14rem);
  line-height: 1.6;
  color: var(--cream-soft);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.7s var(--ease) 0.25s, transform 0.7s var(--ease) 0.25s;
}
.is-in .lede { opacity: 1; transform: none; }

.btn {
  font: 700 13px/1 var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 1.05rem 1.9rem;
  border-radius: 999px;
  text-decoration: none;
  border: 1px solid transparent;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  transition: transform 0.25s var(--ease), background 0.25s, color 0.25s, border-color 0.25s;
}
.btn:hover { transform: translateY(-2px); }
.btn--solid { background: var(--cta); color: var(--on-cta); }
/* brightness(), not a second hardcoded fill. `background: #fff08a` was the
   dark theme's lighter yellow; on the light theme it repainted a navy button
   pale yellow under white text -- 1.16:1 on hover. filter scales whatever the
   fill is, so it holds for both, and it is what the design system's own
   `button:hover` uses. */
.btn--solid:hover { filter: brightness(1.12); }
.btn--ghost { border-color: var(--hair); color: var(--cream); background: var(--veil); }
.btn--ghost:hover { border-color: var(--accent-warm); color: var(--accent-warm); }

/* Used to be pinned a fixed 5rem above the first viewport height, absolute
   against .act--hero -- fine while the hero's content was short and static.
   Now that the hook, badges, lede, welcome block and CTAs all stack
   permanently (every width, not just mobile), that fixed offset is exactly
   as tall as the stack happens to be on a given day, nothing more -- it
   already landed on top of the CTAs once. Normal flow, right after the
   pin (min-height:0'd above for the same reason), always sits below
   whatever height the stack actually reaches. */
/* Lives inside .act--hero's pin, as its last child. Kept in normal flow (it
   used to be absolutely positioned a fixed distance down the viewport, which
   landed it on top of the CTAs once the stack grew) -- but it has to be
   INSIDE the pin, not a sibling of it: as a sibling its height counted toward
   the act while the pin's did not, so the act ended up ~46px taller than the
   pin and position:sticky had exactly that much travel to hold still in. */
.scroll-cue {
  position: static;
  margin: 0.75rem auto 0;
  font: 500 10px/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--cream-dim);
  display: grid;
  justify-items: center;
  gap: 0.5rem;
}
.scroll-cue__arrow {
  width: 16px;
  height: 16px;
  animation: cue 1.6s ease-in-out infinite;
}
@keyframes cue {
  0%, 100% { transform: translateY(0); opacity: 0.45; }
  50% { transform: translateY(5px); opacity: 1; }
}
.is-still .scroll-cue__arrow { animation: none; opacity: 0.7; }

/* --- act 2: narrative --------------------------------------------------- */

/* Pulled up so it overlaps the hero's exit.
   Measured before this: the hero's beat 2 left the top of the frame at +30vh
   past the pin release, and the story's first line only reached the bottom
   edge at +70vh -- about 40vh where the middle of the screen was empty,
   because each pin only centres its own content once its own act reaches the
   top of the viewport. Overlapping the two acts means the story is already
   arriving from the bottom while "BIENVENUE CHEZ Neptune" is still leaving
   through the top, so the handover reads as a crossfade instead of a void.
   The acts genuinely overlap in the document; .act is position:relative and
   the story is the later sibling, so it paints over the departing hero. */
/* --- act 2: the pitch, one card at a time --------------------------------- *
 * Four sentences, four identical cards, in one place. A card holds the frame,
 * then the next rides up over it and takes the frame completely: one appears,
 * the one before it is gone. Flicking through a deck, not building a pile.
 *
 * Native, and the whole mechanism is two declarations. Every card is
 * `position: sticky` at the SAME top, so a card climbs to that line, halts, and
 * the next one climbs over it. Nothing is scrubbed, scripted or timed:
 *
 *   * the cards are the same size and fully opaque, so a card that arrives
 *     covers its predecessor exactly -- that is what makes it read as a
 *     replacement rather than as a stack. The reference's own 20px-per-card
 *     fan is deliberately NOT used here for the same reason: a fan leaves a
 *     pile of edges on screen, which is the opposite of one card at a time.
 *   * nothing is tilted, for the same reason again: a rotated card underneath
 *     pokes its corners out past the one covering it.
 *   * how fast it goes is the list's geometry and nothing else. The card's own
 *     height is how far you scroll during a swap; the gap under it is how long
 *     the card holds the frame alone. Both are deliberately short -- the four
 *     cards are done inside roughly one screen of scrolling.
 *
 * THIS ACT IS NOT PINNED, and it is the only one on the page that is not.
 * `position: sticky` moves nothing while its container holds still, so a card
 * inside a pinned frame cannot stack. The section is in normal flow, with no
 * `.act__pin`. It keeps `data-act="story"` because scene.js still choreographs
 * the 3D phone off this section's progress -- measured over the section's own
 * height now rather than a 190vh track, so those beats re-time with it.
 *
 * No icons, no photographs, no card that is shaped differently from its
 * neighbours: the four sentences carry equal weight, so they get equal cards.
 * The surfaces are soft, low-chroma pastels rather than the page's navy -- they
 * are the one warm, quiet thing in a dark section, which is the whole point of
 * them.
 * ------------------------------------------------------------------------- */
.act--story {
  height: auto;
  margin-top: 0;
  padding: clamp(3rem, 11vh, 8rem) clamp(1rem, 5vw, 3rem) clamp(2rem, 6vh, 5rem);
  /* Card size is free to be chosen for looks: because the gap below is derived
     from it, a smaller card simply gets a bigger gap and the "one at a time"
     rule holds either way. 520px was tried first and left the sentence adrift
     in a half-empty card. */
  --sfc-card-h: min(420px, calc(100svh - 180px));
  /* The line every card climbs to and stops at -- centred in the viewport
     rather than pinned near the top, which is what lets a card sit in the frame
     instead of hanging from its ceiling. max() keeps it clear of the fixed
     topbar if the viewport is ever too short for the centring to make sense. */
  --sfc-top: max(72px, calc((100svh - var(--sfc-card-h)) / 2));
  /* How far past the fold the NEXT card waits while this one is at rest. Small
     on purpose -- it is pure dead scrolling, and the gap rule below already
     guarantees the next card is off screen. */
  --sfc-dwell: 28px;
  /* Ink for the pastel surfaces. Not an invented near-black: this is Neptune UI
     v1's own --ink (brand_portal_dashboard.html), the colour every other
     Neptune surface sets type in. --muted below is that palette's grey. */
  --sfc-ink: #0F172A;
  /* Neptune UI v1's --ink-2, not its --muted. --muted (#64748B) is specified
     against white; over these four pastels it measures 4.55 / 4.10 / 4.01 /
     3.82:1, so three of the four cards failed WCAG AA for a 12px label. --ink-2
     clears it everywhere with room to spare (9.90 / 8.91 / 8.73 / 8.30:1) and
     is still the palette's SECONDARY ink, which is what this label is. */
  --sfc-muted: #334155;
}
.sfc__eyebrow {
  width: min(760px, 100%);
  margin: 0 auto clamp(1.5rem, 4vh, 2.5rem);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
}
.sfc__list {
  width: min(880px, 100%);
  margin: 0 auto;
  list-style: none;
  padding: 0;
  display: grid;
  /* ONE CARD AT A TIME, and this rule is the whole of it.
     The gap is not a taste value: it is measured so that when a card is resting
     on the top line, the next card's top edge is exactly one dwell BELOW the
     fold. Any smaller and two or three cards share the screen, which is what
     this section looked like before -- a list, not a deck.
       card rests occupying   --sfc-top .. --sfc-top + --sfc-card-h
       fold is at             100svh
       so the gap must be     100svh - (--sfc-top + --sfc-card-h) + dwell
     One consequence worth knowing: this fixes the cycle at roughly one viewport
     of scroll per card whatever the card's height, because the arriving card
     has to travel from below the fold up to the top line either way. Four cards
     therefore cost about (100svh - --sfc-top) each, and that is the price of one
     at a time. Centring the card shortens it: the higher the rest line, the
     further the arriving card has to climb. */
  gap: calc(100svh - var(--sfc-top) - var(--sfc-card-h) + var(--sfc-dwell));
  /* This is the LAST card's dwell, and nothing else's. A sticky element is
     released by the bottom edge of its containing block, so every card but the
     last is held in place by the cards under it -- only card four depends on
     this padding, and it holds the frame for exactly as many pixels as there
     are here. At 9vh it held for 81px, which is barely a flick; 20vh gives it
     a real beat to be read on before the section hands off. */
  padding-bottom: clamp(3rem, 20vh, 10rem);
}
.sfc-card {
  /* The whole effect. Every card stops on the SAME line, so each new card
     covers the one before it instead of settling beside it. */
  position: sticky;
  top: var(--sfc-top);
  display: flex;
  flex-direction: column;
  /* Centred on both axes. The card is a frame with one sentence in it, and a
     sentence parked in a corner of a large empty card reads as a layout that
     ran out of content. */
  align-items: center;
  justify-content: center;
  text-align: center;
  /* A fixed height, not a minimum: the cards have to be identical in size or a
     shorter one would not fully cover the card it replaces, and these four
     sentences are not the same length. */
  height: var(--sfc-card-h);
  gap: clamp(0.9rem, 2vw, 1.5rem);
  padding: clamp(1.8rem, 3.6vw, 3rem);
  /* Generously round: at this radius the corner reads as a soft edge rather
     than as a cut, which is most of what makes a card feel calm. */
  border-radius: 28px;
  background: var(--card-bg);
  color: var(--sfc-ink);
  /* Wide, weak and low-contrast -- it lifts the card off the ground without
     drawing a hard seam where the arriving card crosses the one below. */
  box-shadow: 0 20px 44px rgba(2, 11, 22, 0.34);
}
/* Four quiet surfaces. With every card landing in the same place, the colour
   change IS the cue that the card has been replaced, so no two neighbours may
   share a hue -- warm and cool alternate down the run.
   None of these is picked by eye: each is one of the palette's OWN accents
   mixed into white, so the section cannot drift away from the rest of Neptune.
   The greenish tint that used to sit at 02 is gone; neither this page's tokens
   nor Neptune UI v1 has a green outside the status colours, and a status colour
   on a decorative card would be claiming a meaning it does not have.
   Card 01 is not a copy of the CTA's cream, it IS --cta: the owner picked this
   card's colour to replace the page's yellow, so the two must never drift. On
   the light theme --cta becomes the design system's navy and this card would
   follow it into a dark surface with dark text, which is why the light-theme
   block at the end of this section pins card 01 back to the cream. */
.sfc-card:nth-child(1) { --card-bg: var(--cta); }  /* the page's solid fill  */
.sfc-card:nth-child(2) { --card-bg: #ECEEF2; }  /* --accent       18% on white */
.sfc-card:nth-child(3) { --card-bg: #F8E8E8; }  /* --accent-warm  30% on white */
.sfc-card:nth-child(4) { --card-bg: #DFE6FA; }  /* --blue         14% on white */

.sfc-card__value {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  color: var(--sfc-muted);
}
.sfc-card__title {
  margin: 0;
  /* A measure, not the card's full width: centred display type set edge to edge
     is hard to track back to the start of the next line. */
  max-width: 26ch;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.2vw, 3.3rem);
  /* Looser than the page's display setting (0.92-1.14). The lines are full
     sentences read at rest, not headlines. */
  line-height: 1.16;
  color: inherit;
  text-wrap: balance;
}

/* The pastels are chosen against the dark ground. On the light theme they sit
   on #F8FAFC, only a few points away, so the cards lose their edge: a hairline
   and a much lighter shadow give it back without changing the palette. */
:root[data-theme="light"] .sfc-card {
  border: 1px solid rgba(15, 23, 42, 0.08);
  box-shadow: 0 14px 30px rgba(15, 23, 42, 0.10);
}
/* --cta is navy on this theme (see the token block at the top), and a navy card
   under navy type is unreadable. The card keeps the cream literally here. */
:root[data-theme="light"] .sfc-card:nth-child(1) { --card-bg: #FDFBE3; }

/* --- act 3: benefits ---------------------------------------------------- */

/* The -40vh pull-up this rule used to carry was measured against the story act
   as a 190vh scroll TRACK whose pinned frame held still while the reader kept
   scrolling: the overlap is what made the first benefit start counting while
   the story's pin was still leaving, instead of popping in 457 px late.
   The story is a normal-flow section now (see act 2 above), so there is no
   pinned frame left to overlap: its last card is stuck to the top of the
   screen right up to the section's bottom edge, and pulling this act up by
   40vh would slide it over that finished stack. The stack's own trailing
   padding does the hand-off instead. */
.act--benefits { height: 165vh; margin-top: 0; }
/* Section labels ("FONCTIONNEMENT", "TARIFS", "VOTRE COMPTE"). Doubled from
   11px to 22px and moved off Space Mono onto Archivo Black -- the page's own
   brutalist face, already loaded for the benefits, and the only genuinely heavy
   weight in the set (it ships a single 400 that IS the black cut, so
   font-weight has no effect on it). Tracking comes down from 0.26em to 0.12em:
   wide letterspacing is a small-caps device and only makes a 22px word look
   loose. */
.eyebrow {
  font-family: 'Archivo Black', var(--font-display), sans-serif;
  font-size: clamp(1.25rem, 4vw, 1.375rem);
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 1.1rem;
}
.h2 {
  font-family: var(--font-display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(2.3rem, 7vw, 5.4rem);
  line-height: 0.92;
  margin: 0 0 1.4rem;
  color: var(--cream);
  text-wrap: balance;
}
.h2 em { font-style: normal; color: var(--accent); }

/* --- act 3: benefits ------------------------------------------------------ *
 * No containers. Four statements scattered across the frame, each popping in on
 * its own step of the scroll and STAYING for the rest of the act, so the field
 * fills up as the reader goes. Archivo Black is Neptune's own brutalist display
 * face -- heavier than League Gothic and it holds at these sizes.
 * -------------------------------------------------------------------------- */

/* Wider than the page's usual 1080 measure (and .act__inner is widened to
   match, just for this act). The four phrases anchor to this box's left and
   right edges, so widening it is what opens a clear centre channel for the
   phone to stand in at full size -- measured, the phrases run ~308px wide
   each, so a 1280 box leaves 334..959 clear, a 625px channel. At 1080 that
   channel was only 243px and the phone had to shrink or dodge to fit. */
.benefits {
  position: relative;
  list-style: none;
  margin: 0 auto;
  padding: 0;
  width: min(1280px, 100%);
  height: min(66vh, 620px);
}
.act--benefits .act__inner { width: min(1280px, 100%); }

.benefits li {
  position: absolute;
  /* The 360px cap is what keeps the centre channel open. 46% alone was
     fine against the old 1080 box (it left the phrases wrapping to two
     lines ~308px wide), but 46% of the widened 1280 box is 589px -- enough
     for "Votre marque sur l'écran d'accueil" to un-wrap onto ONE 513px
     line, which reaches right into the middle and straight through the
     phone. Capping the measure keeps the two-line shape they already had,
     so widening the box widens the gap between the columns instead of the
     columns themselves. */
  max-width: min(46%, 360px);
  opacity: 0;
  /* Above .benefits-media below regardless of DOM order (the video is a
     later sibling, appended last so it doesn't shift these nth-child
     indices) -- z-index:auto siblings paint at the same level as z-index:0
     ones by DOM order, so an explicit 1 here is what keeps the phrases
     reliably on top if a corner ever overlaps the centred clip. */
  z-index: 1;
  /* Overshoot on the way in gives it the pop; the exit is never used because
     each item latches on for the rest of the act. */
  transform: scale(0.62) translateY(26px);
  transition:
    opacity 0.42s var(--ease),
    transform 0.62s cubic-bezier(0.18, 1.5, 0.36, 1);
  will-change: transform, opacity;
}
.benefits li.is-on { opacity: 1; transform: none; }

/* Scattered, not stacked: each corner of the frame gets one. */
.benefits li:nth-child(1) { top: 0;    left: 0;               text-align: left; }
.benefits li:nth-child(2) { top: 24%;  right: 0;              text-align: right; }
.benefits li:nth-child(3) { top: 52%;  left: 2%;              text-align: left; }
.benefits li:nth-child(4) { bottom: 0; right: 1%;             text-align: right; }

/* The phone holds its size and its place; the phrases are what move.
 *
 * Earlier passes had it shrink and dodge into a corner as each phrase
 * arrived, which read as the product retreating from its own argument --
 * and it ended up too small to be worth showing. What made that seem
 * necessary was measuring collisions against this element's BOX. The
 * source frames are 43.8% phone and 56.2% transparent padding (measured
 * across every frame: the union alpha bbox is 140x272 inside 320x320), so
 * the box was more than twice the width of the thing anyone can see. The
 * frames are now cropped to that silhouette, which makes the box the
 * phone, and the phone then simply stands in the centre channel the
 * widened .benefits box opens up: at 1280 the phrases clear 334..959, and
 * a 300px-wide phone centred on 640 sits 490..790 -- 156px of daylight on
 * the left, 169px on the right, at every scroll position.
 *
 * So scroll drives only two things here, neither of them size:
 *   - the clip's own frame (scrubbed in the script below), which is the
 *     phone rotating and its layers separating; that IS the animation
 *   - a slow vertical drift, ±36px across the whole act, so it reads as
 *     alive rather than pinned, and a short fade-and-rise on entry
 * Transform-only, matching the composited-not-layout technique the rest
 * of this page's choreography already uses. No drop-shadow: with the
 * background gone there is no flat edge to cast one, and it read as a
 * stray dark smear. */
.benefits-media {
  --bp: 0;                                        /* 0..1 across this act */
  --enter: min(1, calc(var(--bp) / 0.12));
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(300px, 24%);
  height: auto;
  aspect-ratio: 592 / 1120;                       /* the cropped frame */
  object-fit: contain;
  opacity: var(--enter);
  transform:
    translate(-50%, -50%)
    translateY(calc(36px - 72px * var(--bp)))
    translateY(calc(20px * (1 - var(--enter))));
}
@media (prefers-reduced-motion: reduce) {
  .benefits-media { opacity: 1; transform: translate(-50%, -50%); }
}

/* The clip was keyed against the dark ground and holds there: every layer in
 * the exploded stack is either navy or near-black except the second one, which
 * is a white phone shell, and against #020b16 that one reads as the brightest
 * plate in the stack.
 *
 * On the light ground it is the same white as the page. Sampled off the keyed
 * frames composited on --abyss #F8FAFC, the shell's outer edge runs 250-253
 * against a 248-252 background, so the silhouette simply stops existing along
 * its top-right run -- the stack loses the plate that gives it its depth, and
 * what is left reads as a badly cut-out image rather than as separated layers.
 *
 * The alpha channel is fine; what is missing is anything BEHIND the shape. So
 * light mode gives it what the dark ground was giving it for free: a 1px rim
 * that restates the silhouette wherever the fill can't, and a soft contact
 * shadow underneath so the stack sits on the page instead of floating in it.
 * drop-shadow (not box-shadow) because it follows the clip's own alpha, which
 * is what the poster frame and every scrubbed frame actually are.
 *
 * Light only: on the dark ground the same rim would be an invisible smudge and
 * the ambient shadow would darken navy on navy for no gain. */
:root[data-theme="light"] .benefits-media {
  filter:
    drop-shadow(0 0 1px rgba(15, 23, 42, 0.45))
    drop-shadow(0 14px 22px rgba(15, 23, 42, 0.22));
}

.benefits li > span {
  font-family: 'Archivo Black', var(--font-display), sans-serif;
  font-weight: 400;              /* the face is already black; 700 would smear */
  font-size: clamp(1.35rem, 3.1vw, 2.6rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  color: var(--ink);
  text-wrap: balance;
  display: inline-block;
  text-shadow: 0 6px 30px rgba(var(--ground-rgb), 0.85);
}
.benefits .ar {
  font-family: var(--font-ar);
  direction: rtl;
  text-transform: none;
  line-height: 1.4;
  display: inline-block;
}
.benefits li > span > .ar { font-size: 0.92em; }
.benefits .lat {
  font-family: 'Archivo Black', var(--font-display), sans-serif;
  direction: ltr;
  display: inline-block;
  text-transform: uppercase;
}

/* --- act 4: galaxy + stats ---------------------------------------------- */

/* 235vh -> 165vh, same reasoning as .act--story above: the stat count-up
   and kicker reveal are threshold-on-progress, not vh-anchored, so this
   rescales cleanly. Nothing follows it with a negative margin-top (the
   .block sections after it use plain flow), so shortening it has no
   overlap math to re-tune. */
.act--galaxy { height: 165vh; }
.reveal-kicker {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: clamp(4rem, 18vw, 15rem);
  line-height: 0.82;
  margin: 0;
  color: var(--cream);
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}
.is-in .reveal-kicker { opacity: 1; transform: none; }
.reveal-kicker em { font-style: normal; color: var(--accent); }

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: clamp(1rem, 3vw, 2rem);
  margin-top: clamp(2rem, 5vw, 3.4rem);
  border-top: 1px solid var(--hair);
  padding-top: clamp(1.5rem, 4vw, 2.4rem);
}
.stats div { text-align: center; }
.stats .n {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 6vw, 4.4rem);
  line-height: 1;
  color: var(--cream);
}
.stats .l {
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--cream-dim);
  margin-top: 0.45rem;
}

/* --- blocks (how / pricing / auth) -------------------------------------- */

.block {
  position: relative;
  padding: clamp(4rem, 11vw, 9rem) clamp(1rem, 5vw, 3rem);
  background: linear-gradient(180deg, rgba(var(--ground-rgb), 0) 0%, rgba(var(--ground-rgb), 0.86) 12%, var(--abyss) 100%);
}
.block__inner { width: min(1120px, 100%); margin: 0 auto; text-align: center; }

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}
.reveal.is-in { opacity: 1; transform: none; }

.prices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: clamp(1rem, 2.5vw, 1.5rem);
  margin-top: clamp(2rem, 5vw, 3.4rem);
  text-align: left;
}
.price {
  border: 1px solid var(--hair);
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.04);
  padding: clamp(1.5rem, 3vw, 2.1rem);
  display: flex;
  flex-direction: column;
  position: relative;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), border-color 0.25s var(--ease);
}
.price:hover { transform: translateY(-3px); box-shadow: 0 24px 44px -28px rgba(0, 0, 0, 0.55); }
/* Permanent, not just on hover -- "Croissance" is the recommended tier and
   needs to read as such at rest, not only when someone happens to point at
   it. Kept in the card's own blue family (--info) rather than introducing
   the blood-red accent where the page has never used it as emphasis. */
.price.is-featured {
  border-color: rgba(22, 58, 100, 0.75);
  background: rgba(148, 163, 184, 0.08);
  box-shadow: 0 0 0 1px rgba(29, 78, 216, 0.16), 0 28px 52px -30px rgba(2, 11, 22, 0.7);
}
.price.is-featured:hover {
  box-shadow: 0 0 0 1px rgba(29, 78, 216, 0.24), 0 32px 58px -30px rgba(2, 11, 22, 0.75);
}
.price__badge {
  position: absolute;
  top: -10px; left: clamp(1.5rem, 3vw, 2.1rem);
  background: var(--cta);
  color: var(--on-cta);
  font: 700 10px/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
}
.price__name {
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: 1.9rem;
  line-height: 1;
  color: var(--cream);
}
.price__tag { color: var(--cream-dim); font-size: 0.9rem; margin: 0.5rem 0 1.2rem; line-height: 1.5; }
.price__amount { display: flex; align-items: baseline; gap: 0.4rem; margin-bottom: 0.55rem; }
.price__num { font-family: var(--font-display); font-size: 3.4rem; line-height: 1; color: var(--cream); }
.price__unit { font: 500 12px/1 var(--font-mono); color: var(--cream-dim); text-transform: uppercase; letter-spacing: 0.08em; }
/* align-content:start so the rows keep their 0.6rem rhythm instead of
   stretching to fill the card. The three tiers no longer carry the same
   number of bullets, and a grid that grows spreads the shortest list into
   something that reads as a different, looser design. */
.price ul { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: 0.6rem; flex: 1; align-content: start; }
.price li { font-size: 0.92rem; color: var(--cream-soft); padding-left: 1.4rem; position: relative; line-height: 1.45; }
.price li::before { content: '+'; position: absolute; left: 0; color: var(--accent); font-family: var(--font-mono); }
.price li.incl { color: var(--cream); font-weight: 600; padding-left: 0; }
.price li.incl::before { content: ''; }
.price .btn { justify-content: center; }

/* "Sur devis" is words, not a figure: at the numeral size it overflows the card. */
.price__num--custom {
  font-size: clamp(1.9rem, 3vw, 2.4rem);
  letter-spacing: -0.005em;
}
.price__foot { margin-top: clamp(1.2rem, 3vw, 1.8rem); color: var(--cream-dim); font-size: 0.86rem; }
.price__foot a { color: var(--accent-warm); }

/* The technical fee is charged once per brand and is identical on the three
   tiers. It appears twice on purpose: a short line inside each card so the
   monthly figure is never read alone, and the band below, which is where the
   fee is actually justified. */
.price__setup {
  margin: 0 0 1.3rem;
  font: 500 11px/1.4 var(--font-mono);
  letter-spacing: 0.03em;
  color: var(--cream-dim);
  /* One line at every card width the grid produces; `balance` is the fallback
     for the narrow end, where an unbalanced wrap orphans the last word. */
  text-wrap: balance;
}
.setup {
  margin-top: clamp(1rem, 2.5vw, 1.5rem);
  border: 1px solid var(--hair);
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.04);
  padding: clamp(1.4rem, 3vw, 1.9rem) clamp(1.5rem, 3vw, 2.1rem);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(0.9rem, 3vw, 2.2rem);
  text-align: left;
}
.setup__price { display: flex; align-items: baseline; gap: 0.4rem; flex: none; }
.setup__num {
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 4vw, 3.4rem);
  line-height: 1;
  color: var(--cream);
}
.setup__unit {
  font: 500 12px/1 var(--font-mono);
  color: var(--cream-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* Roadmap. Deliberately quieter than a price card: it must read as
   "coming", never as part of what the tiers include. */
.roadmap { margin-top: clamp(1rem, 2.5vw, 1.5rem); text-align: left; }
.roadmap__title {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cream-dim);
  margin: 0 0 0.9rem;
}
.roadmap__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.5rem;
}
.roadmap__list li {
  font-size: 0.84rem;
  color: var(--cream-soft);
  border: 1px dashed var(--hair);
  border-radius: 999px;
  padding: 0.4rem 0.8rem;
}

.setup__body { flex: 1 1 320px; }
.setup__title { color: var(--cream); font-weight: 600; font-size: 0.98rem; margin: 0 0 0.4rem; }
.setup__text { color: var(--cream-soft); font-size: 0.9rem; line-height: 1.55; margin: 0; }

/* The actual conversion point of the page -- the one card that should draw
   the eye at rest, not just on hover, this far down. */
.auth {
  max-width: 520px;
  margin: clamp(2rem, 5vw, 3rem) auto 0;
  border: 1px solid var(--hair);
  border-radius: 22px;
  background: var(--veil);
  backdrop-filter: blur(18px);
  padding: clamp(1.6rem, 4vw, 2.4rem);
  text-align: left;
  box-shadow: 0 0 0 1px rgba(231, 180, 180, 0.1), 0 32px 60px -30px rgba(2, 11, 22, 0.75);
}
.auth ul { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: 0.55rem; }
.auth li { font-size: 0.94rem; color: var(--cream-soft); display: flex; gap: 0.7rem; align-items: center; }
.auth li span { color: var(--accent-warm); font-weight: 700; }
.auth .btn { width: 100%; justify-content: center; }
.auth__fine { font-size: 0.78rem; color: var(--cream-dim); margin: 1rem 0 0; line-height: 1.55; }
.auth__fine a { color: var(--cream-soft); }

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

.footer {
  position: relative;
  z-index: 2;
  background: var(--abyss);
  border-top: 1px solid var(--hair-soft);
  padding: clamp(2.5rem, 6vw, 4rem) clamp(1rem, 5vw, 3rem);
  color: var(--cream-dim);
  font-size: 0.85rem;
}
.footer__inner {
  width: min(1120px, 100%);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
  align-items: center;
}
.footer a { color: var(--cream-soft); text-decoration: none; }
.footer a:hover { color: var(--accent-warm); }
.footer nav { display: flex; gap: 1.2rem; flex-wrap: wrap; }

/* --- hero lede / CTA / photos -------------------------------------------- *
 * Lives INSIDE #heroBeat1, not after the hero act. The WebGL canvas is
 * position:fixed and its director() choreography spans all four .act
 * sections (hero through galaxy) -- anything placed in normal document flow
 * after the hero, even right after it, still scrolls through 3D models still
 * in flight (confirmed: store icons and phone geometry bled through). Only
 * two placements are actually safe: after the LAST act (galaxy, before the
 * static .block sections -- far below the fold), or inside the hero's own
 * pinned, always-on-load beat -- which is what this is, so the photos are
 * visible immediately with the title, no scroll required. z-index rides on
 * main's stacking context (2), already above the canvas (1). */
/* Both are above the fold, visible at load -- so their entrance is a plain
   autoplaying CSS animation, not .reveal/IntersectionObserver. Confirmed by
   dumping the DOM after a real render: .hero-lede got its .is-in, the CTA
   right next to it never did, despite both using the exact same .reveal +
   data-once wiring. Rather than chase why one of two visually identical
   siblings fails to intersect, above-the-fold content simply should not
   depend on a script/observer firing correctly at all -- same lesson as the
   funnel reveal fixed earlier: guaranteed to show, nothing to race. */
.hero-lede {
  max-width: 38ch;
  margin: clamp(0.75rem, 1.8vw, 1rem) auto 0;
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  line-height: 1.55;
  color: var(--cream-soft);
  text-align: center;
  /* No `opacity: 0` base declaration, deliberately. The animation's `both` fill
     already holds the element at the keyframe's `from` (opacity 0) through the
     delay, so the entrance looks exactly the same -- but the element's own
     resting opacity is now 1. Declared as 0, anything that stops the animation
     from running leaves this line permanently invisible, and it is above the
     fold. Same lesson as the .is-in/IntersectionObserver note above: content
     this high on the page must not be able to end up hidden. */
  animation: heroFurnitureIn 0.7s var(--ease) 0.32s both;
}
.hero-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.9rem;
  /* Wider than the lede's own clamp above it: the badges + welcome block
     now sit between the two, and the CTAs read as the stack's final,
     decisive beat -- they earn a touch more air than a mid-stack gap. */
  margin: clamp(1rem, 2vw, 1.25rem) auto 0;
}
/* Same reasoning as .hero-lede above: the `both` fill provides the invisible
   start, so an `opacity: 0` base would only add a way for the two CTAs to end
   up permanently invisible. */
.hero-cta, .hero-cta-secondary {
  animation: heroFurnitureIn 0.7s var(--ease) 0.4s both;
}
.hero-cta-secondary { animation-delay: 0.5s; }
@keyframes heroFurnitureIn {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-lede, .hero-cta, .hero-cta-secondary { animation: none; opacity: 1; }
}
/* Guaranteed-visible animation, not .is-in/IntersectionObserver -- same
   reasoning as .hero-lede/.hero-cta above: this is above the fold, and IO
   is not reliably firing on above-the-fold elements at load (confirmed on
   the CTA button earlier). */
.hero-photos {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: clamp(120px, 20vw, 190px);
  margin-top: clamp(1.4rem, 3.5vw, 2rem);
  opacity: 0;
  animation: heroFurnitureIn 0.7s var(--ease) 0.5s both;
}
@media (prefers-reduced-motion: reduce) {
  .hero-photos { animation: none; opacity: 1; }
}
.hero-photos img { display: block; }
.hero-photos .v-founder {
  width: min(150px, 40vw);
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 20px 26px rgba(2, 11, 22, 0.4));
  transform: rotate(-3deg);
}
.hero-photos .v-icon {
  width: min(96px, 26vw);
  position: absolute;
  right: 30%;
  bottom: 0;
  z-index: 2;
  filter: drop-shadow(0 14px 18px rgba(2, 11, 22, 0.35));
  transform: rotate(4deg);
}
@media (min-width: 640px) {
  .hero-photos .v-founder { width: 190px; }
  .hero-photos .v-icon { width: 120px; right: calc(50% - 150px); }
}

/* --- loader ------------------------------------------------------------- */

.loader {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--abyss);
  display: grid;
  justify-items: center;
  /* place-content, not place-items. Auto grid rows STRETCH by default, so
     `place-items: center` centred each child inside a row a third of the
     viewport tall and spread the three of them from top to bottom -- the
     1.2rem gap never applied to anything. Centring the content box instead
     makes them one lockup, which is what the gap was written for. */
  place-content: center;
  gap: 1.1rem;
  transition: opacity 0.7s var(--ease), visibility 0.7s;
}
/* pointer-events goes with the fade, not with the visibility. visibility is a
   discrete property: it flips at the END of the 0.7s transition, so between
   the curtain going transparent and it actually going away this fixed,
   full-viewport layer was still swallowing every click on the page. */
.loader.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
.loader__mark { font-family: var(--font-logo); font-size: 3rem; color: var(--cream); }
.loader__bar { width: min(240px, 60vw); height: 2px; background: var(--hair); overflow: hidden; }
/* No width transition: neptune.js eases the value itself on rAF, and a CSS
   transition on top of that lagged the number beside it by a third of a
   second -- the bar visibly trailed its own readout. */
.loader__bar i { display: block; height: 100%; width: 0; background: var(--info); }
.loader__pct {
  margin: 0;
  font: 700 12px/1 var(--font-mono);
  letter-spacing: 0.14em;
  color: var(--cream-soft);
  /* Tabular figures so the digits do not shuffle the line as it counts up. */
  font-variant-numeric: tabular-nums;
}

/* --- titles: glossy dark blood red -------------------------------------- *
 * One treatment for every title on the page. Two stacked backgrounds clipped
 * to the glyphs: a vertical gloss ramp (highlight -> blood -> near-black) and
 * a specular band that sweeps across on a slow loop.
 * ------------------------------------------------------------------------ */

/* .display--clay is excluded: it carries the putty treatment, not the red one. */
.hook,
.display:not(.display--clay),
.h2,
.reveal-kicker,
.price__name {
  background-image:
    linear-gradient(100deg,
      transparent 34%,
      rgba(255, 196, 186, 0.50) 47%,
      transparent 60%),
    linear-gradient(180deg,
      var(--blood-hi) 0%,
      var(--blood-mid) 40%,
      var(--blood) 74%,
      var(--blood-deep) 100%);
  background-size: 240% 100%, 100% 100%;
  background-position: 160% 0, 0 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  text-shadow: none;
  /* text-shadow does not paint through a transparent fill; drop-shadow does. */
  filter:
    drop-shadow(0 1px 0 rgba(255, 255, 255, 0.08))
    drop-shadow(0 0 28px rgba(184, 31, 31, 0.40));
  animation: sheen 7s ease-in-out infinite;
}

@keyframes sheen {
  0%        { background-position: 160% 0, 0 0; }
  55%, 100% { background-position: -60% 0, 0 0; }
}

/* The hook runs at body size, where the full ramp's near-black end would be
   illegible. Same family, but the gradient stops at the mid tone and the glow
   is dialled back to suit small glyphs. */
.hook {
  background-image:
    linear-gradient(100deg,
      transparent 34%,
      rgba(255, 214, 206, 0.55) 47%,
      transparent 60%),
    linear-gradient(180deg,
      var(--blood-hi) 0%,
      var(--blood-mid) 62%,
      var(--blood) 100%);
  filter: drop-shadow(0 0 14px rgba(196, 40, 40, 0.35));
}

/* --- light theme: no gloss on the titles --------------------------------- *
 * The treatment above is three stacked effects, and all three were authored to
 * read against #020b16:
 *
 *   the specular band   a light pink diagonal clipped to the glyphs
 *   `sheen`             which sweeps that band across on a 7s loop
 *   the outer glow      drop-shadow(0 0 28px rgba(184,31,31,.40))
 *
 * On paper the glow is the worst of them -- a red halo bleeding off every
 * title, which reads as a smudge rather than as light -- and the sweep is a
 * highlight travelling over a surface that is already brighter than it is.
 *
 * So light mode drops the whole gradient machinery and paints the title in the
 * design system's --accent-red (#701111 via --blood-hi, 11.29:1). Flat, no
 * clip, no filter, no animation.
 *
 * Two things fall out of this: the 721 style recalculations per 6s scroll that
 * the permanent `sheen` animation cost are gone in this theme, and `em` needs
 * its own colour again -- it used --blood-hi to stand out against the darker
 * end of the ramp, and with the ramp gone it would match the title exactly.
 * ------------------------------------------------------------------------ */
:root[data-theme="light"] .hook,
:root[data-theme="light"] .display:not(.display--clay),
:root[data-theme="light"] .h2,
:root[data-theme="light"] .reveal-kicker,
:root[data-theme="light"] .price__name {
  background-image: none;
  /* The dark theme's `-webkit-background-clip: text` is never reset here --
     background-image:none leaves it clipping to nothing. Chromium falls
     back to -webkit-text-fill-color regardless; Safari does not reliably,
     and paints the glyphs invisible instead of falling back. Confirmed live
     on Safari with opacity hard-set to 1 (no transition/timing involved at
     all): the text still never appeared. Releasing the clip here is the
     actual fix. */
  -webkit-background-clip: initial;
  background-clip: initial;
  color: var(--blood-hi);
  -webkit-text-fill-color: var(--blood-hi);
  filter: none;
  animation: none;
}

/* --blood, the lighter stop, so emphasis still separates from the title.
   6.91:1 on the page. */
:root[data-theme="light"] .h2 em,
:root[data-theme="light"] .reveal-kicker em {
  color: var(--blood);
  -webkit-text-fill-color: var(--blood);
}

.is-still .hook,
.is-still .display:not(.display--clay),
.is-still .h2,
.is-still .reveal-kicker,
.is-still .price__name { animation: none; }

/* Emphasis inside a title keeps the bright highlight rather than the cyan it
   used to carry, so it still reads as emphasis against the darker ramp. */
.h2 em,
.reveal-kicker em {
  color: var(--blood-hi);
  -webkit-text-fill-color: var(--blood-hi);
}

/* Raven's contrast audit flags the hook and .hero-lede "indeterminate" at
   every width, desktop's full-bleed cover video included: the real backdrop
   is the hero video, not a flat colour, so no fixed text colour is
   guaranteed to hold -- a dark frame washes out the oxblood gradient-clip,
   a bright one (the phone screen, a lamp) can wash out pale text too.
   Neither theme's usual title treatment was designed for a photographic
   background, so the hook drops the gradient-clip entirely (it has no
   single colour to reason about) for solid white plus a real shadow,
   matching design-system.md's own rule for text-on-image: a reliable solid
   treatment, never a fragile gradient. Both theme branches are overridden
   explicitly (:not([data-theme="light"]) covers dark-explicit and
   no-attribute/system-dark) so this wins the cascade against the shared,
   higher-specificity light-theme rule above regardless of which theme is
   active -- the video underneath does not change with it. */
:root[data-theme="light"] .hook,
:root[data-theme="light"] .hook--sub,
:root:not([data-theme="light"]) .hook,
:root:not([data-theme="light"]) .hook--sub {
  background-image: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  color: #FFFFFF;
  -webkit-text-fill-color: #FFFFFF;
  filter: none;
  animation: none;
  /* The shadow is load-bearing again. The copy is centred over the full-bleed
     clip, so behind any given glyph there may be the dark studio OR one of the
     two pale phone screens, and no single scrim value covers both without
     flattening the product. The shadow is what holds the type together across
     that boundary -- it is the design-system rule for text on image. */
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.6), 0 1px 3px rgba(0, 0, 0, 0.5);
}
.hero-lede {
  color: #F1F5F9;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* The rule above is right about the dark theme and wrong about the light one,
   and the reason is a rule this stylesheet writes itself. On light,
   .hero-video-scrim washes the very same video with 55% to 75% WHITE, so the
   backdrop is no longer unknowable: the page guarantees it is pale. Sampling
   the rendered pixels around "avec votre marque." on a light-theme render gives
   rgb(171,170,171), and white type on that measures 2.32:1 against the 3:1 this
   size needs -- the drop shadow was carrying the whole thing.
   Same solid-plus-shadow treatment, inverted: the page's ink, haloed in the
   page's ground instead of in black. Measured against that same backdrop this
   is about 8:1. Source order, not specificity, is what makes this win -- it
   matches the light branch above exactly. */
/* The light theme keeps its own ink rather than the white above, and this is
   the fix that mattered: the light scrim washes the clip PALE, so white copy on
   it measured 2.32:1 on the rendered pixels against the 3:1 that size needs.
   Near-black on the same ground is about 9:1 at worst -- over the studio grey,
   over a pale phone screen, or over the coral bloom at the end of the clip.
   The halo is in the page's ground colour, mirroring the dark theme's black
   one, and does the same job where the clip darkens behind a glyph. */
:root[data-theme="light"] .hook,
:root[data-theme="light"] .hook--sub,
:root[data-theme="light"] .hero-lede {
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
  text-shadow: 0 1px 12px rgba(248, 250, 252, 0.92), 0 1px 3px rgba(248, 250, 252, 0.85);
}

/* --- the hook's own emphasis: "next level" ------------------------------- *
 * The rule right above forces the whole hook to solid white, and the reason it
 * gives applies here too: the backdrop is the hero video, so nothing about the
 * ground is knowable. A gradient-clipped run was tried first (ember to gold)
 * and failed exactly as that comment predicts -- League Gothic is condensed, so
 * each stroke is narrow enough that a vertical ramp reads as dirt rather than
 * as a ramp, and the pale end of it landed on the poster's own light grey wall.
 *
 * So the glyphs keep the white the rest of the hook has, and the colour goes
 * where it can be solid: a warm halo around the run (which tints it without
 * touching its fill) and an amber rule beneath it that draws itself in on a
 * slight upward slope. The slope is the point -- it is the literal shape of
 * "next level", and it is the only motion in the hero copy.
 *
 * text-shadow is inherited from .hook and does the legibility work; the filter
 * only adds the halo. The animation lives on the pseudo-element because .hook
 * itself is pinned to `animation: none` by the rule above.
 * ------------------------------------------------------------------------- */
.hook-level {
  font-style: normal;
  position: relative;
  display: inline-block;
  padding-bottom: 0.06em;
  filter: drop-shadow(0 0 22px rgba(247, 166, 79, 0.55));
}
.hook-level::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0.085em;
  border-radius: 999px;
  /* Ends on the page's cream rather than on a third yellow (#FFD166). The warm
     coral start is kept: it is --blood-hi, the display face's own colour. */
  background: linear-gradient(90deg, #F4685A 0%, #F7A64F 40%, #FDFBE3 100%);
  box-shadow: 0 2px 14px rgba(247, 166, 79, 0.45);
  transform: scaleX(0) rotate(-2.2deg);
  transform-origin: 0 100%;
  animation: levelRise 1s 0.45s var(--ease) forwards;
}
@keyframes levelRise { to { transform: scaleX(1) rotate(-2.2deg); } }

@media (prefers-reduced-motion: reduce) {
  .hook, .display, .h2, .reveal-kicker, .price__name { animation: none; }
  /* Drawn, not drawing: the rule is part of the composition, the sweep is not. */
  .hook-level::after { animation: none; transform: scaleX(1) rotate(-2.2deg); }
  .marquee__track { animation: none; }
  .scroll-cue__arrow { animation: none; }
  .hook, .lede, .benefits li, .reveal, .reveal-kicker { opacity: 1; transform: none; filter: none; }
  .display .w i { transform: none; }
  /* Nothing here for the story act: its cards carry no transform, transition or
     animation. They are `position: sticky`, which is layout rather than motion,
     and a reader who asked for less movement still wants them where the design
     puts them. */
}

@media (max-width: 700px) {
  /* 190vh belonged to a hero that scrolled through a crossfade -- hook
     fading, "Bienvenue chez Neptune" arriving. Mobile no longer does that
     (hook, lede, CTA and welcome are all permanently on screen together),
     so that long a pin just meant a long, empty-feeling scroll before the
     story act even started. Cut to 150vh rather than closer to the 100vh
     floor: .act--story's -50vh overlap below assumes a release phase to
     pull up INTO, and 150vh keeps 50vh of pin travel for that to still
     land against -- shrinking further without re-measuring that overlap
     risked the two acts colliding instead of handing off cleanly. */
  .act--hero { height: auto; }   /* same reasoning as the desktop rule */
  /* The narrative was the least readable thing on a phone, and the media
     query was the cause: 235vh left the pin 1139 px of travel at 390x844,
     so each of the four lines held for 285 px against 405 px on desktop.
     Less travel for text this query simultaneously enlarges from 4.4vw to
     9.5vw. 300vh puts a line at 422 px, a touch more generous than desktop,
     which is the right way round. The benefits track keeps its -42vh, which
     is measured from the end of this one, so the hand-off is unchanged. */
  /* 300vh -> 205vh kept. -50vh overlap did NOT survive the hero rewrite
     above, though: it assumed a ~100svh hero pin (stuck budget = act
     height - pin height ~= 422px, matching the old -50vh almost exactly).
     The hero pin is now content-sized (~646px, not ~844px), so its stuck
     budget grew to ~620px -- the story's pull-up was landing while the
     CTAs were still fully stuck on screen, printing "Un site, c'est une
     visite." directly over "CRÉER MON APP". Being re-measured below. */
  /* No scroll track and no pull-up here either: both belonged to the pinned
     version of the story act, and this one is in normal flow at every width.
     Its height is whatever the hero card and the stack need. */
  .act--benefits { margin-top: 0; }
  /* 185vh -> 130vh, same reasoning as the desktop galaxy cut above. */
  .act--galaxy { height: 130vh; }

  /* Every gap in this stack is squeezed to its smallest comfortable value --
     the hook, badges, lede, welcome block, CTAs and "Défilez" all need to
     clear the fold together on load, and the phone is the tightest budget
     of any width this stack renders at. */
  .hero-welcome-mobile {
    margin-top: clamp(0.75rem, 4vh, 1.5rem);
  }
  .hero-lede { margin-top: 0.6rem; }
  .hero-actions { margin-top: clamp(0.75rem, 3vh, 1.5rem); }
  /* The desktop button was sized for a hero with far more room; here it
     was reading larger than the headline hierarchy it sits under. */
  .hero-actions .btn {
    padding: 0.8rem 1.4rem;
    font-size: 11.5px;
  }
  .hero-stores {
    width: 200px;
    height: auto;
    margin: clamp(0.5rem, 2vh, 0.9rem) auto 0;
  }
  /* The CTA's 44px touch target sets the bar's content height, and the bar's
     scrim runs to 210% of that height -- left at the desktop padding the veil
     reached y=183 and put the first benefit card back under it. This keeps the
     bar at the ~55px everything below was measured against. */
  .topbar { padding: 0.35rem 1rem; gap: 0.5rem; }
  /* "App Manager" stays on phones; the lockup is scaled to make room. Now
     viewport-relative rather than a flat 1.35rem: the three-across bar needs
     the wordmark under (width - padding - gaps - "Tarifs") / 2, which is 144px
     at 390 but only 113px at 320. A fixed size fitted the first and wrapped on
     the second. */
  .wordmark { font-size: clamp(0.95rem, 4.6vw, 1.35rem); }
  .wordmark span { font-size: 0.6em; margin-left: 0.3rem; }
  .topnav { gap: 0.5rem; }
  .topnav a { font-size: 11px; letter-spacing: 0.06em; }
  .topcta { font-size: 11px; letter-spacing: 0.07em; }

  /* Phone hero: the copy is the layout. The desktop clamp bottoms out at 2rem
     here, which left the hook floating in the middle of an empty screen, so the
     size is driven off the viewport and the block runs to the edges. Kept
     symmetric -- centred in the pin, both lines centred -- so no line hangs off
     into the 3D objects sitting above and below it. */
  .act__pin {
    padding: calc(var(--bar-h) + 2.6rem) 0.85rem 2.5rem;
  }
  /* padding-top raised past the shared 2.6rem (and not left to fall through
     to the desktop hero rule's 4.5rem, which would otherwise win here on
     specificity alone) -- the marquee + topbar are both position:fixed
     overlays, not in flow, and measured, .topbar's own bottom edge sits at
     ~89px while 2.6rem here only cleared --bar-h + 41.6px = ~76px: the
     hook's first line really was rendering behind the bar, not just close
     to it. 4rem clears the measured 89px with a real margin. Bottom is
     safe to shave: "Défilez" just below the pin already carries its own
     spacing, and the badges above (order:2) do the same job at the top. */
  /* 6.6rem, not the 4rem that clears the bar itself: .topbar::before fades
     the page ground out over 210% of the bar's height, down to ~150px at
     390 wide. At 4rem the hook's first line (98-134px) sat inside that fade
     and read washed-out in both themes; 6.6rem starts it at ~140px, where
     the fade has all but ended. */
  .act--hero .act__pin {
    padding-top: calc(var(--bar-h) + 6.6rem);
    padding-bottom: 1.25rem;
  }
  /* The extra top padding this used to carry (13rem) was calibrated to let
     the old 3D hero devices enter the frame before the text did ("devices
     don't enter until y~590"). There is no 3D scene left to clear, so the
     hero now falls back to the same near-top padding every other mobile
     act uses -- text starts right under the bar, not a third of the
     screen down. */

  /* Sized down from the old 3D-hero scale (up to 5rem/4.2rem) -- that was
     tuned to hold its own against full-bleed 3D devices. Against a
     letterboxed, contained video the same size read oversized and lopsided;
     this keeps the hook the clear focal point without dwarfing everything
     under it. */
  .hook {
    /* A phone cannot hold this line in one piece in Archivo Black at any size
       worth setting it at -- 20 characters at 0.52em need 10.4em of width, and
       a 393px screen has about 8em of it. So the wrap is accepted and the size
       chosen for the two-line shape instead: big enough to carry the hero,
       small enough that two lines plus the sub line still leave the phones
       visible under it. */
    font-size: clamp(1.9rem, 8.2vw, 2.7rem);
    line-height: 1.08;
    max-width: none;
    margin: 0 auto;
    text-align: center;
    text-wrap: balance;
  }
  .hook--sub {
    font-size: clamp(1.6rem, 6.8vw, 2.3rem);
    margin-top: 0.3rem;
    text-align: center;
  }
  /* Beat 2 carries the hook's scale. The 3D wordmark measures this element's
     box, so sizing it here scales the extruded Neptune to match for free. */
  .display--intro {
    font-size: clamp(2.4rem, 13.5vw, 5rem);
    line-height: 1.2;
  }
  .display--clay {
    font-size: clamp(3.2rem, 17vw, 6.5rem);
  }

  /* 80px of clearance is a desktop measure; on a phone it is a tenth of the
     screen spent above the card. The card height and the gap are both derived
     from these two, so the "one card at a time" rule holds here unchanged. */
  .act--story {
    --sfc-card-h: min(340px, calc(100svh - 150px));
    --sfc-top: max(48px, calc((100svh - var(--sfc-card-h)) / 2));
    --sfc-dwell: 20px;
  }
  .sfc-card {
    border-radius: 22px;
    padding: clamp(1.4rem, 6vw, 2rem);
    gap: 0.6rem;
  }
  .sfc-card__title { font-size: clamp(1.6rem, 7.2vw, 2.4rem); }

  /* Section headings */
  /* "FONCTIONNEMENT" is 14 characters; at 20px Archivo Black the old 0.2em
     tracking pushed it past a 390px column. */
  .eyebrow { letter-spacing: 0.06em; }
  .h2 { font-size: clamp(2.6rem, 12vw, 5rem); }
  .reveal-kicker { font-size: clamp(4.5rem, 21vw, 10rem); }

  /* Blocks: give the copy the width back, then scale it up. */
  .block { padding-inline: 0.9rem; }

  /* Phones: the scatter would collide, so stagger them down the column and let
     each one run the full width. */
  /* The four cards ran to y=712 and the shop sits at y~607-704, so the last
     one ("meilleure scalabilite pour l'avenir") was printed straight over it.
     The list is shorter and anchored to the top of the pin instead of centred
     in it, which frees the bottom-right corner for the shop without moving the
     first three cards. */
  .act--benefits .act__pin {
    align-items: start;
    /* The column is top-anchored, so the pin's usual 3rem foot is dead space
       under the phone -- and it is space the taller flow column needs back on
       a short screen. */
    padding-bottom: 1rem;
    /* Clear of the topbar's scrim, which runs to 210% of the bar height (71px
       at --bar-h 34) and greyed out the first card once the list stopped being
       centred. Trimmed from 5.6rem: the column now carries the phone as its
       last item rather than hanging it off the list's bottom, so it is taller
       than it was and the head-room is worth more than the extra inset. */
    padding-top: calc(var(--bar-h) + 4.2rem);
  }
  /* The shop's top edge lands at a fixed 0.716 of the viewport height on
     narrow screens (it is scaled off the frustum, so the fraction holds at
     every size). The list is capped against that rather than at a flat vh, so
     short phones give the band back instead of running into it. */
  /* The scatter is absolute, and absolute positions cannot know how tall the
     phrase above them turned out to be. On desktop that is safe -- each corner
     is its own column. On a phone every phrase runs the full width, so the four
     slots are really one column at fixed offsets (0 / 25% / 50% / bottom), and
     the moment a phrase needs one more line than the slot budgeted, it prints
     straight through its neighbour. "Plus de conversions grâce aux
     notifications push" is four lines of Archivo Black at 414px, and it landed
     on top of "Une meilleure relation avec vos clients".
     So mobile stops positioning them at all: one flow column, real gaps, and
     the alternating left/right alignment kept so it still reads as the same
     staggered composition. Line counts can then change with the copy, the
     width or the font without anything colliding, which no set of fixed
     offsets can promise. */
  .benefits {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: clamp(0.9rem, 3.4vw, 1.5rem);
    height: auto;
  }
  .benefits li {
    position: static;
    max-width: 92%;
    top: auto; right: auto; bottom: auto; left: auto;
  }
  .benefits li:nth-child(odd)  { align-self: flex-start; text-align: left; }
  .benefits li:nth-child(even) { align-self: flex-end;   text-align: right; }
  /* 7.4vw put "plus de conversions" at four lines on its own. This is the
     smallest step down that gets it to three at 390-430px while still reading
     as the act's display type. */
  .benefits li > span { font-size: clamp(1.5rem, 6.6vw, 2.4rem); }
  /* No centre channel to stand in here: at 92% max-width every phrase runs
     nearly the full 362px box (measured spans 14-347, 43-376, 14-347,
     43-376 -- practically edge to edge), and the vertical gaps between
     them are 47/48/94px, far too short for a phone. The room that does
     exist is BELOW the stack: the box ends at y=562 and the pin's content
     runs to ~804, so ~242px of genuinely empty space. The phone anchors
     there at full size -- claims stacked above it, product under them --
     rather than being shrunk to fit between two lines. Same slow drift as
     desktop, kept entirely positive so it never rises into li[4]. */
  /* Was absolute at top:100% of the list, i.e. anchored to a box whose height
     the list itself no longer fixes. Now the last item in the same column, so
     it sits under whatever the phrases actually needed. The -50% x-translate
     goes with the absolute anchor it was correcting for. */
  .benefits-media {
    position: static;
    align-self: center;
    top: auto;
    left: auto;
    width: min(124px, 34%);
    margin-top: clamp(0.8rem, 3.5vw, 1.4rem);
    transform:
      translateY(calc(16px - 12px * var(--bp)))
      translateY(calc(20px * (1 - var(--enter))));
  }
  @media (prefers-reduced-motion: reduce) {
    .benefits-media { opacity: 1; transform: translateY(16px); }
  }

  .stats { grid-template-columns: repeat(2, 1fr); }
  .stats .n { font-size: clamp(3rem, 15vw, 5rem); }
  .stats .l { font-size: 0.9rem; }

  .price { padding: 1.6rem 1.3rem; }
  .price__name { font-size: 2.4rem; }
  .price__num { font-size: 4.2rem; }
  .price__tag { font-size: 1rem; }
  .price li { font-size: 1rem; }
  .price__setup { font-size: 12px; }

  .setup { padding: 1.6rem 1.3rem; }
  .setup__title { font-size: 1.05rem; }
  .setup__text { font-size: 1rem; }

  .auth { padding: 1.5rem 1.2rem; }
  .auth li { font-size: 1.02rem; }
}
