/* ═══════════════════════════════════════════════════════════════════════════
   DECK.CSS — « Le Système Opera » : le site présenté comme un plan 3D.

   Principe (façon Prezi / la scène d'Iron Man 2 où Tony inspecte les éléments) :
   ce ne sont PAS des atomes décoratifs qu'on regarde — ce sont LES PAGES du
   site elles-mêmes, posées à des profondeurs différentes dans un même espace.
   La page courante arrive au premier plan, presque plein écran, à sa taille
   réelle ; les autres restent visibles derrière, plus petites, plus sombres et
   légèrement floues. Naviguer, c'est déplacer la caméra d'une page à l'autre.

   Trois couches, du fond vers l'avant :
     1. #opera-canvas — la scène WebGL (réacteur + poussière), très en retrait :
        c'est l'ATMOSPHÈRE, jamais le sujet.
     2. #deck / #world — le plan 3D des pages (CSS 3D, `transform-style`).
        Du vrai HTML : le texte reste sélectionnable, accessible, indexable.
     3. .nav-side — le volet de navigation flottant, hors du plan 3D.

   Noir & blanc strict (DA Operadata) : aucune couleur, seulement des
   transparences de blanc sur du noir.
══════════════════════════════════════════════════════════════════════════════ */

:root {
  --glass:      rgba(10, 11, 13, .92);   /* légèrement translucide : le décor
                                            (sphère visée, grillages) se devine
                                            derrière la page, cf. 18e passe */
  --edge:       rgba(255, 255, 255, 0.12);
  --edge-soft:  rgba(255, 255, 255, 0.06);
  --ink:        #ffffff;
  --ink-mid:    rgba(255, 255, 255, 0.64);
  --pad-side:   18px;
  --rail-w:     236px;

  /* Taille d'une page au premier plan : « presque plein écran ». */
  /* Taille des plans : +10 % (demande Nicolas 09/09/2026). */
  /* La fenêtre laisse une bande libre de chaque côté ET en haut/bas : c'est là
     que se lit la sphère qui l'entoure (09/09/2026, demande de Nicolas — « on
     ne peut pas les voir si elles sont concentrées derrière »). */
  --slide-w: min(1780px, calc(100vw - var(--rail-w) - 230px));
  --slide-h: min(1300px, 90vh);
}

/* ── 0) Fond ────────────────────────────────────────────────────────────── */
html, body { background: #000; height: 100%; overflow: hidden; }
body { color: var(--ink); }

/* 16/09/2026, demande Nicolas : la main partout où c'est cliquable, comme sur
   le volet de navigation (qui l'avait déjà). Filet générique par rôle plutôt
   qu'un audit élément par élément — un <button> n'a PAS la main par défaut
   dans la plupart des navigateurs (contrairement à <a href>), d'où le trou. */
button, [role="button"], [onclick] { cursor: pointer; }

#opera-canvas {
  position: fixed; inset: 0; width: 100%; height: 100%;
  display: block; z-index: 0; background: #000;
  transition: opacity .5s ease;
}
/* 11/09/2026, demande Nicolas : la sphère de décor derrière Opera (page 1.1)
   ne s'est jamais alignée à sa satisfaction malgré plusieurs correctifs —
   « supprime-la, c'est plus simple ». Le décor 3D est donc masqué UNIQUEMENT
   sur la page d'arrivée (même garde que le voile ci-dessus) : Opera (son SVG
   `.hero-orb`, inchangé) reste seule à l'écran, et le décor revient dès
   qu'on la traverse — inchangé sur les 12 autres pages du parcours. */
body.at-hero #opera-canvas { opacity: 0; }
#opera-canvas.no-webgl {
  background:
    radial-gradient(46% 52% at 50% 46%, rgba(255,255,255,.10) 0%, rgba(255,255,255,.03) 40%, transparent 74%),
    radial-gradient(90% 90% at 50% 50%, #08080a 0%, #000 72%);
}
/* Voile léger : l'atmosphère ne doit jamais concurrencer la page lue. */
#opera-veil {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(130% 110% at 50% 50%, rgba(0,0,0,0) 0%, rgba(0,0,0,.28) 100%);
  transition: opacity .5s ease;
}
/* Sur la page d'arrivée, le décor s'assombrit : la sphère d'Opera et sa phrase
   doivent être seules à l'écran. Il revient dès qu'on la traverse. */
/* 16/09/2026, demande Nicolas : la bulle Opera doit rester visible sur la
   page d'arrivée aussi (elle faisait jusqu'ici doublon avec la grande
   sphère, cf. commentaire ci-dessus, désormais retiré). */
#opera-fab { transition: opacity .4s ease; }
/* 10/09/2026, Nicolas : sur le Simulateur et la Démo, seul l'assistant DE LA PAGE
   reste — celui du site (et son bouton Support) disparaissent, ils faisaient
   doublon par-dessus la fenêtre. */
body.at-app #opera-fab,
body.at-app #opera-tip,
body.at-app #opera-panel,
body.at-app #opera-call,
body.at-app #support-fab,
body.at-app #support-panel { display: none !important; }
body.at-hero #opera-veil {
  background: radial-gradient(130% 110% at 50% 50%, rgba(0,0,0,.88) 0%, rgba(0,0,0,.97) 100%);
}

/* La coquille d'origine (header + colonne défilante) n'a plus de rôle :
   le contenu vit désormais dans le plan 3D. */
header, .body-wrap > main, #app > .mob-overlay { }
header { display: none !important; }
#app { position: static; }

/* ── 1) Le plan 3D ──────────────────────────────────────────────────────── */
#deck {
  position: fixed; inset: 0; z-index: 5;
  perspective: 1600px;
  perspective-origin: 50% 50%;
  overflow: hidden;
  /* Le plan est décalé vers la droite du volet de navigation pour que la page
     courante soit centrée dans l'espace réellement disponible. */
}

