  /* ---------- Verlag webfont declarations ----------
     Local TTF files in /Fonts. Available weights: 200 (XLight), 300 (Light),
     900 (Black). Browsers pick the closest match for in-between weights
     (so font-weight: 600 → falls back to Black, font-weight: 400 → Light). */
  @font-face {
    font-family: 'Verlag';
    src: url('../Fonts/Verlag-XLight.ttf') format('truetype');
    font-weight: 200;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag';
    src: url('../Fonts/Verlag-Light.ttf') format('truetype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag';
    src: url('../Fonts/Verlag-LightItalic.ttf') format('truetype');
    font-weight: 300;
    font-style: italic;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag';
    src: url('../Fonts/Verlag-Black.ttf') format('truetype');
    font-weight: 900;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag';
    src: url('../Fonts/Verlag-BlackItalic.ttf') format('truetype');
    font-weight: 900;
    font-style: italic;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag Condensed';
    src: url('../Fonts/VerlagCondensed-XLight.ttf') format('truetype');
    font-weight: 200;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag Condensed';
    src: url('../Fonts/VerlagCondensed-Light.ttf') format('truetype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag Condensed';
    src: url('../Fonts/VerlagCondensed-Book.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Verlag Compressed';
    src: url('../Fonts/VerlagCompressed-Bold.ttf') format('truetype');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
  }

  /* ---------- Reset & base ---------- */
  *, *::before, *::after { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; height: 100%; }
  body {
    font-family: 'Verlag', 'Helvetica Neue', Arial, 'Segoe UI', sans-serif;
    background: #F1EEE8;
    color: #1a1a1a;
    overflow: hidden;
    -webkit-font-smoothing: antialiased;
    /* Default to Light (300) since only Light/Black are bundled —
       text that needs to be heavy explicitly sets font-weight: 900. */
    font-weight: 300;
  }

  :root {
    --cream: #F1EEE8;
    --cyan: #00ABED;
    --cyan-soft: #b8e5f7;
    --red: #ED0000;
    --gray-soft: #c7c7c7;
    --gray-text: #6e6e6e;
    --ink: #1a1a1a;
    --transition-slow: cubic-bezier(.65,.05,.36,1);
  }

  /* ---------- Cadre à ratio paysage clampé [4:3 → 19.5:9] ----------
     Toute l'expérience vit dans #frame, centré dans le viewport. Le ratio est
     clampé entre 4:3 (1.333) et 19.5:9 (2.167) :
       • viewport plus LARGE que 19.5:9 → cadre à 19.5:9, bandes G/D (pillarbox)
       • viewport plus ÉTROIT que 4:3   → cadre à 4:3, bandes haut/bas (letterbox)
       • dans la plage → le cadre remplit le viewport (borne 16:9 : aucune bande)
     container-type:size → les unités cqw/cqh/cqmin du layout sont relatives au
     CADRE (pas au viewport) ; à 16:9 cqw==vw, cqh==vh (borne inchangée). Les
     bandes prennent la couleur crème du body. Le rotate-overlay (portrait) reste
     hors du cadre, plein viewport. */
  #frame {
    position: fixed;
    inset: 0;
    margin: auto;                         /* centre le cadre */
    /* Taille = viewport, clampée [4:3 → 19.5:9]. dvw/dvh = viewport DYNAMIQUE
       (zone réellement visible, BARRE D'URL mobile déduite). vw/vh d'abord en
       REPLI pour les (rares) navigateurs sans dvh. Sans dvh, sur mobile 100vh =
       écran complet (barre d'URL incluse) → le cadre dépasse la zone visible et
       le haut/bas passe sous la barre d'URL. */
    width:  min(100vw, calc(100vh * 2.167));    /* repli (navigateurs sans dvh) */
    height: min(100vh, calc(100vw / 1.333));
    width:  min(100dvw, calc(100dvh * 2.167));  /* pas plus large que 19.5:9 */
    height: min(100dvh, calc(100dvw / 1.333));  /* pas plus haut que 4:3 */
    container-type: size;                 /* active cqw/cqh/cqmin (relatifs au cadre) */
    overflow: hidden;
    background: var(--cream);
    /* Anti-flash d'intro : masqué au 1er paint, révélé par JS (.app-ready) une fois
       la mise en page stabilisée (container queries + init). Sinon on voit une frame
       où la roue + le panneau sont mal dimensionnés/placés (petit, en haut à gauche)
       avant que l'intro centre tout. Fond cream = body → révélation invisible (le
       1er élément visible, le logo central, ne pop qu'à +0,15 s). */
    opacity: 0;
  }
  #frame.app-ready { opacity: 1; }

  /* ---------- Layout ---------- */
  .stage {
    position: relative;
    width: 100%;                          /* remplit le cadre (cq context) */
    height: 100%;
    display: flex;
    align-items: stretch;
    overflow: hidden;
  }

  /* Left side: wheel — initially fills the whole stage (100%), shrinks to
     50% once the intro cascade has settled (`.intro-done` class). The panel
     on the right slides in at the same moment.
     Note: flex-grow: 0 so the wheel-side respects its flex-basis exactly,
     instead of stretching to fill the flex container. */
  .wheel-side {
    flex: 0 0 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: flex-basis .9s cubic-bezier(.65,.05,.36,1);
    z-index: 2;
  }
  .stage.intro-done .wheel-side {
    flex-basis: 50%;
  }

  /* Right side: info panel — sits off-screen until intro ends, then slides
     in. Same class drives the wheel shrink, so the two motions are synced.
     Generous padding gives the inner content breathing room from the
     viewport edges. */
  .panel-side {
    position: absolute;
    top: 0;
    right: 0;
    width: 50%;
    height: 100%;
    background: #ffffff;
    transform: translateX(100%);
    transition: transform .9s cubic-bezier(.65,.05,.36,1);
    display: flex;
    flex-direction: column;
    padding: 11cqh 6cqw 9cqh;
    z-index: 1;
  }
  .stage.intro-done .panel-side {
    transform: translateX(0);
  }

  /* ---------- Wheel ---------- */
  .wheel {
    --wheel-size: min(58cqh, 46cqw, 640px);
    --r-px: calc(var(--wheel-size) * 0.38);    /* radial distance to icon CENTER */
    --icon-half: calc(var(--wheel-size) * 0.11); /* half-width of an icon-circle (22% / 2) */
    /* Where SIDE labels start (top/bottom/left/right): past the icon's outer
       edge along the cardinal axis. */
    --label-clear: calc(var(--r-px) + var(--icon-half) + 14px);
    /* Where DIAGONAL labels start (corners): the diagonal icon's outer edge
       projected onto the horizontal axis is r·sin(45°) + icon-half. So the
       diagonal label is much closer to its icon than a label on the cardinal
       axis would be — matching the reference layout. */
    --label-clear-diag: calc(var(--r-px) * 0.7071 + var(--icon-half) + 14px);
    position: relative;
    width: var(--wheel-size);
    aspect-ratio: 1;
  }

  .center-logo {
    /* OUTER: positioning + intro animations only. Once those animations
       finish (t≈4.85s), this element settles in `forwards` state and is
       NEVER toggled again — so clicking Back/selecting icons can't re-trigger
       the pop-in. */
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%) scale(0);
    width: 38%;
    aspect-ratio: 1;
    z-index: 10;
    /* Two animations chained in time:
       1) logoIn  — pop-in (delay 0.15s, dur 0.9s, ends at t=1.05s)
       2) logoHeartbeat — 4 cycles at the new unified 1.5s rhythm
          (delay 1.05s, dur 1.5s × 4 iters = 6s, ends at t≈7.05s,
          comfortably before labels-on fires at t=7.6s). */
    animation-name: logoIn, logoHeartbeat;
    animation-duration: .9s, 1.5s;
    animation-delay: .15s, 1.05s;
    animation-timing-function: cubic-bezier(.65,.05,.36,1), ease-in-out;
    animation-iteration-count: 1, 4;
    animation-fill-mode: forwards, forwards;
    pointer-events: none;
  }
  /* Once the intro is done (and no icon is selected), the logo becomes
     clickable so the user can trigger an extra heartbeat. When an icon is
     selected (panel-open) the logo is hidden + non-interactive. */
  .stage.intro-done:not(.panel-open) .center-logo {
    pointer-events: auto;
    cursor: pointer;
  }
  /* INNER: handles panel-open visibility via plain CSS transitions. Toggling
     `.stage.panel-open` modifies opacity + scale on this inner wrapper only,
     leaving the outer's animation state untouched → no restart on Back. */
  .center-logo-inner {
    width: 100%;
    height: 100%;
    opacity: 1;
    transform: scale(1);
    transform-origin: center;
    transition: opacity .25s ease, transform .4s cubic-bezier(.65,.05,.36,1);
  }
  .stage.panel-open .center-logo-inner {
    opacity: 0;
    transform: scale(0.6);
  }
  /* PNG has true transparent background → no blend-mode needed. */
  .center-logo img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    user-select: none;
    -webkit-user-drag: none;
  }

  /* Big center circle, holding the selected icon (the "pin head" of the
     wheel). Background switches to the selected icon's --selected-accent
     (set on .stage by JS when an icon is clicked). Defaults to cyan. */
  .center-display {
    position: absolute;
    top: 50%; left: 50%;
    width: 32%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--selected-accent, var(--cyan));
    transform: translate(-50%, -50%) scale(0);
    opacity: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 12;
    pointer-events: none;
    transition: transform .5s cubic-bezier(.34, 1.56, .64, 1),
                opacity .3s ease,
                background .4s ease;
  }
  .center-display img {
    width: 60%;
    height: 60%;
    object-fit: contain;
    /* Inside the colored pin head, the icon is WHITE for clean contrast
       against cyan or red backgrounds. The source PNG is already white
       on transparent, so no filter is needed. */
    filter: none;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
  }
  .stage.panel-open .center-display {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
    transition-delay: .12s;
  }
  /* Switching between icons while panel is open: fade the current icon out
     (no scale-down), swap the <img>, fade the new one in. The .swapping class
     is added briefly by JS during the switch. */
  .stage.panel-open .center-display.swapping {
    opacity: 0;
    transition: opacity .18s ease;
    transition-delay: 0s;
  }

  /* ----- Discover transition -----
     Clicking Discover sends the selected icon (the cyan .center-display
     "pin head") to the center of the VIEWPORT while the rest of the page
     (wheel, labels, right panel, topbar, hint) fades out.

     Why translate by +25vw: when `.intro-done` is active, .wheel-side is
     50vw wide and the wheel is centered inside it, so the .center-display
     currently sits at the horizontal center of the wheel-side = 25vw from
     the viewport's left edge. To land it on the page center (50vw),
     we shift right by +25vw. Vertical position is already 50vh
     (translateY -50% from a top: 50% absolute origin → no Y change needed).
     Scale is bumped to 1.35× for a stronger "presentation" feel. */
  .stage.discovering .center-display {
    transform: translate(calc(-50% + 25cqw), -50%) scale(1.35);
    transition: transform .95s cubic-bezier(.65, .05, .36, 1),
                opacity   .3s ease;
    z-index: 1000;
  }

  /* Everything that should fade away while the icon flies to center. */
  .stage.discovering .panel-side,
  .stage.discovering .icon-node:not(.selected),
  .stage.discovering .icon-label,
  .stage.discovering .topbar,
  .stage.discovering .hint {
    opacity: 0 !important;
    pointer-events: none;
    transition: opacity .55s ease;
  }
  /* The little cyan dot that remains in the selected icon's wheel slot,
     and the line connecting it to the center, both fade out (the "pin"
     leaves the wheel and travels to center). `!important` + height: 0
     to outrank the `.stage.panel-open .icon-node.selected::before`
     declaration which has equal specificity. */
  .stage.discovering .icon-node.selected .icon-circle {
    opacity: 0 !important;
    transition: opacity .4s ease;
  }
  .stage.discovering .icon-node.selected::before {
    opacity: 0 !important;
    height: 0 !important;
    transition: opacity .25s ease, height .25s ease;
  }
  /* Centered icon becomes clickable to return to wheel. */
  .stage.discovering .center-display {
    pointer-events: auto;
    cursor: pointer;
  }
  /* Hide the DOKI DOKI heart logo during the discover sequence — even
     though .panel-open already hides .center-logo-inner, this belt-and-
     braces rule ensures nothing residual is rendered behind the icon
     as it flies to center. */
  .stage.discovering .center-logo {
    opacity: 0 !important;
    transition: opacity .3s ease;
  }

  /* On narrow / portrait layouts, .wheel-side spans the full width above
     the panel, so its center is already at 50vw — no horizontal shift. */
  @media (max-width: 800px) and (orientation: portrait) {
    .stage.discovering .center-display {
      transform: translate(-50%, -50%) scale(1.35);
    }
  }

  /* ----- Heartbeat on the centered icon -----
     Triggered 0.5s after the icon reaches the center. Two cycles, each
     1.5s long (lub-dub in first 500ms, then 1s rest before next beat). */
  @keyframes centerHeartbeat {
    0%       { transform: translate(calc(-50% + 25cqw), -50%) scale(1.35); }
    6.67%    { transform: translate(calc(-50% + 25cqw), -50%) scale(1.55); }
    14.67%   { transform: translate(calc(-50% + 25cqw), -50%) scale(1.32); }
    22.67%   { transform: translate(calc(-50% + 25cqw), -50%) scale(1.60); }
    33.33%   { transform: translate(calc(-50% + 25cqw), -50%) scale(1.35); }
    100%     { transform: translate(calc(-50% + 25cqw), -50%) scale(1.35); }
  }
  @media (max-width: 800px) and (orientation: portrait) {
    @keyframes centerHeartbeat {
      0%       { transform: translate(-50%, -50%) scale(1.35); }
      6.67%    { transform: translate(-50%, -50%) scale(1.55); }
      14.67%   { transform: translate(-50%, -50%) scale(1.32); }
      22.67%   { transform: translate(-50%, -50%) scale(1.60); }
      33.33%   { transform: translate(-50%, -50%) scale(1.35); }
      100%     { transform: translate(-50%, -50%) scale(1.35); }
    }
  }
  .center-display.heartbeat {
    animation: centerHeartbeat 1.5s ease-in-out 2;
  }
  /* Icon vanishes the instant the hole begins to open — leaving the
     bare video underneath as the mask peels back. */
  .center-display.vanish {
    opacity: 0;
    transition: opacity .35s ease;
  }
  /* Hide the original center-display while the reveal-icon clone (which
     lives ABOVE the mosaic, inside the reveal-stack) is on stage. */
  .stage.in-reveal .center-display {
    opacity: 0;
    transition: opacity .2s ease;
  }

  /* ----- Reveal stack: mosaic (top) + video (bottom) -----
     Sits above everything during the reveal sequence. The mosaic is a
     GRID OF INDIVIDUAL X TILES (built by JS at reveal time). Each tile
     animates scale(0)→scale(1)→scale(0) on a single keyframe pulse, with
     its animation-delay set to be proportional to its distance from the
     viewport center. Net effect: a ring of full-size X's propagates from
     center outward, with smaller "ghost" X's appearing ahead of the front
     (still growing) and X's behind the front rapidly scaling back down.
     Once the wave has passed the entire viewport, every tile has settled
     at scale 0 — leaving the video below fully visible. */
  .reveal-stack {
    position: absolute;   /* relatif à #frame */
    inset: 0;
    z-index: 800;
    pointer-events: none;
    visibility: hidden;
    opacity: 0;
    /* On DEACTIVATION (close): fade out over 0.2s, then hide. */
    transition: opacity .2s ease, visibility 0s linear .2s;
  }
  .reveal-stack.active {
    visibility: visible;
    opacity: 1;
    /* On ACTIVATION: appear INSTANTLY (no opacity fade). Otherwise the
       reveal-stack's fade-in blends with the first heartbeat pulsation,
       making the icon look like it's mid-fade during its first beat. */
    transition: opacity 0s, visibility 0s linear 0s;
  }
  .reveal-video-frame {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    background: #ffffff;
    /* Video stays hidden until the mosaic fully covers the screen — only
       then do we fade the frame in, so the user never glimpses the
       placeholder through the wave's transparent areas. */
    opacity: 0;
    transition: opacity .25s ease;
  }
  /* Video frame is now visible from the moment the wave starts — the
     pulsing tiles play OVER the video, not as a screen-filling mask. */
  .reveal-stack.show-video .reveal-video-frame {
    opacity: 1;
  }
  .reveal-video {
    width: 100%;
    height: 100%;
    /* contain (not cover) so the 16:9 video keeps its full aspect ratio
       on any viewport; the white frame background fills the letterbox
       or pillarbox bars if the viewport isn't itself 16:9. */
    object-fit: contain;
    display: block;
    background: #ffffff;
  }
  /* Placeholder shown until a real <source> is wired into .reveal-video.
     White theme to match the new reveal transition palette. */
  .reveal-video-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    color: var(--gray-text);
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    background: #ffffff;
  }
  .rvp-glyph {
    width: 84px;
    height: 84px;
    border-radius: 50%;
    border: 2.5px solid var(--cyan);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--cyan);
    font-size: 38px;
    padding-left: 6px;  /* visual-center the play triangle */
  }
  .rvp-label {
    font-size: 14px;
    letter-spacing: .42em;
    font-weight: 900;
    color: var(--ink);
  }
  .rvp-sub {
    font-size: 11px;
    letter-spacing: .25em;
    font-weight: 300;
    color: var(--gray-text);
  }

  .reveal-mosaic {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    overflow: hidden;
    /* Opaque cover — white to match the transition palette. */
    background-color: #ffffff;
    /* Opacity-transition restored for VAGUE 1: the mosaic must fade IN
       gradually as the icon fades OUT — 1.5s matches the cross-fade
       duration (one full heartbeat cycle). For VAGUE 2 (outro), the JS
       uses an inline `transition: none` override at the moment of
       .dark-in so the mosaic snaps instantly, avoiding the brief
       color-step the user reported. */
    opacity: 0;
    transition: opacity 1.5s ease;
    /* Radial reveal mask: a transparent disc of radius --reveal-r grows
     from center, with a thin (1%) feather to keep the seam soft. Inside
     the disc → transparent (video visible). Outside → opaque (mosaic
     stays drawn). The disc tracks the trailing edge of the X wave: only
     opens up areas where the tiles have already finished pulsing. */
    --reveal-r: 0%;
    -webkit-mask-image: radial-gradient(circle at center,
      transparent calc(var(--reveal-r)),
      black calc(var(--reveal-r) + 1%));
    mask-image: radial-gradient(circle at center,
      transparent calc(var(--reveal-r)),
      black calc(var(--reveal-r) + 1%));
  }
  .reveal-mosaic.dark-in {
    opacity: 1;
  }

  /* One tile = one X icon, absolute-positioned by JS. The PNG already has
     red + cyan baked in, so no filter needed. Starts hidden at scale 0. */
  .reveal-tile {
    position: absolute;
    width: 105px;
    height: 105px;
    background-image: var(--asset-cross);
    background-repeat: no-repeat;
    background-position: center center;
    background-size: contain;
    transform: translate(-50%, -50%) scale(0);
    pointer-events: none;
    /* PAS de `will-change: transform` permanent : il promouvait les ~270 tuiles
       en autant de calques GPU EN PERMANENCE, y compris pendant la lecture
       vidéo (la mosaïque reste dans le DOM, masque ouvert) → compositing lourd
       par-dessus la vidéo = saccades (lecteur natif = fluide). Chrome promeut
       de toute façon les tuiles pendant leur animation de transform (la vague
       reste fluide) ; une fois la vague finie elles redeviennent de simples
       éléments scale(0), sans calque → vidéo fluide. */
  }

  /* The pulse: each tile grows 0→1 in the first ~45% of its animation,
     briefly sits at full size, then collapses back to 0. The ramp-up is
     fast (out-cubic) so the wave front feels bright + crisp; the ramp-down
     is gentler so X's don't blink off behind the front. */
  @keyframes tilePulse {
    0%   { transform: translate(-50%, -50%) scale(0);     }
    45%  { transform: translate(-50%, -50%) scale(1);     }
    55%  { transform: translate(-50%, -50%) scale(1);     }
    100% { transform: translate(-50%, -50%) scale(0);     }
  }

  /* OUTRO wave variant: tiles show the DOKI DOKI heart-with-letters logo
     instead of the X cross. Smaller tile size (per user spec) so multiple
     logos read in the propagating wave. */
  .reveal-mosaic.outro-wave .reveal-tile {
    width: 110px;
    height: 110px;
    background-image: var(--asset-logo-letters);
  }

  /* --norm is a per-tile CSS variable set by JS: 0 at the center,
     1 at the farthest corner. Delay scales linearly with --norm so
     the wave radiates outward at constant angular velocity. */
  .reveal-mosaic.wave-out .reveal-tile {
    animation: tilePulse 1.4s cubic-bezier(.4, 0, .3, 1) forwards;
    animation-delay: calc(var(--norm, 0) * 2.4s);
  }

  /* Mask-open: triggers the radial reveal that uncovers the video. JS
     adds this class AFTER the central tiles have finished decaying so
     the disc grows in sync with the trailing edge of the wave. */
  .reveal-mosaic.mask-open {
    transition: --reveal-r 2.4s linear;
  }
  /* Une fois la vidéo RÉVÉLÉE (masque grand ouvert → mosaïque entièrement
     transparente), on retire complètement la mosaïque du rendu. Sinon ce
     calque masqué PLEIN ÉCRAN (radial-gradient) reste composité PAR-DESSUS la
     vidéo à CHAQUE frame → saccades de lecture (cause structurelle, même en
     local ; la vidéo en lecteur natif est fluide). Ajouté par JS ~à la fin de
     l'ouverture du masque ; retiré au reset / avant la VAGUE 2. */
  .reveal-mosaic.reveal-done {
    display: none;
  }

  /* ----- Reveal icon: clone of the centered icon, drawn ABOVE the mosaic.
     Sits at the viewport center matching the original's resting position
     (which has been at viewport center since the fly-in completed).
     Stays visible during the entire wave-in phase and the first half of
     hole-out, so the user clearly sees the heartbeat in front of the
     spreading mosaic. */
  .reveal-icon {
    --rsize: min(58cqh, 46cqw, 640px);
    position: absolute;
    top: 50%;
    left: 50%;
    width: calc(var(--rsize) * 0.32);
    aspect-ratio: 1;
    border-radius: 50%;
    /* Picks up --selected-accent set on :root by selectIcon(), so the
       intro "pin head" matches the chosen icon's border color (cyan or
       red). Falls back to cyan. */
    background: var(--selected-accent, var(--cyan));
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;                 /* above .reveal-mosaic (z:2) and video (z:1) */
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.35);
    /* No opacity transition by default — the icon must appear INSTANTLY
       when .reveal-stack.active is added, otherwise it would still be
       fading in when the first heartbeat pulsation fires, producing a
       visible fade-blended pulse. The .vanish rule sets its own .5s
       transition for the controlled fade-out later. */
    pointer-events: none;
  }
  .reveal-stack.active .reveal-icon {
    opacity: 1;
  }
  .reveal-icon img {
    width: 60%;
    height: 60%;
    object-fit: contain;
    /* PNG source is WHITE on transparent; leave it as-is so the icon
       renders white on top of the cyan/red pin head. */
    filter: none;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
  }
  /* Outro mode for the reveal-icon — used when a DATA entry has
     `outroIcon`. Drops the cyan ring (transparent background, no border),
     shrinks the slot, and lets the realistic illustration fill the
     full area in its native colors (no brightness:0 silhouette filter).
     Aura was removed per user spec. */
  .reveal-icon.outro-mode {
    background: transparent;
    width: calc(var(--rsize) * 0.44);
  }
  .reveal-icon.outro-mode img {
    width: 100%;
    height: 100%;
    filter: none;
  }

  /* Outro entry: fade IN + scale-up from 0 → 1.35 so the icon emerges
     from nothing rather than from a partial size. Lands at the heartbeat
     base scale. */
  @keyframes outroIconAppear {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(0); }
    100% { opacity: 1; transform: translate(-50%, -50%) scale(1.35); }
  }
  .reveal-icon.appearing {
    animation: outroIconAppear 1.4s cubic-bezier(.34, 1.1, .64, 1) forwards;
  }

  /* Outro dissolve: fade OUT + scale up to 1.85 in place, used at the
     moment the wave-2 reveal exposes the Keelebunn image behind. */
  @keyframes outroIconDissolve {
    0%   { opacity: 1; transform: translate(-50%, -50%) scale(1.35); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(1.85); }
  }
  .reveal-icon.dissolving {
    animation: outroIconDissolve 1.2s ease-out forwards;
  }

  /* Heartbeat — ONE lub-dub cycle. JS chains beats by removing
     + re-adding the class (with a reflow in between) for the swap moment.
     New rhythm (1.5s cycle): lub-dub completes by 500ms, then 1.0s rest
     before the next beat — gives "1 second between beats" feel everywhere. */
  @keyframes revealIconHeartbeat {
    0%       { transform: translate(-50%, -50%) scale(1.35); }
    6.67%    { transform: translate(-50%, -50%) scale(1.55); }  /* lub peak  (100ms) */
    14.67%   { transform: translate(-50%, -50%) scale(1.32); }  /* lub end   (220ms) */
    22.67%   { transform: translate(-50%, -50%) scale(1.60); }  /* dub peak  (340ms) */
    33.33%   { transform: translate(-50%, -50%) scale(1.35); }  /* dub end   (500ms) */
    100%     { transform: translate(-50%, -50%) scale(1.35); }  /* rest end (1500ms) */
  }
  .reveal-icon.heartbeat {
    animation: revealIconHeartbeat 1.5s ease-in-out 1;
  }
  /* Outro variant — same lub-dub keyframe but renamed so the browser
     sees a DIFFERENT animation-name and restarts it from 0% instead of
     skipping (which is what happens when the same name is reused on an
     already-finished animation). Runs twice in a row → user sees two full
     heartbeats with 1s pause between them.
     OUTRO AMPLITUDE: scale jumps are MUCH bigger than the intro keyframe
     (peak 1.95 vs 1.60) so the icon punches forward visibly during the
     wave. The icon is smaller in outro-mode (0.22 of rsize), so a larger
     scale doesn't make it dominate DOKI DOKI — it just reads as a
     stronger pulse on a small element. */
  @keyframes revealIconHeartbeatOutro {
    0%       { transform: translate(-50%, -50%) scale(1.35); }
    6.67%    { transform: translate(-50%, -50%) scale(1.80); }  /* lub peak — stronger */
    14.67%   { transform: translate(-50%, -50%) scale(1.25); }  /* deeper return */
    22.67%   { transform: translate(-50%, -50%) scale(1.95); }  /* dub peak — biggest */
    33.33%   { transform: translate(-50%, -50%) scale(1.35); }
    100%     { transform: translate(-50%, -50%) scale(1.35); }
  }
  .reveal-icon.heartbeat-2 {
    animation: revealIconHeartbeatOutro 1.5s ease-in-out 2;
  }
  /* Fades the clone out the instant the wave has finished covering, so
     when the hole opens and reveals the video below, no icon lingers on
     top of it. `!important` to outrank `.reveal-stack.active .reveal-icon`
     (which is `opacity: 1` and has equal-or-higher specificity). */
  .reveal-stack.active .reveal-icon.vanish,
  .reveal-icon.vanish {
    opacity: 0 !important;
    /* Cross-fade duration tied to the heartbeat rhythm: 1 full beat cycle
       (1.5s) elapses between end-of-1st-pulsation of beat 1 (icon) and
       end-of-1st-pulsation of beat 2 (doki). All three layers — icon,
       mosaic background, doki — share this same .8 → 1.5s. */
    transition: opacity 1.5s ease;
  }
  /* DOKI DOKI letters logo — a separate layered element above the wave
     that cross-fades IN as the selected-icon cross-fades OUT. Larger in
     the outro to be the hero of the wave transition (the user wants it
     bigger than the icon during the heartbeats + wave). */
  .reveal-doki {
    --rsize: min(58cqh, 46cqw, 640px);
    position: absolute;
    top: 50%;
    left: 50%;
    /* Taille de BASE = section 3 (VAGUE 1) : 0.75 de --rsize. NE PAS agrandir
       ici : en section 3 le logo fait le cross-fade PAR-DESSUS l'icône (il doit
       la recouvrir) ; l'agrandir laissait voir la pastille nue (cf. historique).
       La section 5 (VAGUE 2) a sa propre taille via .behind-icon ci-dessous. */
    width: calc(var(--rsize) * 0.75);
    z-index: 11;
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.92);
    /* Slight scale-up while fading in adds a touch of "emergence" feel.
       Bumped to 1.5s to match the new heartbeat-driven cross-fade window. */
    transition: opacity 1.5s ease, transform 1.5s cubic-bezier(.34, 1.1, .64, 1);
    pointer-events: none;
  }
  .reveal-stack.active .reveal-doki.active {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  .reveal-doki img {
    width: 100%;
    height: auto;
    object-fit: contain;
    display: block;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
  }
  @keyframes revealDokiHeartbeat {
    0%       { transform: translate(-50%, -50%) scale(1);     }
    6.67%    { transform: translate(-50%, -50%) scale(1.10);  }  /* lub peak  (100ms) */
    14.67%   { transform: translate(-50%, -50%) scale(0.98);  }  /* lub end   (220ms) */
    22.67%   { transform: translate(-50%, -50%) scale(1.13);  }  /* dub peak  (340ms) */
    33.33%   { transform: translate(-50%, -50%) scale(1);     }  /* dub end   (500ms) */
    100%     { transform: translate(-50%, -50%) scale(1);     }  /* rest end (1500ms) */
  }
  .reveal-doki.heartbeat {
    animation: revealDokiHeartbeat 1.5s ease-in-out 1;
  }
  /* Outro variant — same reason as .reveal-icon.heartbeat-2: renamed
     keyframe forces a clean animation restart even though the original
     intro .heartbeat class is still present on the element. */
  @keyframes revealDokiHeartbeatOutro {
    0%       { transform: translate(-50%, -50%) scale(1);     }
    6.67%    { transform: translate(-50%, -50%) scale(1.10);  }
    14.67%   { transform: translate(-50%, -50%) scale(0.98);  }
    22.67%   { transform: translate(-50%, -50%) scale(1.13);  }
    33.33%   { transform: translate(-50%, -50%) scale(1);     }
    100%     { transform: translate(-50%, -50%) scale(1);     }
  }
  .reveal-doki.heartbeat-2 {
    animation: revealDokiHeartbeatOutro 1.5s ease-in-out 2;
  }
  /* Same `!important` defense as .reveal-icon.vanish so it outranks the
     .reveal-stack.active .reveal-doki.active opacity-1 rule. */
  .reveal-stack.active .reveal-doki.vanish,
  .reveal-doki.vanish {
    opacity: 0 !important;
    /* Matches the cross-fade-in duration so both directions feel symmetric. */
    transition: opacity 1.5s ease;
  }

  .panel-main {
    transition: opacity .18s ease;
  }
  .panel-side.swapping .panel-main {
    opacity: 0;
  }

  /* Hide the placeholder when a real video source is wired in for the
     selected element. The .has-video class is toggled by JS in selectIcon. */
  .reveal-stack.has-video .reveal-video-placeholder {
    display: none;
  }
  /* Registered properties for the orbit-in animation and the reveal mask
     radius. --reveal-r needs to be registered as <percentage> so a CSS
     transition can interpolate it (and trigger mask-image re-evaluation). */
  @property --reveal-r {
    syntax: '<percentage>';
    inherits: false;
    initial-value: 0%;
  }
  @property --orbit-a {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
  }
  @property --orbit-s {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
  }
  @property --orbit-o {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
  }

  /* 8 icon positions on a circle. Orbital entry:
     - icon spawns at top (--orbit-a = 0deg), invisible & scaled to 0
     - first fades in / scales up (phase 1, ~0.3s)
     - then orbits along the circumference to its target --angle (phase 2)
     - using @property so the angle can be interpolated by CSS */
  .icon-node {
    --size: 22%;
    --orbit-a: 90deg;    /* live angle (animated) — starts at radiant 0 (right) */
    --orbit-s: 1;        /* scale fixed at 1 (no spawn animation, fade-in only) */
    --orbit-o: 0;        /* live opacity (animated) */
    position: absolute;
    top: 50%; left: 50%;
    width: var(--size);
    aspect-ratio: 1;
    transform: translate(-50%, -50%)
               rotate(var(--orbit-a))
               translate(0, calc(var(--r-px, 200px) * -1))
               rotate(calc(var(--orbit-a) * -1))
               scale(var(--orbit-s));
    opacity: var(--orbit-o);
    animation-name: var(--anim-name, orbitIn-1);
    animation-duration: var(--anim-dur, 3.85s);
    animation-delay: var(--delay, 0s);
    animation-timing-function: linear;
    animation-fill-mode: forwards;
    cursor: pointer;
    transition: transform .55s var(--transition-slow), opacity .4s ease;
  }

  /* Pendant l'intro de la roue ET le slide-in du panel, les icônes ne
     sont pas cliquables — un clic prématuré déclencherait selectIcon()
     avant que le panel ne soit complètement en place, provoquant des
     sauts visuels. Seuil = .panel-text-in (ajoutée à T=+6,25s) : à ce
     moment-là le panel a fini son slide depuis la droite et son texte
     commence à fade in, donc l'utilisateur voit clairement qu'il peut
     interagir. Couvre aussi le hover (pointer-events: none désactive
     les deux), donc le curseur ne change pas non plus pendant l'intro. */
  .stage:not(.panel-text-in) .icon-node {
    pointer-events: none;
  }

  .icon-circle {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    background: var(--cream);
    /* Border uses the per-icon --accent (set on .pos-N). Falls back to
       cyan if --accent isn't defined for some reason. */
    border: 2.5px solid var(--accent, var(--cyan));
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background .4s ease, border-color .4s ease, transform .4s var(--transition-slow);
    box-shadow: 0 0 0 0 rgba(0,171,237,0);
  }
  /* Icon images: the asset PNGs are WHITE on transparent. brightness(0)
     collapses them to pure black for the resting state. The selected and
     center-display variants override this filter to switch the silhouette
     color (e.g. white on cyan when the icon is inside the big pin head). */
  .icon-circle img {
    width: 62%;
    height: 62%;
    object-fit: contain;
    filter: brightness(0);
    transition: filter .35s ease;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
  }
  .icon-node:hover .icon-circle {
    transform: scale(1.08);
    box-shadow: 0 6px 24px -10px rgba(0,171,237,.55);
  }

  /* Dimmed (when another is selected) — borders fade to gray and icon
     images shift from black to mid-gray via a softer brightness curve. */
  .stage.panel-open .icon-node:not(.selected) .icon-circle {
    border-color: var(--gray-soft);
  }
  .stage.panel-open .icon-node:not(.selected) .icon-circle img {
    filter: brightness(0) opacity(.45);
  }

  /* Selected pin look: shrinks the icon-circle into a small filled blue dot
     (the pin's "head" at the original slot) + a line going INWARD to the
     central .center-display (the pin's "body"). */
  .icon-node.selected {
    z-index: 20;
  }
  .stage.panel-open .icon-node.selected .icon-circle {
    background: var(--accent, var(--cyan));
    border-color: var(--accent, var(--cyan));
    transform: scale(0.42);
  }
  .stage.panel-open .icon-node.selected .icon-circle img {
    opacity: 0;
    transition: opacity .25s ease;
  }

  /* Legacy .pin-tail (decorative droplet above the icon) — not used in
     the new design. Kept in the DOM for backward compat but hidden. */
  .pin-tail { display: none; }

  /* The line connecting the selected slot to the central display. It's a
     thin vertical bar anchored at the icon's center, rotated by --angle so
     it points inward toward the wheel center. Height ramps from 0 to a
     length that just reaches the central display when selected. */
  /* Pin connector line uses the selected icon's --accent so the whole
     pin (head + body) reads as one solid color. */
  .icon-node::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 5px;
    height: 0;
    background: var(--accent, var(--cyan));
    border-radius: 3px;
    transform: translate(-50%, 0) rotate(var(--angle, 0deg));
    transform-origin: 50% 0%;
    opacity: 0;
    pointer-events: none;
    z-index: -1;
    transition: height .35s cubic-bezier(.65,.05,.36,1), opacity .25s ease;
  }
  .stage.panel-open .icon-node.selected::before {
    /* Line length = (distance from icon center to wheel center) − (center-display
       radius) + small overshoot. center-display is 32% of wheel, so its radius
       is 16% of wheel. The +4px overshoot tucks the line tip slightly inside
       the cyan center disc to guarantee no visible gap at any zoom level. */
    height: calc(var(--r-px, 200px) - var(--wheel-size) * 0.16 + 4px);
    opacity: 1;
    transition: height .4s cubic-bezier(.65,.05,.36,1) .15s,
                opacity .25s ease .15s;
  }

  /* =========================================================
     Cascade (112.5°/s linear) + EXTRA REVOLUTION (1 full lap)
     ---------------------------------------------------------
     Step time T = 0.4s → cascade speed 45°/0.4s = 112.5°/s.
     All icons finish their cascade phase at wall-clock t=3.8s, then all
     rotate +360° together over **1.5s** (extra rev), ending at t=5.3s.

     Timing function for the extra rev: cubic-bezier(0.4, 0.188, 0.5, 1):
       • normalized start slope = 0.188/0.4 = 0.47
         → real start velocity = 0.47 × (360°/1.5s) = ~112.5°/s
            (matches the cascade velocity — no jerk at the join)
       • normalized end slope = 0 → smooth stop
       • peak slope is now lower (~1.5 vs ~1.68 before) and the ramp is
         spread over more time → noticeably smoother acceleration
         (peak speed ~360°/s instead of ~500°/s)
     ========================================================= */

  /* —— N=1 PLANTS, K=7, dur 4.3s (cascade 2.8s + extra-rev 1.5s), final 765° —— */
  @keyframes orbitIn-1 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    6.98%   {                    --orbit-o: 1; }
    65.12%  { --orbit-a: 405deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 765deg; --orbit-o: 1; }
  }
  /* —— N=2 KEELEBUNN, K=6, dur 3.9s, final 720° —— */
  @keyframes orbitIn-2 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    7.69%   {                    --orbit-o: 1; }
    61.54%  { --orbit-a: 360deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 720deg; --orbit-o: 1; }
  }
  /* —— N=3 STEEL, K=5, dur 3.5s, final 675° —— */
  @keyframes orbitIn-3 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    8.57%   {                    --orbit-o: 1; }
    57.14%  { --orbit-a: 315deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 675deg; --orbit-o: 1; }
  }
  /* —— N=4 PANELS, K=4, dur 3.1s, final 630° —— */
  @keyframes orbitIn-4 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    9.68%   {                    --orbit-o: 1; }
    51.61%  { --orbit-a: 270deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 630deg; --orbit-o: 1; }
  }
  /* —— N=5 FOUNDATION, K=3, dur 2.7s, final 585° —— */
  @keyframes orbitIn-5 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    11.11%  {                    --orbit-o: 1; }
    44.44%  { --orbit-a: 225deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 585deg; --orbit-o: 1; }
  }
  /* —— N=6 MEMBRANE, K=2, dur 2.3s, final 540° —— */
  @keyframes orbitIn-6 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    13.04%  {                    --orbit-o: 1; }
    34.78%  { --orbit-a: 180deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 540deg; --orbit-o: 1; }
  }
  /* —— N=7 FURNITURE, K=1, dur 1.9s, final 495° —— */
  @keyframes orbitIn-7 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    15.79%  {                    --orbit-o: 1; }
    21.05%  { --orbit-a: 135deg; animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
    100%    { --orbit-a: 495deg; --orbit-o: 1; }
  }
  /* —— N=8 AV, K=0, dur 1.5s — no cascade; rotates 360° during extra rev. —— */
  @keyframes orbitIn-8 {
    0%      { --orbit-a:  90deg; --orbit-o: 0; }
    20%     {                    --orbit-o: 1; }
    100%    { --orbit-a: 450deg; --orbit-o: 1; }
  }

  /* Per-icon class: keyframe + delay + duration with T=0.4s step.
     Entry stagger: 1.0s, 1.4s, 1.8s, 2.2s, 2.6s, 3.0s, 3.4s, 3.8s
     Cascade end at t=3.8s for everyone, extra rev ends at t=5.3s
     (extra rev duration extended from 1.2s → 1.5s for smoother acceleration). */
  /* Per-icon class: keyframe + delay + duration with T=0.4s step.
     Entry stagger: 1.0s, 1.4s, 1.8s, 2.2s, 2.6s, 3.0s, 3.4s, 3.8s
     Cascade end at t=3.8s for everyone, extra rev ends at t=5.3s
     (extra rev duration extended from 1.2s → 1.5s for smoother acceleration).

     --accent: per-icon accent color, alternating cyan / red in ENTRY
     ORDER (not position order) so the user sees a tidy blue-red-blue-red
     rhythm as the wheel populates. The same --accent is used for:
       • the icon's border (resting state)
       • the pin's "head" + connecting line when selected
       • the center-display ring (cyan or red) when this icon is selected
       • the panel title (Verlag Black)
     Labels are now always black (per spec) — they don't read --accent. */
  .pos-0 { --angle:   0deg; --delay: 1.40s; --anim-dur: 3.9s; --anim-name: orbitIn-2; --accent: var(--red);  }  /* KEELEBUNN  (entry #2 → red)  */
  .pos-1 { --angle:  45deg; --delay: 1.00s; --anim-dur: 4.3s; --anim-name: orbitIn-1; --accent: var(--cyan); }  /* PLANTS     (entry #1 → cyan) */
  .pos-2 { --angle:  90deg; --delay: 3.80s; --anim-dur: 1.5s; --anim-name: orbitIn-8;
           --accent: var(--red);                                                                                /* AV         (entry #8 → red)  */
           /* AV's whole animation is during the extra rev → override the
              element-level `linear` so the icon eases like the others. */
           animation-timing-function: cubic-bezier(0.4, 0.188, 0.5, 1); }
  .pos-3 { --angle: 135deg; --delay: 3.40s; --anim-dur: 1.9s; --anim-name: orbitIn-7; --accent: var(--cyan); }  /* FURNITURE  (entry #7 → cyan) */
  .pos-4 { --angle: 180deg; --delay: 3.00s; --anim-dur: 2.3s; --anim-name: orbitIn-6; --accent: var(--red);  }  /* MEMBRANE   (entry #6 → red)  */
  .pos-5 { --angle: 225deg; --delay: 2.60s; --anim-dur: 2.7s; --anim-name: orbitIn-5; --accent: var(--cyan); }  /* FOUNDATION (entry #5 → cyan) */
  .pos-6 { --angle: 270deg; --delay: 2.20s; --anim-dur: 3.1s; --anim-name: orbitIn-4; --accent: var(--red);  }  /* PANELS     (entry #4 → red)  */
  .pos-7 { --angle: 315deg; --delay: 1.80s; --anim-dur: 3.5s; --anim-name: orbitIn-3; --accent: var(--cyan); }  /* STEEL      (entry #3 → cyan) */

  /* ---------- Labels around icons (only when no panel open) ----------
     Per-position placement matching the original video: top/bottom icons
     get vertically displaced labels; sides + corners get HORIZONTAL labels
     placed at the icon's y-level on the outside of the wheel. */
  /* Labels: always BLACK now (per user spec). Per-icon --accent is no
     longer used here. */
  .icon-label {
    position: absolute;
    top: 50%; left: 50%;
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    /* Responsive : scale avec la LARGEUR du cadre (cqw). Plus le cadre est
       étroit, plus les labels rétrécissent -> évite qu'ils débordent sur 16:9.
       Plancher 10px, plafond 15px (réduit) pour limiter la largeur des labels. */
    font-size: clamp(10px, 0.85cqw, 15px);
    font-weight: 900;    /* Verlag Black for clear readability around the wheel */
    letter-spacing: .04em;
    color: var(--ink);
    line-height: 1.15;
    text-transform: uppercase;
    /* Largeur contrainte : les titres longs passent à la ligne (≈ 1 mot par
       ligne) au lieu de s'étaler. max-width >= 13em pour que même le mot le plus
       long (FUNDAMENTBLÖCKE ≈ 12,3em en allemand) tienne sur UNE ligne sans être
       coupé. En `em` → suit la taille de police clampée. overflow-wrap: normal
       = on ne coupe JAMAIS un mot en plein milieu (retour à la ligne entre mots
       seulement). */
    max-width: 13em;
    overflow-wrap: normal;
    opacity: 0;
    /* Single transition for both fade-in and fade-out. The initial reveal
       gets a staggered delay via the .first-labels-reveal class below;
       subsequent appearances (e.g. after Back) reuse this same transition
       but without per-label delays, so all labels return to opacity 1 in
       one quick fade. */
    transition: opacity .6s ease;
    pointer-events: none;
  }
  /* Labels visible when labels-on AND not panel-open. */
  .stage.labels-on:not(.panel-open) .icon-label {
    opacity: 1;
  }
  /* Stagger only on the INITIAL reveal (set + then removed by JS).
     130ms between each, 0.6s duration → last label finishes at ~1.5s. */
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-0 { transition-delay: .00s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-1 { transition-delay: .13s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-2 { transition-delay: .26s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-3 { transition-delay: .39s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-4 { transition-delay: .52s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-5 { transition-delay: .65s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-6 { transition-delay: .78s; }
  .stage.first-labels-reveal:not(.panel-open) .icon-label.pos-7 { transition-delay: .91s; }

  /* All labels are pushed to --label-clear from the wheel center
     (= past the icon's outer edge + 14px buffer), so no overlap. */

  /* Top (KEELEBUNN): centered horizontally, above the wheel */
  .icon-label.pos-0 {
    transform: translate(-50%, calc(-100% - var(--label-clear, 220px) - 4px));
    text-align: center;
  }
  /* Bottom (MEMBRANE): centered horizontally, below the wheel */
  .icon-label.pos-4 {
    transform: translate(-50%, calc(var(--label-clear, 220px) + 4px));
    text-align: center;
  }
  /* Right (AV): label past the wheel's right outer edge, vertically centered */
  .icon-label.pos-2 {
    transform: translate(var(--label-clear, 220px), -50%);
    text-align: left;
  }
  /* Left (PANELS): mirror of pos-2 — anchored from the label's right edge */
  .icon-label.pos-6 {
    transform: translate(calc(var(--label-clear, 220px) * -1 - 100%), -50%);
    text-align: right;
  }
  /* Top-right (PLANTS): label placed just past the icon's right edge
     (~r·sin45° + icon-half + 14), NOT all the way at the wheel's outer x. */
  .icon-label.pos-1 {
    transform: translate(var(--label-clear-diag, 175px), calc(var(--r-px, 200px) * -0.7071 - 50%));
    text-align: left;
  }
  /* Bottom-right (FURNITURE): same x as pos-1, y mirrored */
  .icon-label.pos-3 {
    transform: translate(var(--label-clear-diag, 175px), calc(var(--r-px, 200px) * 0.7071 - 50%));
    text-align: left;
  }
  /* Bottom-left (FOUNDATION BLOCKS): mirror of pos-3 */
  .icon-label.pos-5 {
    transform: translate(calc(var(--label-clear-diag, 175px) * -1 - 100%), calc(var(--r-px, 200px) * 0.7071 - 50%));
    text-align: right;
  }
  /* Top-left (STEEL STRUCTURE): mirror of pos-1 */
  .icon-label.pos-7 {
    transform: translate(calc(var(--label-clear-diag, 175px) * -1 - 100%), calc(var(--r-px, 200px) * -0.7071 - 50%));
    text-align: right;
  }

  /* Note: opacity transition + animation handled above. The stagger-keyed
     `labelFadeIn` animation reads `:not(.panel-open)` so labels auto-fade
     out via the .25s transition when an icon is selected.

     Exception: the label of the SELECTED icon stays visible (so the user
     keeps context on which slot they clicked). JS adds `.selected` to the
     matching .icon-label when an icon is clicked. */
  .stage.panel-open .icon-label.selected {
    opacity: 1;
  }

  /* ---------- Right panel content ----------
     .panel-main is the default-state container holding title / desc /
     actions. Made it a flex column with full height so auto-margins push
     the actions to the bottom edge of the panel (with explicit min-margins
     also enforced for cramped viewports). */
  .panel-main {
    display: flex;
    flex-direction: column;
    height: 100%;
    transition: opacity .18s ease;
  }
  /* Title + desc + actions all start invisible + slightly translated down.
     The `.panel-text-in` class (added after the panel slide finishes) plays
     a staggered fade+slide animation top → bottom. animation-fill-mode:
     forwards locks the final state so subsequent content changes don't
     re-trigger the animation. */
  .panel-title,
  .panel-desc,
  .panel-actions {
    opacity: 0;
    transform: translateY(20px);
  }
  .stage.panel-text-in .panel-title {
    animation: panelChildIn .85s cubic-bezier(.34, 1.1, .64, 1) .0s forwards;
  }
  .stage.panel-text-in .panel-desc {
    animation: panelChildIn .85s cubic-bezier(.34, 1.1, .64, 1) .22s forwards;
  }
  .stage.panel-text-in .panel-actions {
    animation: panelChildIn .85s cubic-bezier(.34, 1.1, .64, 1) .44s forwards;
  }
  @keyframes panelChildIn {
    from { opacity: 0; transform: translateY(20px); }
    to   { opacity: 1; transform: translateY(0);    }
  }

  /* Wraps title + desc so they can be vertically centered as a block in
     the available space between the top padding and the bottom actions row. */
  .panel-copy {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 0;
  }

  .panel-title {
    /* Title color is driven by the selected icon's --selected-accent
       (set on .stage by JS). Falls back to red so the default panel
       ("Circular Concept") still reads with the brand red. */
    color: var(--selected-accent, var(--red));
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(34px, 3.2cqw, 50px);
    font-weight: 900;        /* Verlag Black */
    letter-spacing: 0;
    line-height: 1.05;
    margin: 0 0 36px;        /* breathing room before the description */
    transition: color .4s ease;
  }
  .panel-desc {
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(17px, 1.5cqw, 22px);
    font-weight: 300;        /* Verlag Light */
    line-height: 1.65;       /* slightly looser line-height for legibility */
    color: #1a1a1a;
    max-width: 600px;
    margin: 0;
  }
  .panel-actions {
    margin-top: 56px;        /* enforced gap between desc and actions */
    padding-top: 8px;
    display: flex;
    align-items: center;
    /* Default = flex-start (prompt block alone, anchored to the LEFT,
       i.e. closer to the central border where the wheel meets the panel).
       Overridden to space-between when .stage.panel-open is active. */
    justify-content: flex-start;
    gap: 32px;
    flex-wrap: wrap;
  }
  /* Section 1 (panneau par défaut, AUCUN élément sélectionné) — rapprocher le
     prompt « Select an element… » du texte : on centre le BLOC [titre+desc+prompt]
     ENSEMBLE au lieu de laisser .panel-copy (flex:1) pousser le prompt tout en bas.
     Scopé à `:not(.panel-open)` → la section 2 (Back/Discover) garde ses actions en
     bas. En cadre compact, `.panel-copy` garde son overflow/scroll (cf. @container),
     donc reste responsive (le bloc se centre s'il tient, scrolle s'il déborde). */
  .stage:not(.panel-open) .panel-main { justify-content: center; }
  .stage:not(.panel-open) .panel-copy { flex: 0 1 auto; }
  .btn {
    cursor: pointer;
    border: 2px solid var(--cyan);
    border-radius: 999px;
    background: transparent;
    color: var(--ink);
    padding: 15px 42px;
    font-size: 15px;
    font-weight: 300;        /* Verlag Light for buttons */
    letter-spacing: .02em;
    transition: background .25s ease, color .25s ease, transform .2s ease;
    font-family: inherit;
  }
  .btn:hover { transform: translateY(-2px); }
  /* Discover (primary CTA) — visually elevated to be the panel's focal action:
     • slightly larger padding than the Back button
     • bold Verlag Black weight (vs Light on Back) for confident voice
     • cyan-tinted drop shadow that grows on hover (lifts off the page)
     • subtle scale-up on hover so the button feels reactive */
  .btn-primary {
    background: var(--cyan);
    color: #fff;
    font-weight: 900;            /* Verlag Black, vs Light on Back */
    padding: 16px 48px;
    letter-spacing: .04em;
    box-shadow: 0 10px 26px -10px rgba(0, 171, 237, 0.65),
                0 4px 10px -4px rgba(0, 171, 237, 0.35);
    transition: background .25s ease,
                color .25s ease,
                transform .2s ease,
                box-shadow .25s ease;
  }
  .btn-primary:hover {
    background: #0098cf;
    transform: translateY(-3px) scale(1.04);
    box-shadow: 0 16px 32px -10px rgba(0, 171, 237, 0.75),
                0 6px 14px -4px rgba(0, 171, 237, 0.45);
  }
  .btn-primary:active {
    transform: translateY(-1px) scale(1.01);
    box-shadow: 0 6px 16px -8px rgba(0, 171, 237, 0.55);
  }

  /* Prompt line: arrow + text + (only when selected) right-arrow + Discover.
     Slightly larger + bolder than before to elevate the Discover CTA group
     as the visual focal point of the panel actions row. */
  .discover-prompt {
    display: flex;
    align-items: center;
    gap: 24px;
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(16px, 1.25cqw, 19px);
    font-weight: 900;        /* Verlag Black — matches "Select an element…" weight in reference */
    color: var(--ink);
  }
  .arrow-svg {
    /* Même flèche que la section 1 (.arrow-back-to-wheel) : chevron épais
       cyan dimensionné en em (proportionnel au texte du prompt), mais
       pointant à DROITE (vers Discover). */
    width: 2.9em;
    height: 1.21em;
    flex-shrink: 0;
    /* Léger nudge horizontal pour attirer l'œil vers le bouton Discover. */
    animation: arrowNudge 1.6s ease-in-out infinite;
  }
  /* Default-state arrow — fat left-pointing chevron pointing back toward
     the wheel. Cyan stroke, rounded caps + joins. Dimensionnée en em pour
     suivre la taille du texte du prompt (clamp 16-19px) → toujours
     proportionnée, quelle que soit la résolution. Ratio = viewBox 120:50
     (= 2.4:1) : hauteur = largeur / 2.4. */
  /* Même léger nudge que la flèche de la section 2, mais vers la GAUCHE
     (la flèche pointe vers la roue). */
  .arrow-back-to-wheel {
    width: 2.9em; height: 1.21em; flex-shrink: 0;
    animation: arrowNudgeLeft 1.6s ease-in-out infinite;
  }

  @keyframes arrowNudge {
    0%, 100% { transform: translateX(0);   }
    50%      { transform: translateX(8px); }
  }
  @keyframes arrowNudgeLeft {
    0%, 100% { transform: translateX(0);    }
    50%      { transform: translateX(-8px); }
  }

  /* State-driven visibility of action items. Using display: none/inline-flex
     (instead of just opacity) so the hidden elements don't reserve flex
     space and force the visible row to wrap onto a second line.

     DEFAULT state (no icon selected):
       SHOW : arrow-back-to-wheel + prompt text
       HIDE : Back btn, right-arrow svg, Discover btn

     PANEL-OPEN state (icon selected):
       SHOW : Back btn, prompt text, right-arrow svg, Discover btn
       HIDE : arrow-back-to-wheel */
  .panel-actions #backBtn,
  .panel-actions .arrow-svg,
  .panel-actions .btn-primary {
    display: none;
  }
  .arrow-back-to-wheel { display: inline-block; }

  .stage.panel-open .panel-actions #backBtn,
  .stage.panel-open .panel-actions .btn-primary {
    display: inline-flex;
  }
  .stage.panel-open .panel-actions .arrow-svg {
    display: inline-block;
  }
  .stage.panel-open .arrow-back-to-wheel {
    display: none;
  }
  /* In default state the prompt block is alone on the line, so flex-end
     keeps it right-aligned. In panel-open state, Back is on the left and
     the prompt+arrow+Discover group is on the right → space-between. */
  .stage.panel-open .panel-actions {
    justify-content: space-between;
  }

  /* ---------- Second-life view ---------- */
  .second-life {
    position: absolute;
    inset: 0;
    padding: 11cqh 6cqw 9cqh;
    background: #fff;
    display: flex;
    flex-direction: column;
    gap: 36px;
    opacity: 0;
    pointer-events: none;
    transition: opacity .5s ease;
  }
  .panel-side.show-secondlife .second-life { opacity: 1; pointer-events: auto; }
  .panel-side.show-secondlife .panel-main { opacity: 0; pointer-events: none; }

  .sl-mini {
    display: flex;
    align-items: center;
    gap: 22px;
    margin-bottom: 4px;
  }
  .sl-mini .mini-icon {
    width: 70px;
    height: 70px;
    border-radius: 50%;
    background: var(--cream);
    border: 2.5px solid var(--cyan);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .sl-mini .mini-icon img { width: 65%; height: 65%; object-fit: contain; filter: brightness(0); }
  .sl-mini h2 {
    color: var(--red);
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(26px, 2.6cqw, 36px);
    font-weight: 900;
    margin: 0;
    letter-spacing: 0;
  }
  .sl-text {
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(14px, 1.1cqw, 16px);
    font-weight: 300;
    line-height: 1.6;
    max-width: 540px;
  }
  .sl-photo {
    flex: 1;
    background: linear-gradient(135deg, #ddd, #f3f3f3);
    border-radius: 8px;
    position: relative;
    overflow: hidden;
    min-height: 200px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #999;
    font-size: 14px;
    font-style: italic;
  }
  .sl-photo .map-pin {
    position: absolute;
    top: 18px; right: 18px;
    width: 56px;
    height: 56px;
    background: var(--cyan);
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
    box-shadow: 0 4px 14px rgba(0,0,0,.18);
    display: flex; align-items: center; justify-content: center;
  }
  .sl-photo .map-pin svg { transform: rotate(45deg); width: 28px; height: 28px; fill: #fff; }

  /* ---------- Animations ---------- */
  @keyframes logoIn {
    0%   { transform: translate(-50%, -50%) scale(0) rotate(-20deg); opacity: 0; }
    60%  { transform: translate(-50%, -50%) scale(1.1) rotate(2deg); opacity: 1; }
    100% { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
  }

  /* Heartbeat — unified 1.5s rhythm: lub-dub finishes by ~500ms, then
     1.0s rest before the next beat ("1s between beats" per spec). */
  @keyframes logoHeartbeat {
    0%       { transform: translate(-50%, -50%) scale(1);    }
    6.67%    { transform: translate(-50%, -50%) scale(1.07); }
    13.33%   { transform: translate(-50%, -50%) scale(1);    }
    21.33%   { transform: translate(-50%, -50%) scale(1.10); }
    30%      { transform: translate(-50%, -50%) scale(1);    }
    100%     { transform: translate(-50%, -50%) scale(1);    }
  }
  /* Header */
  .topbar {
    position: absolute;
    top: 0; left: 0; right: 0;
    padding: 22px 36px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 12px;
    color: var(--gray-text);
    letter-spacing: .12em;
    z-index: 100;
    pointer-events: none;
  }
  .topbar .brand { font-weight: 700; color: var(--ink); }
  .topbar .meta { opacity: .7; }

  /* ---------- Sélecteur de langue (haut gauche) ---------- */
  .lang-switch {
    position: absolute;   /* relatif à #frame */
    top: 22px;
    left: 28px;
    z-index: 1500;
    display: flex;
    gap: 6px;
    /* Cachés pendant l'intro de la roue ; révélés EN MÊME TEMPS que le
       texte du bas (.panel-actions, sous .panel-text-in à +6,25s, délai
       .44s) puis affichés EN PERMANENCE (panel-text-in n'est jamais
       retirée), y compris pendant la révélation. Même délai/durée/easing
       que .panel-actions pour une apparition synchrone. */
    opacity: 0;
    pointer-events: none;
    transition: opacity .85s cubic-bezier(.34, 1.1, .64, 1) .44s;
  }
  .lang-btn {
    min-width: 40px;
    height: 36px;
    padding: 0 11px;
    border: 1px solid rgba(0, 171, 237, 0.35);
    border-radius: 999px;
    background: #fff;           /* fond blanc opaque, sans flou (perf vidéo) */
    color: var(--ink);
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: 13px;
    font-weight: 900;
    letter-spacing: .06em;
    cursor: pointer;
    transition: background .2s ease, color .2s ease, border-color .2s ease, transform .15s ease;
  }
  .lang-btn:hover { transform: translateY(-2px); border-color: var(--cyan); }
  .lang-btn.active {
    background: var(--cyan);
    color: #fff;
    border-color: var(--cyan);
  }
  /* Révélés en même temps que le texte du panneau (.panel-text-in, +6,25s),
     puis affichés en permanence (panel-text-in reste posée toute la session).
     :has() évite de dépendre de l'ordre DOM (le sélecteur est AVANT .stage). */
  body:has(.stage.panel-text-in) .lang-switch {
    opacity: 1;
    pointer-events: auto;
  }
  /* Masqué pendant les sections 3-4-5 (VAGUE 1 / vidéo / VAGUE 2). .in-reveal
     est posée à la section 3 et retirée à l'entrée de la section 6 (seconde-vie)
     → couvre exactement 3-4-5, pas la 2 ni la 6. Placé APRÈS la règle ci-dessus
     (même spécificité) pour gagner pendant la révélation ; réapparaît en
     section 6 et au retour Accueil. */
  body:has(.stage.in-reveal) .lang-switch {
    opacity: 0;
    pointer-events: none;
  }

  /* ---------- Home button ----------
     Discreet pill in the top-right, visible whenever the user has navigated
     away from the home (wheel) screen — i.e. when an icon is selected
     (.panel-open) or the Discover reveal is running (.discovering).
     Sits above the reveal stack (z:800) so it stays clickable during the
     video playback. */
  .home-btn {
    position: absolute;   /* relatif à #frame */
    top: 22px;
    right: 28px;
    z-index: 1500;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 9px 16px 9px 13px;
    background: #fff;           /* fond blanc opaque, sans flou (perf vidéo) */
    border: 1px solid rgba(0, 171, 237, 0.35);
    border-radius: 999px;
    color: var(--ink);
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: 11px;
    font-weight: 900;
    letter-spacing: .18em;
    text-transform: uppercase;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-6px);
    transition: opacity .35s ease,
                transform .35s cubic-bezier(.34, 1.1, .64, 1),
                background .25s ease,
                border-color .25s ease;
  }
  .home-btn svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    stroke: var(--cyan);
  }
  .home-btn:hover {
    background: #fff;
    border-color: var(--cyan);
    transform: translateY(-6px) scale(1.04);
  }
  .home-btn:active {
    transform: translateY(-5px) scale(1.0);
  }
  /* Visible à partir de la section 3 (reveal sequence : Discover cliqué)
     et au-delà — PAS en section 2 (panel-open seul, où l'utilisateur
     vient juste de sélectionner un élément et reste dans le contexte
     de la roue ; le panel propose déjà un bouton Back).
     :not(.skip-btn) car #skipBtn partage la classe .home-btn pour
     hériter du chrome (chip blanc, position haut-droit) mais a sa
     propre règle de visibilité (.stage.video-playing ~ .skip-btn) —
     sans cette exclusion, .stage.discovering ~ .home-btn le matcherait
     aussi et le ferait apparaître dès la section 3, avant la vidéo. */
  .stage.discovering ~ .home-btn:not(.skip-btn),
  .stage.viewing-secondlife ~ .home-btn:not(.skip-btn) {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
  }
  /* On the white reveal video / second-life screen, the home button keeps
     its default light treatment (cyan-on-white) — no override needed. */

  /* ----- Video progress bar -----
     Simulates a 3-second video timeline. Lives at the bottom of the
     reveal stack, becomes visible at the END of the wave reveal (when
     the video is fully exposed), then fills 0→100% over 3s on .running.
     At 100%, JS triggers the second-life full-screen view. */
  .video-progress {
    position: absolute;
    left: 8cqw;
    right: 8cqw;
    bottom: 2.4cqh;
    height: 3px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.06);
    overflow: hidden;
    /* z:1 — sits above the video frame (z:1, DOM-order later) but BELOW
       the reveal mosaic (z:2). The wave of X's therefore covers the bar
       just like it covers the video; both are revealed together as the
       mask opens. */
    z-index: 1;
    opacity: 0;
    pointer-events: none;
    transition: opacity .4s ease;
  }
  .video-progress.show {
    opacity: 0.55;          /* discreet by default */
  }
  .video-progress-fill {
    height: 100%;
    width: 0%;
    background: var(--cyan);
    /* Lighter glow than before so it doesn't pull the eye away from the video. */
    box-shadow: 0 0 6px rgba(0, 171, 237, 0.25);
    border-radius: 999px;
    transition: width 10s linear;
  }
  .video-progress.running .video-progress-fill {
    width: 100%;
  }
  /* Time-code label sitting just above the right end of the bar.
     Smaller, lighter color, no bold — keeps the whole bar quiet. */
  .video-progress-time {
    position: absolute;
    bottom: 100%;
    right: 0;
    margin-bottom: 6px;
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: 10px;
    font-weight: 300;
    letter-spacing: 0.18em;
    color: rgba(0, 0, 0, 0.42);
  }
  .video-progress-time .now { color: rgba(0, 0, 0, 0.68); margin-right: 4px; }

  /* ----- Second-life full-screen view -----
     Triggered when the simulated 3s video ends. Splits the viewport into
     two halves: TEXT (left) and IMAGE (right, full-height). Sits above
     the reveal stack but below the home button. */
  .secondlife-view {
    position: absolute;   /* relatif à #frame */
    inset: 0;
    z-index: 1200;
    display: flex;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    visibility: hidden;
    transition: opacity .6s ease, visibility 0s linear .6s;
  }
  .secondlife-view.active {
    opacity: 1;
    pointer-events: auto;
    visibility: visible;
    transition: opacity .6s ease, visibility 0s linear 0s;
  }

  .sl-left {
    flex: 1 1 50%;
    max-width: 50%;
    padding: 11cqh 6cqw 9cqh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 36px;
    /* Positioning context for .sl-next-wrap (the "Next" button anchored to
       the bottom-right of the left column — see rule below). */
    position: relative;
    /* Animates smoothly when .image-only is toggled off — collapses to
       0 width during the centered-image phase, then expands back to half. */
    transition: flex .9s cubic-bezier(.65,.05,.36,1),
                max-width .9s cubic-bezier(.65,.05,.36,1),
                padding .9s cubic-bezier(.65,.05,.36,1),
                opacity .6s ease;
  }
  /* Bloc texte (eyebrow + titre + texte + lieu). Porte l'espacement vertical.
     En cadre court/étroit il devient la zone SCROLLABLE (cf. @container), comme
     .panel-copy des sections 1-2 ; sur desktop il reste un simple bloc centré
     par .sl-left (justify-content:center). */
  .sl-copy {
    display: flex;
    flex-direction: column;
    gap: 36px;
    min-width: 0;
  }
  /* Icône du bouton Next : cachée par défaut (desktop = libellé « Next »).
     Affichée seulement en cadre court (cf. @container), où le bouton devient
     une icône — même PLAY (▶) que le bouton Discover des sections 1-2. */
  .sl-next-icon { display: none; }
  .sl-eyebrow {
    display: flex;
    align-items: center;
    gap: 14px;
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: 12px;
    font-weight: 900;
    letter-spacing: .22em;
    text-transform: uppercase;
    /* Même accent que le titre (.sl-full-title) : rouge ou bleu selon
       l'élément sélectionné, via --selected-accent. */
    color: var(--selected-accent, var(--red));
  }
  .sl-eyebrow-icon {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    /* Contour dans la couleur d'accent de l'élément (comme le titre). */
    border: 2px solid var(--selected-accent, var(--red));
    background: var(--cream);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .sl-eyebrow-icon img { width: 65%; height: 65%; object-fit: contain; filter: brightness(0); }
  .sl-full-title {
    /* Same accent driver as .panel-title — the second-life title reads
       in the selected icon's color. */
    color: var(--selected-accent, var(--red));
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(38px, 4cqw, 60px);
    font-weight: 900;
    margin: 0;
    letter-spacing: 0;
    line-height: 1.05;
    text-transform: uppercase;
  }
  .sl-full-text {
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: clamp(16px, 1.35cqw, 20px);
    font-weight: 300;
    line-height: 1.7;
    color: var(--ink);
    margin: 0;
    max-width: 560px;
  }
  .sl-location {
    display: flex;
    align-items: center;
    gap: 12px;
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-size: 14px;
    font-weight: 900;
    letter-spacing: .12em;
    color: var(--ink);
    text-transform: uppercase;
    margin-top: 8px;
  }
  .sl-location svg { width: 20px; height: 20px; fill: var(--red); flex-shrink: 0; }

  .sl-right {
    flex: 1 1 50%;
    position: relative;
    overflow: hidden;
    background: #ffffff;
    /* Animates between full-width (.image-only) and half-width (split). */
    transition: flex .9s cubic-bezier(.65,.05,.36,1);
  }

  /* Image-only mode: outro wave reveals the Keelebunn image filling the
     viewport. .sl-left is collapsed to zero, .sl-right expands to 100%.
     After the icon dissolves on top, removing .image-only animates the
     split back to its normal half-half layout. */
  .secondlife-view.image-only .sl-left {
    flex: 0 0 0;
    max-width: 0;
    padding-left: 0;
    padding-right: 0;
    opacity: 0;
    overflow: hidden;
  }
  .secondlife-view.image-only .sl-right {
    flex: 1 1 100%;
  }
  /* Couches photo du carousel — empilées, crossfade par opacité. La couche
     active porte .on (opacity 1). Couche 0 = #slRightImage (réutilisée par la
     révélation VAGUE 2 + le préchargement anti-flash). */
  /* Photos seconde-vie : `contain` → l'image ENTIÈRE est toujours visible
     (jamais de recadrage/zoom), aussi bien dans le carousel (vue split) que
     pendant la révélation plein écran (image-only, VAGUE 2). Les bandes
     (blanches, fond .sl-right) apparaissent là où le ratio de l'image diffère
     du cadre : haut/bas pour une photo paysage, côtés pour une portrait. */
  .sl-right-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: none;
    opacity: 0;
    transition: opacity .6s ease;
    z-index: 1;
  }
  .sl-right.has-image .sl-right-image { display: block; }
  .sl-right.has-image .sl-right-image.on { opacity: 1; }
  .sl-right.has-image .sl-right-placeholder { display: none; }
  .sl-right img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
  }
  /* ----- Carousel : flèches + points (photo droite, section #6) ----- */
  .sl-carousel-ui {
    position: absolute; inset: 0; z-index: 3;
    pointer-events: none; opacity: 0; transition: opacity .4s ease;
  }
  /* Visible uniquement en vue split (pas image-only) ET s'il y a plusieurs photos. */
  .secondlife-view.active:not(.image-only) .sl-right.has-carousel .sl-carousel-ui { opacity: 1; }
  .sl-car-nav {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 46px; height: 46px; border-radius: 50%;
    border: none; cursor: pointer; pointer-events: auto;
    background: rgba(0,0,0,.42); color: #fff;
    font-size: 27px; line-height: 1;
    display: flex; align-items: center; justify-content: center;
    transition: background .2s ease;
  }
  .sl-car-nav:hover { background: rgba(0,0,0,.64); }
  .sl-car-nav.prev { left: 14px; }
  .sl-car-nav.next { right: 14px; }
  .sl-car-dots {
    position: absolute; left: 0; right: 0; bottom: 16px;
    width: fit-content;             /* la pastille épouse les points… */
    max-width: calc(100% - 28px);   /* …mais reste dans le cadre (marge = flèches, 14px) */
    margin-inline: auto;            /* centrée dans la zone left:0/right:0 (largeur dispo = pleine) */
    display: flex; justify-content: center; flex-wrap: wrap; gap: 9px;
    pointer-events: auto;
    /* Pastille sombre translucide DERRIÈRE les points — même langage visuel que
       .sl-car-nav (rgba(0,0,0,.42)). Garantit la lisibilité des points blancs
       aussi bien sur la bande blanche (photo paysage en `contain`) que sur
       l'image. La pastille épouse les points (largeur auto, centrée). */
    background: rgba(0, 0, 0, .42);
    padding: 7px 12px;
    border-radius: 999px;
  }
  .sl-car-dot {
    width: 10px; height: 10px; border-radius: 50%; padding: 0;
    border: 1.6px solid #fff; background: transparent; cursor: pointer;
    box-shadow: 0 0 3px rgba(0,0,0,.5);
    transition: background .2s ease;
  }
  .sl-car-dot.on { background: #fff; }
  /* Placeholder when no real photo is wired in. Plays the same dark
     gradient as the video placeholder so the whole reveal→second-life
     transition reads as one tonal family. */
  .sl-right-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    text-align: center;
    padding: 0 6cqw;
    background: #ffffff;
    color: var(--gray-text);
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
  }
  .sl-right-placeholder .pin {
    width: 64px;
    height: 64px;
    background: var(--cyan);
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
    box-shadow: 0 8px 24px rgba(0,171,237,.18);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .sl-right-placeholder .pin svg { transform: rotate(45deg); width: 28px; height: 28px; fill: #fff; }
  .sl-right-placeholder .ph-caption {
    font-size: 14px;
    letter-spacing: .3em;
    font-weight: 900;
    color: var(--ink);
  }
  .sl-right-placeholder .ph-sub {
    font-size: 11px;
    letter-spacing: .25em;
    font-weight: 300;
    color: var(--gray-text);
  }

  /* Staggered intro for the left-side content — runs ONLY when the
     view is active AND not in image-only mode, so the stagger plays
     when the layout transitions from centered to split. */
  .secondlife-view .sl-eyebrow,
  .secondlife-view .sl-full-title,
  .secondlife-view .sl-full-text,
  .secondlife-view .sl-location {
    opacity: 0;
    transform: translateY(22px);
  }
  .secondlife-view.active:not(.image-only) .sl-eyebrow      { animation: panelChildIn .8s cubic-bezier(.34, 1.1, .64, 1) .35s forwards; }
  .secondlife-view.active:not(.image-only) .sl-full-title   { animation: panelChildIn .8s cubic-bezier(.34, 1.1, .64, 1) .50s forwards; }
  .secondlife-view.active:not(.image-only) .sl-full-text    { animation: panelChildIn .8s cubic-bezier(.34, 1.1, .64, 1) .70s forwards; }
  .secondlife-view.active:not(.image-only) .sl-location     { animation: panelChildIn .8s cubic-bezier(.34, 1.1, .64, 1) .90s forwards; }

  /* Section 6 — "Next" button that returns to the wheel (section 1).
     Anchored to the bottom-right of .sl-left, aligned on its padding
     (9vh from bottom, 6vw from right). Wrapped in a div so the wrap
     handles position+entry-animation while the .btn-primary inside keeps
     its hover transform intact (no conflict). Fades in last in the
     stagger cascade (1.10s, after .sl-location at .90s). */
  .secondlife-view .sl-next-wrap {
    position: absolute;
    bottom: 9cqh;
    right: 6cqw;
    opacity: 0;
    transform: translateY(22px);
    /* Hidden during the .image-only phase (panel collapsed to 0 width)
       and on initial mount — opacity flip via the cascade rule below. */
  }
  .secondlife-view.active:not(.image-only) .sl-next-wrap {
    animation: panelChildIn .8s cubic-bezier(.34, 1.1, .64, 1) 1.10s forwards;
  }
  /* Right-side image fades + scales in subtly */
  .secondlife-view .sl-right {
    opacity: 0;
    transform: scale(1.04);
    transition: opacity .8s ease, transform 1.1s cubic-bezier(.65,.05,.36,1);
  }
  .secondlife-view.active .sl-right {
    opacity: 1;
    transform: scale(1);
    transition-delay: .1s;
  }

  /* Home button must remain visible on the second-life screen too. */
  /* Transition image — shown after the video ends, before the outro
     heartbeat sequence. Same z-level as the video frame, but DOM-after
     it so it draws on top during the fade-in. Slower fade (1.5s) for
     a deliberate "image appears in fade" feel + matches the symmetric
     fade-OUT to black during the outro cross-fade. */
  .reveal-transition-frame {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    background: transparent;
    opacity: 0;
    pointer-events: none;
    transition: opacity 1.5s ease;
    /* Mask used during the Vague 2 reveal: a transparent disc grows
     from center, punching a hole through the image to reveal the
     second-life view behind. Same mechanism as the mosaic. */
    --reveal-r: 0%;
  }
  .reveal-transition-frame.mask-open {
    -webkit-mask-image: radial-gradient(circle at center,
      transparent calc(var(--reveal-r)),
      black calc(var(--reveal-r) + 1%));
    mask-image: radial-gradient(circle at center,
      transparent calc(var(--reveal-r)),
      black calc(var(--reveal-r) + 1%));
    transition: --reveal-r 2.4s linear, opacity 1.5s ease;
  }
  .reveal-stack.show-transition .reveal-transition-frame {
    opacity: 1;
  }
  /* DIM mode: the transition image stays at FULL opacity but gets a
     brightness filter applied so it looks like the photo went into
     shadow — not a transparent fade. Used at T=3.9s of the outro: the
     image visibly darkens in place to become the wave background. */
  .reveal-stack.show-transition.dim-transition .reveal-transition-frame {
    opacity: 1;
  }
  .reveal-stack.show-transition.dim-transition .reveal-transition-frame img {
    filter: brightness(0.35);
    transition: filter 1.5s ease;
  }
  .reveal-transition-frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: filter 1.5s ease;
  }

  /* Reveal-doki BEHIND the icon for the outro pre-wave moment. The icon
     stays at z:10; doki drops to z:9 so the user sees doki letters peeking
     around the icon's cyan ring (matches the user's spec). */
  .reveal-doki.behind-icon {
    z-index: 9;
  }

  /* Second-life view BEHIND the reveal stack — used during the outro
     wave so the mask opening uncovers the second-life content. Combined
     with .active so children's entry animations still play. */
  .secondlife-view.behind {
    z-index: 700;
    pointer-events: none;
  }

  /* Sur l'écran seconde-vie (section 6), AUCUN résidu d'icône ne doit rester
     visible : ni le clone du reveal-stack (#revealIcon, laissé parfois en
     opacity 1 par l'outro), ni la tête d'épingle centrale (centerDisplay).
     Belt-and-braces indépendant du timing JS — ne s'applique QUE quand
     viewing-secondlife est posé (T=14s, outro terminé). !important pour
     battre .reveal-stack.active .reveal-icon. La transition Next retire
     viewing-secondlife dès le départ → ces règles ne gênent pas le rejeu. */
  .stage.viewing-secondlife ~ .reveal-stack {
    opacity: 0 !important;
    visibility: hidden !important;
  }
  .stage.viewing-secondlife .center-display {
    opacity: 0 !important;
  }

  /* Skip button: same chrome as the home button, positioned just to the
     left of it. Only visible while a video is actively playing (JS toggles
     `.stage.video-playing`). The triangle + bar inside is sized small
     so it reads as an icon, not a CTA. */
  .skip-btn {
    right: 175px;
  }
  .stage.video-playing ~ .skip-btn {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
  }
  .skip-btn svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
  }

  /* Helper text bottom */
  .hint {
    position: absolute;
    bottom: 26px; left: 50%;
    transform: translateX(-50%);
    font-size: 12px;
    letter-spacing: .15em;
    color: var(--gray-text);
    opacity: 0;
    transition: opacity .5s ease;
  }
  .stage.labels-on:not(.panel-open) .hint { opacity: 1; }

  @media (max-width: 800px) and (orientation: portrait) {
    .stage { flex-direction: column; }
    .panel-side { width: 100%; height: 60%; top: auto; bottom: 0; transform: translateY(100%); padding: 30px 24px; }
    .stage.panel-open .panel-side { transform: translateY(0); }
    .wheel-side { flex-basis: 100% !important; }
    .stage.panel-open .wheel-side { flex-basis: 40% !important; }
  }

  /* ---------- Overlay "tournez en paysage" (viewport portrait) ----------
     Intégré depuis le handoff LUX03 (orientation.js + rotate-page). Affiché
     UNIQUEMENT en portrait (mobile / fenêtre étroite) via JS ; masqué en
     paysage. Icône = SVG inline (pas de dépendance CDN, marche offline). */
  .rotate-overlay {
    position: fixed;
    inset: 0;
    z-index: 3000;                 /* au-dessus de tout (lang-switch/home/reveal/secondlife) */
    display: none;                 /* affiché (flex) par JS seulement en portrait */
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 3.5vmin;
    background: var(--cream);
    color: var(--ink);
    text-align: center;
    padding: 6vmin;
    box-sizing: border-box;
    overflow: hidden;
  }
  .rotate-overlay .rotate-logo {
    width: clamp(88px, 19vmin, 165px);
    height: auto;
    transform-origin: center;
    animation: rotateOverlayBeat 1.5s ease-in-out infinite;   /* battement lub-dub unifié 1,5 s */
  }
  @keyframes rotateOverlayBeat {
    0%     { transform: scale(1.00); }
    6.67%  { transform: scale(1.15); }   /* lub */
    14.67% { transform: scale(0.98); }
    22.67% { transform: scale(1.19); }   /* dub */
    33.33% { transform: scale(1.00); }
    100%   { transform: scale(1.00); }
  }
  .rotate-overlay .rotate-phone {
    width: clamp(62px, 14vmin, 110px);
    height: auto;
    color: var(--ink);
  }
  .rotate-overlay .rotate-phone .phone-g {
    transform-box: fill-box;       /* origine = centre du téléphone, pas du viewBox */
    transform-origin: center;
    animation: rotateHint 2.6s ease-in-out infinite;
  }
  /* Le téléphone bascule du portrait au paysage et revient — un "tourne-moi". */
  @keyframes rotateHint {
    0%, 18%  { transform: rotate(0deg);   }
    55%, 73% { transform: rotate(-90deg); }
    100%     { transform: rotate(0deg);   }
  }
  .rotate-overlay .rotate-text {
    font-family: 'Verlag', 'Helvetica Neue', Arial, sans-serif;
    font-weight: 300;
    font-size: clamp(14px, 2.6vmin, 20px);
    line-height: 1.55;
    opacity: .85;
    max-width: 80vw;
  }
  /* Respecte "réduire les animations" : on fige logo + téléphone. */
  @media (prefers-reduced-motion: reduce) {
    .rotate-overlay .rotate-logo,
    .rotate-overlay .rotate-phone .phone-g { animation: none; }
  }

  /* ===================================================================
     PANNEAU COMPACT — cadre COURT (≤ 700 px de haut) OU ÉTROIT (ratio ≤ 17/10).
     Ciblé via CONTAINER QUERY sur #frame (container-type:size, l.110) :
     suit la taille du CADRE, pas du viewport → robuste en letterbox.
     Le panneau desktop (centré, sans scroll, boutons texte) ne loge la longue
     desc client (ex. Keelebunn) QUE si le cadre est assez HAUT : mesuré, ça
     rentre à 768 px mais déborde dès ~616 px (titre poussé hors champ + actions
     chevauchées). On bascule donc en compact (panneau scrollable + actions en
     icônes) dès que :
       • le cadre est COURT (≤ 700 px) — quel que soit le ratio. Couvre les
         cibles Vuplex Samsung Tab S4 (853×616) ET iPhone 11 (979×494,
         ratio 1.982 = LARGE mais court → non capté par le ratio seul) ;
       • OU le cadre est ÉTROIT (ratio ≤ 17/10) — quelle que soit la hauteur
         (panneau étroit = desc qui s'allonge ; idem labels masqués l.2232).
     N'affecte PAS le 16:9 desktop (≥ 768 px de haut ET ratio > 1.7).
     ------------------------------------------------------------------- */
  @container (max-height: 700px) or (max-aspect-ratio: 17 / 10) {

    /* --- Correctif 1 : débordement du panneau (texte coupé en paysage) ---
       Le panneau occupait 610 px dans un cadre de 390 px (overflow visible).
       On resserre les espacements (paddings cqh + marges fixes du titre/actions
       + line-height) et on autorise le scroll en filet pour les textes longs
       (descriptions client). .second-life est absolute/inset:0 → seul
       .panel-main (en flux) scrolle. */
    .panel-side,
    .second-life {
      /* Plus d'air EN HAUT pour que le titre de couleur ne colle pas au bord
         du cadre court (était 4cqh → titre à ~11% de la hauteur). 8cqh en
         haut (~15%), bas resserré à 4cqh : la desc scrolle (.panel-copy) et
         les actions restent ancrées, donc ce padding haut ne pousse rien
         hors cadre. */
      padding: 8cqh 6cqw 4cqh;
    }
    .second-life { overflow-y: auto; }
    /* En-tête/desc scrollables (.panel-copy, flex-basis:0 -> prend EXACTEMENT
       l'espace restant) + actions ancrées en bas (flex-shrink:0). La desc
       scrolle dans sa zone, les actions ne sont jamais chevauchées ni poussées
       hors cadre. */
    .panel-side { overflow: hidden; }
    .panel-copy {
      flex: 1 1 0;
      min-height: 0;
      overflow-y: auto;
      /* Centré verticalement quand le contenu RENTRE (ex. ratios 4:3 / 5:4 /
         16:10, hauts mais ≤ 17/10 → uniforme avec le 16:9 centré) ; `safe`
         bascule en alignement HAUT (donc scrollable sans rognage) quand le
         texte déborde (petits cadres courts). */
      justify-content: safe center;
    }
    .panel-title {
      font-size: clamp(22px, 6cqh, 32px);
      margin: 0 0 2.5cqh;
    }
    .panel-desc {
      font-size: 15px;
      line-height: 1.4;
    }
    .panel-actions {
      margin-top: 3cqh;
      flex-shrink: 0;          /* ancré en bas, jamais compressé ni recouvert */
    }

    /* --- Correctif 2 : zones tactiles Home / Skip ≥ 44 px ---
       Mesuré en paysage mobile : Home/Skip = 35 px (sous le seuil 44 px). */
    /* Langue : on NE gonfle PAS à 44 px ici — sur un cadre très court
       (Oppo Vuplex 293 px) la rangée déborderait sur le haut de la roue.
       On la garde compacte (plus petite que le desktop 36 px) : 30 px de
       haut, padding/min-width/font resserrés. */
    .lang-btn {
      height: 30px;
      min-width: 30px;
      padding: 0 8px;
      font-size: 12px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    /* --- Home / Skip : alignés sur le gabarit COMPACT des boutons de langue ---
       Avant : gonflés à 44px (zone tactile) → plus gros que les lang-btn (30px),
       incohérent. Désormais MÊME gabarit que .lang-btn : 30px de haut, padding /
       police / icône resserrés, pour un rendu homogène en haut du cadre court. */
    .home-btn {            /* couvre aussi #skipBtn (class="home-btn skip-btn") */
      min-height: 0;
      height: 30px;
      padding: 0 13px;
      gap: 6px;
      font-size: 12px;
    }
    .home-btn svg,
    .skip-btn svg {
      width: 12px;
      height: 12px;
    }

    /* --- Section 6 (seconde-vie) : même principe que les sections 1-2 ---
       Le bloc texte .sl-copy (eyebrow + titre + texte + lieu) devient la zone
       SCROLLABLE (comme .panel-copy) : titre + texte scrollent ensemble.
       eyebrow ancré en haut, Next ancré en bas. Le Next passe EN FLUX
       (bas-droite) et se transforme en ICÔNE flèche, comme Back/Discover. */
    .secondlife-view .sl-left {
      justify-content: flex-start;
      /* padding-haut min 58px : dégage les boutons de langue (haut-gauche,
         bas ≈ 52px) qui se trouvent au-dessus de la colonne de gauche en
         section 6 → plus de chevauchement quand le contenu est aligné en haut
         (cas scroll). */
      padding: max(58px, 8cqh) 6cqw 4cqh;
      gap: clamp(8px, 2cqh, 16px);
    }
    .secondlife-view .sl-copy {
      flex: 1 1 0;
      min-height: 0;
      overflow-y: auto;
      gap: clamp(10px, 2.4cqh, 24px);
      /* Centré verticalement quand le contenu RENTRE ; `safe` bascule en
         alignement haut (donc scrollable sans rognage) quand il déborde. */
      justify-content: safe center;
    }
    .secondlife-view .sl-full-title { font-size: clamp(24px, 6cqh, 40px); line-height: 1.05; }
    .secondlife-view .sl-full-text  { font-size: clamp(13px, 3.4cqh, 18px); line-height: 1.45; }
    .secondlife-view .sl-eyebrow     { font-size: 11px; gap: 10px; }
    .secondlife-view .sl-eyebrow-icon { width: 28px; height: 28px; }
    .secondlife-view .sl-location    { font-size: 12px; margin-top: 0; }
    /* Next ancré en bas-droite EN FLUX (plus d'absolu). Le passage du libellé
       « Next » en ICÔNE PLAY ne vit PAS ici mais dans le bloc max-width:960
       plus bas (comme Back/Discover) → garde le TEXTE « Next » sur les écrans
       larges mais hauts (iPad 4:3 1024, laptop 16:10 1440). */
    .secondlife-view .sl-next-wrap {
      position: static;
      align-self: flex-end;
      flex-shrink: 0;
      bottom: auto;
      right: auto;
    }

    /* --- Correctif 4 : prompt + flèche FLUIDES (responsive) ---
       Hors cadre court, le prompt = clamp(16-19px, 1.25cqw) : bien sur desktop,
       mais il PLANCHE à 16px en mobile (ne descend jamais). On le fait scaler
       avec la HAUTEUR du cadre (cqh) : ~13px @360px (petit Android paysage)
       -> 16px @480px (raccord continu avec le desktop). La flèche (.arrow-*,
       dimensionnée en em) suit automatiquement. Pas de palier : 100% fluide. */
    .discover-prompt {
      font-size: clamp(13px, 3.6cqh, 16px);
      gap: 12px;                 /* resserre flèche / texte / Discover (24 -> 12px) */
    }

    /* La taille compacte des boutons + leur passage en ICÔNES vit désormais
       dans un bloc séparé piloté par la LARGEUR (max-width) — voir plus bas —
       pour ne PAS iconifier Back/Discover sur les écrans larges mais hauts
       (laptop 16:10, iPad 4:3, 5:4) qui ont la place pour le texte. */
  }

  /* === Back / Discover : compacts + ICÔNES — UNIQUEMENT sur cadre ÉTROIT ===
     Le passage texte→icône ne doit se faire que quand le PANNEAU (50% du cadre)
     est trop étroit pour les libellés : mobile + Vuplex/Unity. Les écrans
     LARGES mais hauts (laptop 16:10 1440, iPad 4:3 1024, 5:4 1280) ont la place
     → ils gardent les boutons TEXTE comme le 16:9. D'où un seuil de LARGEUR du
     cadre (max-width) au lieu du ratio :
       • ≤ 960px → icônes (Vuplex S4 853 / iPad Air 834, mobile ≤ 932…)
       • > 960px → texte (iPad 4:3 1024, laptop, desktop).
     N'affecte donc PAS les formats mobile / Vuplex (tous ≤ 932px de large). */
  @container (max-width: 960px) {
    .btn,
    .btn-primary {
      font-size: clamp(12px, 3.2cqh, 15px);
      padding: 0.55em 1.4em;
      min-height: 44px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    .panel-actions { gap: 12px; }

    /* Layout panel-open : Back (icône) · prompt (centre) · Discover (icône) sur
       UNE ligne. display:contents éclate .discover-prompt en frères flex. */
    .stage.panel-open .panel-actions {
      flex-wrap: nowrap;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }
    .stage.panel-open .discover-prompt { display: contents; }
    .stage.panel-open #discoverPrompt {
      order: 2;
      flex: 1;
      min-width: 0;             /* autorise le wrap du texte en contexte flex */
      text-align: center;
      font-size: clamp(13px, 3.6cqh, 16px);
    }
    .stage.panel-open #backBtn     { order: 1; }
    .stage.panel-open #discoverBtn { order: 3; }
    .stage.panel-open .panel-actions .arrow-svg { display: none; }

    /* Back + Discover en icônes carrées (libellé masqué, gardé pour l'a11y) */
    .stage.panel-open #backBtn,
    .stage.panel-open #discoverBtn {
      font-size: 0;
      width: 44px;
      min-width: 44px;
      padding: 0;
      flex-shrink: 0;
    }
    .stage.panel-open #backBtn::before,
    .stage.panel-open #discoverBtn::before {
      content: '';
      display: block;
      width: 22px;
      height: 22px;
      background-color: currentColor;          /* ink sur Back, blanc sur Discover */
      -webkit-mask: var(--btn-ic) center / 22px no-repeat;
      mask: var(--btn-ic) center / 22px no-repeat;
    }
    .stage.panel-open #backBtn {
      --btn-ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z'/%3E%3C/svg%3E");
    }
    .stage.panel-open #discoverBtn {
      --btn-ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
    }

    /* Section 6 — bouton Next : MÊME logique que Back/Discover. Le libellé
       « Next » passe en ICÔNE PLAY (▶, le SVG .sl-next-icon du HTML) UNIQUEMENT
       sur cadre étroit (≤ 960px : mobile + Vuplex/Unity). Sur iPad 4:3 1024 /
       laptop 16:10 1440 (> 960px) le bouton garde son TEXTE « Next ».
       La position en flux (bas-droite) reste pilotée par .sl-next-wrap au-dessus. */
    .secondlife-view #slNextBtn {
      font-size: 0;                /* masque le libellé « Next » */
      width: 44px;
      min-width: 44px;
      height: 44px;
      min-height: 44px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    .secondlife-view #slNextBtn .sl-next-icon {
      display: block;
      width: 22px;
      height: 22px;
    }
  }

  /* ===================================================================
     ROUE — labels masqués (icônes seules) quand le cadre est trop étroit pour
     les loger : soit COURT (mobile paysage, max-height 480px), soit au RATIO
     < ~16:9 (4:3 / 5:4 / 16:10 → demi-cadre trop étroit, labels coupés à gauche
     + chevauchent le panneau à droite). Sur 16:9 et plus large, labels visibles.
     Le nom s'affiche dans le panneau au clic ; labels gardés pour les lecteurs
     d'écran (sr-only, pas display:none).
     ------------------------------------------------------------------- */
  @container (max-height: 480px) or (max-aspect-ratio: 17 / 10) {
    .icon-label {
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
      clip-path: inset(50%);
      white-space: nowrap;
    }
  }

  /* ===================================================================
     ROUE 16:9 large (labels VISIBLES) — anti-débordement des libellés.
     Problème : à 58cqh la roue + ses libellés ne tenaient pas dans la moitié
     gauche du cadre → libellés cardinaux coupés à GAUCHE (ex. "Exterior
     Panels" / DE "Außenpaneele") et débordant SOUS LE PANNEAU à droite
     (ex. "Audiovisual equipment" / DE "Audiovisuelle Ausstattung"), dans
     TOUTES les langues. Les longs mots non sécables (DE "Fundamentblöcke",
     "Audiovisuelle") interdisent de jouer sur max-width seule.
     Solution équilibrée : resserrer le "nuage" → roue plus compacte
     (47cqh au lieu de 58 ; ~−19%) + police de libellé un cran plus basse
     (clamp cap 13 / 0.73cqw au lieu de 15 / 0.85cqw → ~10px @1366, 13px @1920).
     Mesuré : marges ~20px+ des deux côtés en EN ET DE, à 1366 ET 1920.
     Overrider --wheel-size suffit : --r-px/--icon-half/--label-clear* en
     dérivent (var()) et se recalculent.
     SCOPE = complément EXACT du breakpoint labels-masqués ci-dessus
     (min-aspect 17/10 + min-height 481) → mobile / Vuplex / cadre court
     STRICTEMENT inchangés. Bloc isolé = revert facile. */
  @container (min-aspect-ratio: 17 / 10) and (min-height: 481px) {
    .wheel {
      --wheel-size: min(47cqh, 46cqw, 560px);
    }
    .icon-label {
      font-size: clamp(10px, 0.73cqw, 13px);
    }
  }