#world {
  position: absolute;
  /* Le plan est recentré sur l'espace réellement disponible, à droite du volet. */
  left: calc(var(--rail-w) + var(--pad-side) * 2); right: 0; top: 0; bottom: 0;
  transition: left .4s cubic-bezier(.22,1,.36,1);
  transform-style: preserve-3d;
  transform-origin: 50% 50%;
}
#deck.nav-hidden #world { left: 0; }

/* Une page = une dalle posée dans l'espace. */
.slide {
  position: absolute;
  left: 50%; top: 50%;
  width: var(--slide-w);
  height: var(--slide-h);
  margin-left: calc(var(--slide-w) / -2);
  margin-top: calc(var(--slide-h) / -2);
  transform-style: preserve-3d;
  background: var(--glass);
  border: 1px solid var(--edge);
  border-radius: 22px;
  box-shadow: 0 40px 120px rgba(0,0,0,.75), inset 0 1px 0 rgba(255,255,255,.05);
  overflow: hidden;
  pointer-events: none;            /* seule la page courante est cliquable */
  contain: layout paint;
}
.slide.is-current { pointer-events: auto; }
.slide.is-hidden  { visibility: hidden; }

/* Liseré lumineux : c'est ce qui donne l'aspect « élément en lévitation »
   quand la dalle est vue de loin, où le texte n'est plus lisible. */
.slide::after {
  content: ''; position: absolute; inset: -1px; border-radius: 22px;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.16), 0 0 60px rgba(255,255,255,.05);
}

.slide-body {
  position: absolute; inset: 0;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.16) transparent;
}
.slide-body::-webkit-scrollbar { width: 4px; }
.slide-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.16); border-radius: 4px; }

/* ── 2) Le contenu d'une page d'accueil dans sa dalle ───────────────────── */
/* Les sections gardent leur HTML d'origine : on ne fait que les remettre à
   l'échelle d'une dalle (elles étaient dessinées pour une page pleine). */
.slide .section {
  background: transparent !important;
  color: var(--ink);
  min-height: 100%;
  padding: clamp(28px, 4vh, 56px) clamp(24px, 3.4vw, 60px);
  display: flex; flex-direction: column; justify-content: center;
}
.slide .section-inner {
  max-width: 1000px !important; width: 100%;
  margin: 0 auto;
  background: none; border: 0; box-shadow: none; padding: 0;
}

/* Les animations « d'apparition au scroll » de la page d'origine reposaient sur
   un IntersectionObserver calé sur la fenêtre. Dans le plan 3D, une dalle
   lointaine n'intersecte plus rien de façon fiable : le contenu resterait
   invisible. Le fondu d'arrivée est donc neutralisé ici. */
.slide .reveal { opacity: 1 !important; transform: none !important; }

/* Grilles : dessinées pour 1200 px de large, la dalle en fait ~1100 — elles
   passent donc telles quelles, sauf les 4 colonnes d'étapes, trop serrées. */
.slide .steps-grid { grid-template-columns: 1fr 1fr !important; gap: 30px 44px !important; }
.slide .step + .step { border-left: 0 !important; }
.slide .step { padding: 0 !important; }

/* Typographie & composants sur fond sombre */
.slide .hero-title, .slide .section-title { color: #fff; }
.slide .section-label { color: rgba(255,255,255,.42); }
.slide .section-sub, .slide .feature-text,
.slide .persona-text, .slide .eq-desc { color: var(--ink-mid); }
.slide .btn-dark { background: #fff !important; color: #000 !important; }
/* .js-hover : doublon de :hover posé par deck.js — :hover ne se déclenche
   jamais nativement sur un bouton/lien de dalle (contexte preserve-3d). */
.slide .btn-dark:hover, .slide .btn-dark.js-hover { background: rgba(255,255,255,.82) !important; }
.slide .btn-outline { border-color: rgba(255,255,255,.45) !important; color: #fff !important; }
.slide .btn-outline:hover, .slide .btn-outline.js-hover { background: #fff !important; color: #000 !important; }
.slide .pain-grid { background: rgba(255,255,255,.09); }
.slide .pain-card { background: rgba(255,255,255,.035); }
.slide .pain-number { color: rgba(255,255,255,.09); }
.slide .personas-grid { background: rgba(255,255,255,.10) !important; }
.slide .persona-card { background: #101114 !important; border: 0 !important; }
.slide .persona-card:nth-child(2) { background: #16181c !important; }
.slide .persona-card:nth-child(3) { background: #1c1f24 !important; }
.slide .persona-role { color: #fff; opacity: .55 !important; }
.slide .persona-name { color: #fff; }
.slide .persona-text { color: rgba(255,255,255,.72) !important; opacity: 1 !important; }
.slide .persona-list li { color: rgba(255,255,255,.78); opacity: 1 !important; }
.slide .persona-list li::before { color: rgba(255,255,255,.5); }
.slide .feature-card { border-top-color: var(--edge) !important; }
.slide .feature-icon { filter: none !important; opacity: .8; }
.slide .form-input, .slide .form-textarea {
  background: rgba(255,255,255,.06); border: 1px solid var(--edge);
  color: #fff; border-radius: 12px;
}
.slide .form-input::placeholder, .slide .form-textarea::placeholder { color: rgba(255,255,255,.3); }
.slide .form-input:focus, .slide .form-textarea:focus {
  border-color: rgba(255,255,255,.55); outline: none; background: rgba(255,255,255,.09);
}
.slide .form-label { color: rgba(255,255,255,.72); }
.slide .contact-icon { background: rgba(255,255,255,.1); }
.slide .contact-text { color: var(--ink-mid); }
.slide #submit-btn { background: #fff !important; color: #000 !important; }
/* 16/09/2026, demande Nicolas : même effet de surbrillance que
   « Demandez-nous une démonstration » — cette règle ID (`#submit-btn`,
   plus spécifique que `.btn-dark.js-hover` même avec `!important` des deux
   côtés) n'avait pas son propre doublon `:hover`/`.js-hover`, donc rien ne
   pouvait jamais l'emporter sur son fond blanc figé, même en survolant. */
.slide #submit-btn:hover, .slide #submit-btn.js-hover { background: rgba(255,255,255,.82) !important; }
/* Photos en niveaux de gris : la DA Operadata est strictement monochrome. */
.slide img:not([src$=".svg"]), .slide video { filter: grayscale(1) contrast(1.04) brightness(.95); }
.slide .dashboard-showcase { grid-template-columns: 1fr 1fr !important; gap: 34px !important; }
.slide .dashboard-showcase-media img { max-height: 44vh !important; border-radius: 14px; }

/* ── 3) Les dalles « application » (Simulateur, Démo, Juridique) ────────── */
/* Ce sont les VRAIES pages du site, chargées telles quelles dans la dalle.
   Rien n'est réécrit : la page de production est affichée à l'identique. */
.slide-frame .slide-body { overflow: hidden; }

/* ⚠️ Les fenêtres NE VIVENT PAS dans le plan 3D (09/09/2026).
   Chromium ne teste plus le survol d'une iframe placée dans un contexte
   `preserve-3d` + `perspective` : la boîte est au bon endroit à l'écran, mais
   tout clic atterrit sur le document parent — d'où « on ne peut pas cliquer
   sur Suivant ». On les pose donc à plat dans cette couche, calée au pixel
   près sur le rectangle de leur dalle (qui, une fois la caméra arrivée, est un
   simple rectangle de face). La dalle, elle, ne garde que son aperçu. */
#app-layer {
  position: fixed; inset: 0; z-index: 6;
  pointer-events: none;
}
/* En plein écran la fenêtre remplit la couche, qui commence déjà sous la
   rangée de boutons sur téléphone — donc rien n'est jamais rogné en haut. */
.app-frame {
  position: absolute;
  border-radius: 22px; overflow: hidden;
  background: #fff;
  pointer-events: auto;
  display: none;
  box-shadow: 0 40px 120px rgba(0,0,0,.75);
}
.app-frame.on { display: block; }
/* Plein écran « maison » (l'API Fullscreen ne s'applique pas à un bloc sur
   Safari iOS) : la fenêtre prend tout le cadre, quel que soit l'appareil. */
.app-frame.is-max {
  left: 0 !important; top: 0 !important;
  width: 100% !important; height: 100% !important;
  border-radius: 0;
}
.app-full {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  width: 30px; height: 30px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff; cursor: pointer;
  background: rgba(0,0,0,.62); border: 1px solid rgba(255,255,255,.22);
  border-radius: 9px;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  opacity: .55; transition: opacity .18s ease, background .18s ease;
}
.app-full:hover { opacity: 1; background: rgba(0,0,0,.8); }
/* Repli seulement : sur ordinateur, le bouton vit désormais DANS la rangée
   d'icônes de la page embarquée (cf. deck.js/injecterBoutonPlein) et cette
   pastille est alors masquée. */
.app-frame.is-max .app-full { opacity: 1; }
/* En plein écran, la couche des fenêtres passe DEVANT le volet de navigation
   du site (z-index 40), le rail des pages et les boutons de réouverture — le
   volet reste là, simplement en arrière-plan (09/09/2026, demande Nicolas). */
body.app-max #app-layer { z-index: 60; }
.app-frame iframe {
  width: 100%; height: 100%; border: 0; display: block; background: #fff;
}
/* Tant que la dalle est loin, on n'ouvre pas la page : on montre son titre.
   (Une page lointaine n'est de toute façon pas lisible — inutile de la charger.) */
.slide-stub {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; text-align: center; padding: 40px;
  background:
    radial-gradient(70% 60% at 50% 45%, rgba(255,255,255,.06) 0%, transparent 70%),
    repeating-linear-gradient(0deg, rgba(255,255,255,.028) 0 1px, transparent 1px 46px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.028) 0 1px, transparent 1px 46px);
}
.slide-stub-k {
  font-family: 'Inter', sans-serif; font-size: 10px; letter-spacing: 3px;
  text-transform: uppercase; color: rgba(255,255,255,.34);
}
.slide-stub-t {
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: clamp(22px, 3vw, 40px); color: #fff; letter-spacing: -.5px;
}
.slide-stub-s { font-family: 'Inter', sans-serif; font-size: 13px; color: rgba(255,255,255,.45); max-width: 460px; }

/* Les fenêtres (Simulateur, Démo) sont interactives dès qu'elles sont au
   premier plan — plus de voile « Cliquer pour explorer » (09/09/2026, Nicolas :
   « ne t'embête pas avec ce bouton, il faut que ça fonctionne »). La molette
   reste arbitrée par la position du curseur, cf. deck.js/onWheel. */

/* Le numéro de page n'est plus affiché sur la dalle : le volet de navigation
   porte désormais la numérotation complète (09/09/2026, demande Nicolas). */
.slide-tag { display: none; }

/* ── 4) Le volet de navigation (hors du plan 3D) ────────────────────────── */
.nav-side {
  position: fixed;
  top: var(--pad-side); left: var(--pad-side); bottom: var(--pad-side);
  width: var(--rail-w);
  background: rgba(6, 6, 8, 0.76);
  -webkit-backdrop-filter: blur(20px) saturate(1.25);
  backdrop-filter: blur(20px) saturate(1.25);
  border: 1px solid var(--edge);
  border-radius: 22px;
  box-shadow: 0 24px 70px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.06);
  overflow: hidden;
  display: flex; flex-direction: column;
  z-index: 40;
  transition: transform .38s cubic-bezier(.22,1,.36,1), opacity .3s ease;
}
.rail-brand {
  display: flex; align-items: baseline; gap: 2px;
  padding: 20px 20px 16px; text-decoration: none;
  border-bottom: 1px solid var(--edge-soft);
}
.rail-brand .bd {
  font-family: 'Beautifully Delicious', 'Poppins', sans-serif;
  font-size: 25px; line-height: 1; letter-spacing: 1.5px; color: #fff;
}
.rail-brand .bd-thin { color: #fff; }
.side-hdr { background: transparent; border-bottom: 1px solid var(--edge-soft); padding: 12px 18px; }
.side-hdr-t { color: rgba(255,255,255,.45); font-size: 9.5px; letter-spacing: 2.4px; text-transform: uppercase; }
.side-collapse { border-color: rgba(255,255,255,.14); color: rgba(255,255,255,.5); border-radius: 8px; }
.side-collapse:hover { border-color: rgba(255,255,255,.4); color: #fff; }
.side-scroll { padding: 12px 10px 22px; scrollbar-width: thin; }
.side-scroll::-webkit-scrollbar { width: 3px; }
.side-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 3px; }

.nv-node.nv-sec {
  background: rgba(255,255,255,.045);
  border: 1px solid var(--edge-soft);
  border-radius: 12px; margin-bottom: 7px;
}
.nv-row:hover { background: rgba(255,255,255,.07); }
.nv-arrow { color: rgba(255,255,255,.42); }
.nv-lbl { color: rgba(255,255,255,.66); }
.nv-sec > .nv-row .nv-lbl { color: #fff; }
.nv-children { border-left-color: rgba(255,255,255,.11); }
.nv-page:hover { background: rgba(255,255,255,.08); }
.nv-page .nv-lbl { color: rgba(255,255,255,.52); }
.nv-page.on, .nv-row.on, .nv-node.nv-sec.on > .nv-row { background: rgba(255,255,255,.92); }
.nv-page.on .nv-lbl, .nv-row.on .nv-lbl,
.nv-node.nv-sec.on > .nv-row .nv-lbl { color: #000; font-weight: 600; }
.nv-row.on .nv-arrow { color: rgba(0,0,0,.45); }
/* Les 4 sections sont alignées : la flèche des lignes sans enfant garde sa place. */
.nv-arrow.nv-arrow-void { visibility: hidden; }
.nv-sep { background: rgba(255,255,255,.10); }

.nav-side.collapsed {
  transform: translateX(calc(-100% - var(--pad-side) - 6px));
  opacity: 0; pointer-events: none;
}
.side-reopen.left {
  left: var(--pad-side); top: var(--pad-side);
  width: 42px; height: 42px;
  background: rgba(6,6,8,.8);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid var(--edge); border-radius: 14px;
  color: #fff; z-index: 45;
}
.side-reopen.left:hover { background: rgba(255,255,255,.14); }

/* ── 5) Le rail des stations ────────────────────────────────────────────── */
#journey-rail {
  position: fixed; right: 16px; top: 50%; transform: translateY(-50%);
  z-index: 30; display: flex; flex-direction: column; gap: 9px;
}
.jr-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: rgba(255,255,255,.20);
  border: 0; padding: 0; cursor: pointer; transition: all .25s ease;
}
.jr-dot.sep { margin-top: 12px; }
.jr-dot:hover { background: rgba(255,255,255,.55); transform: scale(1.3); }
.jr-dot.on {
  background: #fff; box-shadow: 0 0 0 4px rgba(255,255,255,.12), 0 0 14px rgba(255,255,255,.6);
}

/* Indication de parcours, en bas au centre. */
#deck-hint {
  position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%);
  z-index: 30; display: flex; align-items: center; gap: 10px;
  font-family: 'Inter', sans-serif; font-size: 10px; letter-spacing: 2px;
  text-transform: uppercase; color: rgba(255,255,255,.34);
  pointer-events: none; transition: opacity .4s;
}
#deck-hint.gone { opacity: 0; }
#deck-hint .k {
  border: 1px solid rgba(255,255,255,.18); border-radius: 6px;
  padding: 2px 7px; font-size: 9px; letter-spacing: 1px;
}

/* ── 6) Mobile ──────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  :root {
    --pad-side: 10px;
    /* La rangée de boutons occupe le haut : le plan commence en dessous, et la
       zone sûre de l'iPhone est retirée — sans ça le haut de la fenêtre passait
       sous la barre du navigateur (retour Nicolas, capture du 09/09/2026). */
    --bar-h: calc(50px + env(safe-area-inset-top, 0px));
    --slide-w: calc(100vw - 34px);   /* place pour le rail des pages, à droite */
    --slide-h: calc(100dvh - var(--bar-h) - 22px);
  }
  #deck { perspective: 1100px; top: var(--bar-h); }
  #app-layer { top: var(--bar-h); }
  #world { left: 0; }
  #journey-rail { right: 6px; }
  #deck-hint { display: none; }

  .nav-side {
    top: 0; left: 0; bottom: 0;
    width: 84vw !important; max-width: 320px;
    border-radius: 0 22px 22px 0;
    transform: translateX(-102%);
    z-index: 205;
  }
  .nav-side.mob-open { transform: translateX(0); }
  .nav-side.collapsed { transform: translateX(-102%); opacity: 1; }
  .rail-brand { padding-left: 68px; }
  .side-collapse { display: none; }

  /* ── Les trois boutons, tous identiques ────────────────────────────────
     Celui du SITE (.mob-burger, déplacé à la racine du document au démarrage)
     puis, quand une fenêtre est au premier plan, ceux de la FENÊTRE — sa propre
     en-tête étant masquée par le style injecté (cf. deck.js). */
  .mob-burger, .app-burger {
    display: flex !important;
    flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    width: 42px; height: 42px; padding: 0;
    background: rgba(6,6,8,.86);
    -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
    border: 1px solid var(--edge) !important; border-radius: 12px;
    cursor: pointer;
  }
  .mob-burger span, .app-burger span {
    display: block; width: 17px; height: 2px; background: #fff; border-radius: 2px;
  }
  .mob-burger {
    position: fixed; z-index: 210;
    top: calc(8px + env(safe-area-inset-top, 0px)); left: 8px;
  }
  /* La rangée s'étale AU-DESSUS de la carte : navigation de la fenêtre à côté
     de celle du site, filtres contre le bord droit — comme dans la page
     elle-même (retour Nicolas, 09/09/2026). */
  #app-mobbar {
    position: fixed; z-index: 210;
    top: calc(8px + env(safe-area-inset-top, 0px));
    left: 0; right: 0; padding: 0 8px 0 58px;
    display: none; align-items: center; justify-content: space-between;
    pointer-events: none;
  }
  #app-mobbar.on { display: flex; }
  #app-mobbar .app-burger { pointer-events: auto; }
  /* Les deux boutons de la FENÊTRE sont en inverse vidéo (10/09/2026, Nicolas :
     « mets le hamburger de navigation du simulateur et de la démo du même blanc
     que celui des filtres ») — ce qui les distingue au passage de celui du SITE,
     resté sombre à gauche. */
  .app-burger-nav, .app-burger-flt { background: rgba(255,255,255,.92); }
  .app-burger-nav span, .app-burger-flt span { background: #111; }

  .side-reopen { display: none !important; }
  .mob-overlay { top: 0; background: rgba(0,0,0,.72); z-index: 198; }
  /* 10/09/2026, Nicolas (page 1.3) : le dernier élément d'une section — ici le
     bouton « Demandez-nous une démonstration » — arrivait collé au bord bas de
     la dalle, sous la bulle Opera et le bandeau de narration. On réserve donc
     de quoi le dégager complètement. */
  .slide .section { padding: 20px 16px 96px; }
  .slide .dashboard-showcase { grid-template-columns: 1fr !important; }
  /* Et la photo, haute de ~370 px sur un écran de 770, repoussait tout le texte
     hors de vue : elle est recadrée pour que le contenu utile arrive vite. */
  .slide .dashboard-showcase-media img { max-height: 230px !important; object-fit: cover; }
  /* Une section plus haute que la dalle doit se lire jusqu'au bout : c'est le
     corps de la dalle qui défile, la hauteur imposée l'en empêchait. */
  .slide .section { min-height: 0 !important; height: auto !important; }
  .slide-body { -webkit-overflow-scrolling: touch; }
  /* Le plein écran se place sous la rangée de boutons, contre le bord droit. */
  /* Le plein écran passe SOUS la rangée de boutons (le burger filtres
     occupe désormais le coin haut-droit). */
  .app-full { top: 10px; right: 10px; opacity: .9; }
}

/* 16/09/2026, Nicolas : « c'est toujours saccadé sur téléphone ». Ni le
   retrait seul du backdrop-filter, ni le gel de la scène (encore essayés
   depuis) n'ont suffi — la seule version confirmée fluide par Nicolas est
   celle-ci : scène coupée en entier sur mobile (cf. `deck.js`, `sceneOffMobile`)
   ET toute transition/animation CSS neutralisée, en plus du fond opaque
   ci-dessous à la place du flou en direct des pastilles fixes
   (`#opera-narr`/`.mob-burger`/`.app-burger`/`.app-full`, affichées en
   permanence par-dessus le contenu qui défile). */
@media (max-width: 900px) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
  #opera-narr, .mob-burger, .app-burger, .app-full {
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
  }
  /* ⚠️ 16/09/2026, retour de Nicolas : un fond forcé identique sur les 3
     pastilles (`.app-burger` couvre aussi bien `.app-burger-nav` que
     `.app-burger-flt`) écrasait, via `!important`, le blanc voulu pour les
     2 boutons de la FENÊTRE (règle plus spécifique mais non `!important`,
     donc perdante face à celle-ci) — indiscernables l'un de l'autre ET du
     hamburger sombre du SITE. Même chose pour `.app-full` (plein écran),
     qui perdait sa propre teinte (`rgba(0,0,0,.62)`, plus claire/translucide
     que le hamburger du site) au profit d'un noir quasi plein identique.
     Le fond opaque anti-flou ne cible donc plus QUE `.mob-burger`, seul à
     n'avoir aucune couleur propre plus spécifique ; `#opera-narr` garde le
     sien (bandeau de narration, pas une pastille de navigation). Le blanc de
     `.app-burger-nav`/`.app-burger-flt` (10/09/2026) et le noir translucide
     de `.app-full` redeviennent les seules couleurs qui s'appliquent — les
     3 pastilles à nouveau visuellement distinctes. */
  #opera-narr, .mob-burger {
    background: rgba(8,8,10,.94) !important;
  }
}

/* La dalle d'Opera ne coupe pas ce qui déborde : sa sphère doit pouvoir grandir
   au-delà du cadre pour qu'on ait le sentiment de la traverser. */
.slide.slide-hero, .slide.slide-hero .slide-body { overflow: visible; }
.slide.slide-hero { background: transparent; border-color: transparent; box-shadow: none; }
.slide.slide-hero .slide-body { display: flex; align-items: center; justify-content: center; }

/* ═══ 8) OPERA NARRATRICE ════════════════════════════════════════════════════
   La page d'accueil est Opera : une grande sphère, et une phrase qui s'écrit.
   Les autres pages reçoivent la même voix, en bandeau discret sous la fenêtre.
   (09/09/2026, demande de Nicolas — « Opera nous fait découvrir l'entreprise
   au travers des sphères qui la composent ».)
══════════════════════════════════════════════════════════════════════════════ */
/* La page d'arrivée : rien d'autre que la sphère, au centre exact de l'écran,
   sur du noir. La phrase se pose en bas, en absolu, pour ne pas décentrer la
   sphère (09/09/2026, demande de Nicolas). */
.slide.slide-hero .slide-body { padding: 0 !important; }
.slide .opera-hero {
  padding: 0 !important; width: 100%; height: 100%; flex: 1 1 auto;
  display: flex; align-items: center; justify-content: center;
}
.slide .opera-hero .section-inner,
.slide .opera-hero-inner {
  display: flex; align-items: center; justify-content: center; text-align: center;
  gap: 0; max-width: none; width: 100%; height: 100%; padding: 0;
}
/* 11/09/2026, demande Nicolas : la sphère est centrée ENTRE le volet de
   navigation et le bord droit, et non plus au centre de l'écran — c'est
   exactement le cadre du plan 3D, qui commence déjà après le volet. Il n'y a
   donc plus aucune compensation à appliquer (l'ancienne marge négative
   ramenait la sphère sous le volet).
   Taille ramenée à 78vh pour que les couronnes extérieures ne soient plus
   rognées par le haut et le bas de l'écran (09/09/2026, retour de Nicolas). */
/* 12/09/2026, 5e passe (retour "fais un rectangle, décolle les sphères de la
   sphère centrale") : la grille 3×2 de la passe précédente centrait la
   sphère sur TOUT le rectangle (`grid-column:1/-1`) — sa propre taille
   (indépendante des colonnes) débordait donc sur les colonnes satellites dès
   qu'elle dépassait le tiers central, l'inverse du but recherché. Retour à un
   positionnement absolu, avec la position de chaque satellite calculée à
   partir du RAYON RÉEL de la sphère (`--orb-size`, même formule
   `min(78vh,980px)` que `.hero-orb`) plutôt qu'un pourcentage arbitraire :
   `--h-off`/`--v-off` ci-dessous visent `rayon + marge` du centre, la marge
   horizontale étant volontairement plus généreuse que la verticale pour
   utiliser l'espace latéral disponible sur un écran large (« beaucoup de
   place sur les côtés ») — la hauteur, elle, reste contrainte par la sphère
   + le bandeau de narration en bas de page. Détail du calcul et du
   rabattement sur les bords (`max()`/`min()`, cf. plus bas) juste avant les
   4 règles `.hero-sat-*`. */
.hero-stage {
  position: relative; width: 100%; height: 100%; box-sizing: border-box;
  --orb-size: min(78vh, 980px);
  /* Décalage HORIZONTAL du point d'ancrage (rayon de la sphère + marge) :
     l'axe où le format d'écran offre le plus de réserve, donc celui où les
     satellites se décollent franchement du bord de la sphère. Décalage
     VERTICAL volontairement plus modeste (une fraction du rayon, pas rayon +
     hauteur du satellite) : l'axe vertical est déjà occupé par la sphère
     (78vh) + le bandeau de narration en bas, `translateY(-50%)` centre donc
     chaque satellite SUR cette ligne au lieu de le pousser entièrement
     au-delà — sans ça (essai du 12/09/2026, 5e passe) les 2 satellites du
     haut sortaient carrément au-dessus du cadre sur un écran large et bas
     (1920×1080). */
  --sat-w: clamp(216px, 17vw, 264px);
  --h-off: calc(var(--orb-size) / 2 + clamp(24px, 6vw, 96px));
  --v-off: calc(var(--orb-size) * 0.36);
  padding: 48px min(9vw, 140px);
}
.hero-orb {
  /* Centrage par marge négative, PAS par `transform:translate()` : deck.js
     pose un `transform` inline sur cet élément pour le zoom/la plongée vers
     1.2 (`heroOrb.style.transform = 'scale(...)'`) — un style inline
     l'emporte toujours sur une règle CSS, donc un `translate()` posé ici
     aurait été écrasé dès le premier appel de `apply()`, recentrant la
     sphère à zéro (piège rencontré ici le 12/09/2026, 5e passe). */
  position: absolute; left: 50%; top: 50%;
  margin-left: calc(var(--orb-size) / -2); margin-top: calc(var(--orb-size) / -2);
  width: var(--orb-size); height: var(--orb-size);
  transform-origin: center center;
  will-change: transform, opacity;
}
.hero-orb .opc-svg { filter: drop-shadow(0 0 14px rgba(255,255,255,.22)); }
.hero-orb .hero-mesh { opacity: .22; }
.hero-orb .hero-mesh line { stroke-width: .3; }
/* Le mot OPERADATA au cœur de la sphère : même police de marque que le logo
   du volet (.rail-brand .bd, 'Beautifully Delicious'), demande Nicolas 26/09/2026. */
.hero-orb-word {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 1; font-family: 'Beautifully Delicious', 'Poppins', sans-serif; font-weight: 400;
  font-size: clamp(26px, 3.2vw, 44px); line-height: 1; letter-spacing: 2px; white-space: nowrap;
  color: #fff; text-shadow: 0 0 16px rgba(255,255,255,.45); pointer-events: none;
}

/* Les 4 sphères satellites : Opera se « démultiplie » pour répondre, en une
   seule page, aux 4 questions que Laurent Bourrelly attend d'une page
   d'accueil — où suis-je / que me propose-t-on / pourquoi vous faire
   confiance / que faire ensuite. 12/09/2026 (retour de Nicolas) : ce sont de
   VRAIES sphères — même dessin (`drawOrb()`) que le décor et que la grande
   sphère d'accueil, pas des cartes de verre — et elles apparaissent EN MÊME
   TEMPS que la phrase d'accueil (`revealHeroSat()` appelée au tout début de
   l'écriture, cf. deck.js), pas après. `display:none` au repos plutôt qu'une
   simple opacité à 0 : sur la mise en page mobile (en flux normal, cf. plus
   bas) un élément invisible mais toujours en boîte aurait quand même réservé
   sa place. */
.hero-sat {
  display: none; position: absolute; width: var(--sat-w);
  aspect-ratio: 1; border-radius: 50%; box-sizing: border-box;
  align-items: center; justify-content: center; text-align: center;
  padding: 20px; text-decoration: none;
  font-family: 'Inter', sans-serif; font-weight: 400; font-size: 12.5px;
  line-height: 1.4; color: rgba(255,255,255,.95);
}
.hero-sat .opc-svg { filter: drop-shadow(0 0 10px rgba(255,255,255,.16)); }
/* Le patch sombre qui rend le texte lisible sur le grillage — même principe
   que le fond `iaCore` du schéma 1.9 (« Le rôle de l'IA »), en CSS plutôt
   qu'en SVG puisque le texte, lui, reste du vrai HTML. */
.hero-sat-core {
  position: absolute; inset: 12%; border-radius: 50%; z-index: 1; pointer-events: none;
  background: radial-gradient(circle, rgba(0,0,0,.74) 0%, rgba(0,0,0,.5) 58%, rgba(0,0,0,0) 100%);
}
.hero-sat p {
  position: relative; z-index: 2; margin: 0; max-width: 72%;
  text-shadow: 0 1px 3px rgba(0,0,0,.9);
}
/* Chaque satellite « sort » de la sphère centrale (translate depuis le cœur
   du plan jusqu'à sa position, cf. --dup-x/--dup-y ci-dessous) au lieu de
   grandir sur place — Opera se démultiplie visiblement, elle n'apparaît pas
   par magie à côté d'elle-même. Et elles arrivent l'UNE APRÈS L'AUTRE
   (délais très écartés) plutôt que quasi en même temps. */
.hero-stage.split .hero-sat {
  display: flex;
}
/* 12/09/2026, 5e passe : chaque satellite est positionné par ses coordonnées
   RÉELLES (`left`/`top`, pas un ancrage + `transform`) — le bord proche de
   chaque satellite vise `rayon de la sphère + marge` du centre
   (`--h-off` en x, `--v-off` en y, cf. plus haut : marge horizontale
   généreuse, l'axe qui a le plus de réserve sur un écran large ; marge
   verticale plus modeste, la hauteur manquant déjà d'air avec la sphère à
   78vh). `max()`/`min()` RABATTENT ensuite ce calcul sur le bord de
   `.hero-stage` lui-même dès que la distance désirée dépasserait l'espace
   réellement disponible (petit écran, format carré) : le satellite reste
   alors VISIBLE, collé au bord, plutôt que de sortir entièrement du cadre —
   piège rencontré ici même (un premier essai avec `transform:translateX
   (±100%)` poussait le satellite hors champ sur un écran étroit, sans que
   `document.scrollWidth` ne le signale puisque `#deck` le clippe en
   silence). `--dup-x`/`--dup-y` (effet « provient du centre », cf.
   l'animation plus bas) n'ont pas besoin de cette précision : une
   approximation (distance visée, sans le rabattement) suffit pour un simple
   effet de démultiplication. */
.hero-sat-tl {
  left: max(0px, calc(50% - var(--h-off) - var(--sat-w)));
  top: max(0px, calc(50% - var(--v-off) - var(--sat-w) / 2));
  --dup-x: calc(var(--h-off) + var(--sat-w) / 2); --dup-y: var(--v-off);
}
.hero-sat-tr {
  left: min(calc(100% - var(--sat-w)), calc(50% + var(--h-off)));
  top: max(0px, calc(50% - var(--v-off) - var(--sat-w) / 2));
  --dup-x: calc(-1 * (var(--h-off) + var(--sat-w) / 2)); --dup-y: var(--v-off);
}
.hero-sat-bl {
  left: max(0px, calc(50% - var(--h-off) - var(--sat-w)));
  top: min(calc(100% - var(--sat-w)), calc(50% + var(--v-off) - var(--sat-w) / 2));
  --dup-x: calc(var(--h-off) + var(--sat-w) / 2); --dup-y: calc(-1 * var(--v-off));
}
.hero-sat-br {
  left: min(calc(100% - var(--sat-w)), calc(50% + var(--h-off)));
  top: min(calc(100% - var(--sat-w)), calc(50% + var(--v-off) - var(--sat-w) / 2));
  --dup-x: calc(-1 * (var(--h-off) + var(--sat-w) / 2)); --dup-y: calc(-1 * var(--v-off));
}
.hero-stage.split .hero-sat-tl { animation-delay: 0s; }
.hero-stage.split .hero-sat-tr { animation-delay: .5s; }
.hero-stage.split .hero-sat-bl { animation-delay: 1s; }
.hero-stage.split .hero-sat-br { animation-delay: 1.5s; }
.hero-stage.split .hero-sat { animation-name: heroSatIn; }
.hero-sat {
  animation-duration: .85s; animation-timing-function: cubic-bezier(.16,.8,.2,1);
  animation-fill-mode: both;
}
@keyframes heroSatIn {
  0%   { opacity: 0; transform: translate(var(--dup-x), var(--dup-y)) scale(.1); }
  45%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0) scale(1); }
}
/* Une fois l'entrée jouée, l'animation est retirée (cf. revealHeroSat() dans
   deck.js, sur `animationend`) : avec `fill-mode:both` elle continuerait sinon
   à imposer son opacité de fin et empêcherait zoomHero() de la faire baisser
   pendant la traversée vers la page 1.2 (un style posé en JS ne peut pas
   l'emporter sur un effet d'animation encore actif). `transform:none` est
   ici sans risque (contrairement à un essai précédent) : `left`/`top` sont
   les coordonnées RÉELLES du satellite, plus un simple ancrage à annuler. */
.hero-stage.split .hero-sat.sat-in { animation: none; opacity: 1; transform: none; }

/* Le CTA (4e satellite) reste la même sphère, juste un texte en gras et une
   flèche qui glisse au survol — c'est la seule des 4 qui appelle une action
   (lien vers Contact, 1.10). */
.hero-sat-cta { cursor: pointer; font-weight: 600; }
.hero-sat-cta .hero-sat-arrow { display: inline-block; transition: transform .2s ease; }
.hero-sat-cta:hover, .hero-sat-cta:focus-visible { outline: none; }
.hero-sat-cta:hover .opc-svg, .hero-sat-cta:focus-visible .opc-svg { filter: drop-shadow(0 0 16px rgba(255,255,255,.3)); }
.hero-sat-cta:hover .hero-sat-arrow, .hero-sat-cta:focus-visible .hero-sat-arrow { transform: translateX(4px); }

@keyframes narrCaret { 0%, 50% { opacity: 1 } 50.01%, 100% { opacity: 0 } }

/* Le bandeau de narration des AUTRES pages : il se loge dans la bande laissée
   libre sous la fenêtre, jamais par-dessus le contenu. */
#opera-narr {
  position: fixed; left: 50%; bottom: 8px;
  /* 16/09/2026, hypothèse retenue pour « une barre apparaît pendant le scroll
     sur téléphone » (bug iOS Safari bien connu : un élément `position:fixed`
     ailleurs dans la page se détache visuellement de sa position pendant le
     scroll tactile momentum d'un conteneur `-webkit-overflow-scrolling:touch`
     comme `.slide-body`, et réapparaît au milieu du contenu jusqu'à ce que le
     scroll s'arrête). `translate3d` au lieu de `translateX` force ce bandeau
     sur sa propre couche GPU, le remède standard à ce glitch de composition.
     ⚠️ Non confirmé sur un vrai appareil (aucun disponible en session, comme
     d'habitude sur ce site) — à vérifier par Nicolas. */
  transform: translate3d(-50%, 0, 0);
  z-index: 7; display: none; align-items: center; gap: 10px;
  width: min(880px, 92vw); padding: 7px 16px 7px 10px;
  border-radius: 999px; border: 1px solid var(--edge);
  background: rgba(6,6,8,.72);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  font-size: 12.5px; line-height: 1.35; color: rgba(255,255,255,.82); text-wrap: balance;
  pointer-events: none; opacity: 0; transition: opacity .35s ease;
}
#opera-narr.on { display: flex; opacity: 1; }
#opera-narr .narr-orb { width: 22px; height: 22px; position: relative; flex-shrink: 0; }
#opera-narr .narr-t { text-align: left; }
#opera-narr .narr-t::after {
  content: ''; display: inline-block; width: 1.5px; height: .95em; margin-left: 3px;
  background: #fff; vertical-align: -.1em; animation: narrCaret 1s step-end infinite;
}
#opera-narr .narr-t.done::after { animation: none; opacity: 0; }

@media (max-width: 900px) {
  #opera-narr { bottom: 4px; padding: 5px 12px 5px 8px; font-size: 11.5px; width: 94vw; }

  /* La sphère occupe déjà presque tout l'écran : les 4 satellites ne peuvent
     plus s'orbiter autour d'elle. Ils se rangent en pile en dessous et la
     page défile pour les lire — comme n'importe quelle autre dalle
     (`.slide .section{height:auto}` permet déjà ce défilement mobile). */
  .slide.slide-hero .slide-body {
    display: block; overflow-y: auto; height: auto;
    padding: calc(4vh + var(--bar-h, 0px)) 16px 10vh;
  }
  /* Mêmes DEUX sélecteurs que la règle de base groupée : l'élément porte à la
     fois `.section-inner` et `.opera-hero-inner`, et c'est le plus spécifique
     des deux (`.slide .opera-hero .section-inner`, 3 classes) qui décidait du
     `display:flex` — un simple `.opera-hero-inner` (2 classes) ne suffisait
     pas à le emporter, quel que soit l'ordre dans le fichier. */
  .slide .opera-hero .section-inner,
  .slide .opera-hero-inner { display: block; height: auto; }
  /* La base est passée en grille (3e retour de Nicolas, cf. plus haut) : sur
     mobile on repasse explicitement en `flex` colonne, `flex-direction` seul
     n'aurait plus aucun effet sur un conteneur resté en `grid`. */
  .hero-stage { display: flex; flex-direction: column; align-items: stretch; gap: 14px; padding: 0; width: 100%; }
  /* 13/09/2026, CAUSE RACINE du rendu cassé sur téléphone (« encore une fois
     il n'y a rien qui va ») : la règle de base centre la sphère par
     `position:absolute` + marge négative calculée sur `--orb-size` (cf. plus
     haut). Cette section ne redéfinissait jamais `position`/`left`/`top` sur
     mobile — seuls `width`/`height`/`margin` l'étaient — donc la sphère
     restait ancrée en (50%,50%) SANS la marge négative qui la recentre,
     un bug pur et simple (elle grossissait hors cadre, décalée). Elle revient
     donc dans le flux de la colonne flex, comme n'importe quel autre item.
     ⚠️ `relative`, SURTOUT PAS `static` : le SVG de la sphère et le patch
     sombre du texte sont en `position:absolute` (`inset:0` / `inset:12%`,
     cf. `.opc-svg`/`.hero-sat-core`) — en `static` leur bloc conteneur
     devient `.hero-stage`, donc TOUTE la colonne. Mesuré en session au 2e
     essai : le SVG d'un satellite faisait 354×1639 px au lieu de 304×304, sa
     sphère était redimensionnée pour tenir dans ce cadre (d'où le cercle
     géant rogné aux bords de l'écran) et les 4 patches sombres se
     superposaient sur toute la page — exactement la capture de Nicolas.
     `relative` + `left/top:auto` garde la position du flux ET le rôle de bloc
     conteneur. (La sphère centrale, elle, ne l'avait pas montré : le
     `transform` en ligne que lui pose zoomHero() crée à lui seul un bloc
     conteneur, ce qui masquait le bug de son côté.) */
  .hero-orb {
    position: relative !important; left: auto !important; top: auto !important;
    margin: 0 auto !important; width: min(62vh, 94vw); height: min(62vh, 94vw);
  }
  .hero-orb-word { font-size: clamp(24px, 7.5vw, 32px); letter-spacing: 1.5px; }
  /* Sphères STATIQUES sur mobile, comme demandé par Nicolas le 13/09/2026
     (« sphères statiques et sans animations si ça peut aider ») : même
     ancrage défensif que `.hero-orb` ci-dessus (`relative`, cf. l'avertissement
     juste au-dessus), rotation du maillage coupée (`spin:true` côté JS ne vaut
     donc que pour desktop), et apparition en simple fondu sans transformation
     ni filtre — le moins de calcul possible pendant le défilement tactile.
     `align-self:center` écrase le start/end hérité des règles de grille
     desktop (`.hero-sat-tl` etc.) : dans un flex `column`, `align-self` joue
     sur l'axe transversal (horizontal). */
  .hero-sat {
    position: relative !important; left: auto !important; top: auto !important;
    width: min(78vw, 320px); margin: 0 auto !important; align-self: center;
  }
  .hero-sat .opc-svg { filter: none; }
  .hero-sat .opc-svg .spin { animation: none !important; }
  .hero-stage.split .hero-sat {
    animation-name: heroSatInMobile; animation-duration: .5s; animation-timing-function: ease-out;
  }
  @keyframes heroSatInMobile { from { opacity: 0; } to { opacity: 1; } }
}

/* ── 7) Accessibilité ───────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  #world, .nav-side, #deck, .hero-sat { transition: none !important; animation: none !important; }
}
