/* ============================================================================
   IKONIQ MOTION — kit d'animations, 0 dépendance
   À copier tel quel dans n'importe quel site (HTML pur, Next.js, Astro…).
   Couplé à ikoniq-motion.js. Voir animations.md pour le mode d'emploi.
   ----------------------------------------------------------------------------
   PROGRESSIVE ENHANCEMENT : rien n'est masqué tant que le <html> ne porte pas
   la classe .io-on (posée par le script en <head>). Sans JS, la page reste
   entièrement lisible. C'est volontaire — un site invisible ne se vend pas.
   ========================================================================== */

:root{
  --io-dur:  .72s;          /* durée de base (surchargeable par --io-dur-el)  */
  --io-dur-l: 1.05s;        /* durée des gros blocs (hero, images pleine larg.)*/
  --io-ease: cubic-bezier(.22,.61,.36,1);   /* sortie douce, pas de rebond    */
  --io-ease-out: cubic-bezier(.16,1,.3,1);  /* "expo out", pour les masques   */
  --io-dist: 28px;          /* distance de translation par défaut             */
}

/* ---------- 0. GARDE-FOU HORIZONTAL ----------------------------------------
   Un reveal "left"/"right" décale l'élément de 28px AVANT d'entrer : sur mobile
   ça fabrique une barre de défilement horizontale. `clip` la supprime SANS
   casser `position:sticky` (contrairement à `hidden`) — les cartes empilées et
   le défilement horizontal continuent de fonctionner.
--------------------------------------------------------------------------- */
html.io-on{ overflow-x:clip }

/* ---------- 1. REVEAL AU SCROLL --------------------------------------------
   <div data-reveal>              → fondu + montée
   <div data-reveal="left">       → arrive de la gauche
   <h2  data-reveal="mask">       → dévoilé par un volet (clip-path)
   <p   data-reveal="blur">       → net + fondu
   <img data-reveal="zoom">       → dézoom lent
   <ul  data-reveal-stagger>      → les enfants entrent en cascade
   attributs : data-reveal-delay="120" (ms) · data-reveal-dist="60" (px)
--------------------------------------------------------------------------- */
.io-on [data-reveal]{
  opacity:0;
  transition:
    opacity   var(--io-dur-el,var(--io-dur)) var(--io-ease) var(--io-delay,0ms),
    transform var(--io-dur-el,var(--io-dur)) var(--io-ease-out) var(--io-delay,0ms),
    filter    var(--io-dur) var(--io-ease) var(--io-delay,0ms),
    clip-path var(--io-dur-l) var(--io-ease-out) var(--io-delay,0ms);
  will-change:opacity,transform;
}
.io-on [data-reveal=""],
.io-on [data-reveal="up"]   { transform:translate3d(0,var(--io-d,var(--io-dist)),0) }
.io-on [data-reveal="down"] { transform:translate3d(0,calc(-1 * var(--io-d,var(--io-dist))),0) }
.io-on [data-reveal="left"] { transform:translate3d(calc(-1 * var(--io-d,var(--io-dist))),0,0) }
.io-on [data-reveal="right"]{ transform:translate3d(var(--io-d,var(--io-dist)),0,0) }
.io-on [data-reveal="scale"]{ transform:scale(.94) }
.io-on [data-reveal="zoom"] { transform:scale(1.08); opacity:1 }
.io-on [data-reveal="blur"] { filter:blur(10px); transform:translate3d(0,12px,0) }
.io-on [data-reveal="mask"] { clip-path:inset(0 0 102% 0); opacity:1; transform:none }
.io-on [data-reveal="fade"] { transform:none }

.io-on [data-reveal].io-in{
  opacity:1;
  transform:none;
  filter:none;
  clip-path:inset(0 0 0 0);
}
.io-on [data-reveal].io-done{ will-change:auto }

/* ---------- 2. TEXTE DÉCOUPÉ ------------------------------------------------
   <h1 data-split="words">  ou  data-split="lines"
   Le script enveloppe chaque mot/ligne ; on anime le contenu derrière un volet.
--------------------------------------------------------------------------- */
.io-line{ display:block; overflow:hidden }
.io-word{ display:inline-block; overflow:hidden; vertical-align:top }
.io-line > i, .io-word > i{
  display:inline-block; font-style:inherit;
  transform:translate3d(0,110%,0);
  transition:transform .9s var(--io-ease-out) var(--io-delay,0ms);
  will-change:transform;
}
.io-in .io-line > i, .io-in .io-word > i{ transform:none }

/* ---------- 2 bis. SURLIGNAGE AU DÉFILEMENT --------------------------------
   <p data-split="words" data-highlight>…</p>
   Le texte est posé en clair et chaque mot passe en pleine encre à mesure que
   la page descend. Piloté par la POSITION de défilement, pas par une entrée :
   c'est ce qui le distingue d'un simple reveal. Vu sur clause (réf. 10).
   Réglages : data-highlight="0.16" (opacité éteinte)
--------------------------------------------------------------------------- */
.io-on [data-highlight] .io-word{ overflow:visible }
.io-on [data-highlight] .io-word > i{
  transform:none;
  opacity:var(--io-o,.16);
  transition:opacity .25s linear;
}

/* ---------- 2 ter. ROTATEUR DE MOTS -----------------------------------------
   <h1>On répare <span data-rotate>les fuites, les chaudières, les urgences</span></h1>
   Les mots défilent verticalement derrière un volet. Réglages :
   data-rotate-delay="2200" (ms entre deux mots) · data-rotate-dur="1500"
   La largeur est figée sur le mot le plus long : aucun décalage de mise en page.
--------------------------------------------------------------------------- */
.io-rot{ display:inline-block; position:relative; overflow:hidden;
         vertical-align:bottom; max-width:100% }
.io-rot > b{
  position:absolute; inset:0; display:flex; align-items:center; white-space:nowrap;
  font-weight:inherit;
  transform:translateY(115%);
  transition:transform var(--io-rot-dur,1.5s) var(--io-ease-out);
}
.io-rot > b.io-rot-actif{ transform:none }
.io-rot > b.io-rot-sorti{ transform:translateY(-115%) }

/* ---------- 2 quater. DIAPOSITIVES ÉPINGLÉES EN 3D --------------------------
   <section class="io-slides">
     <article class="io-slides__item">…</article>   ×3 à 5
   </section>
   Chaque volet se colle plein écran ; quand le suivant le recouvre, il bascule
   en arrière sur l'axe X, rétrécit et s'efface. Le décor recule vraiment.
   Réglages : --io-slide-rot (25deg) · --io-slide-scale (.27) · --io-slide-persp
--------------------------------------------------------------------------- */
.io-slides{ --io-slide-rot:25deg; --io-slide-scale:.27; --io-slide-persp:1400px }
.io-slides__item{
  position:sticky; top:0; min-height:100svh;
  transform-origin:50% 0;
  will-change:transform,opacity;
  backface-visibility:hidden;
}

/* ---------- 2 quinquies. FOND MAILLÉ + GRAIN --------------------------------
   <div class="io-mesh"> … </div>
   Remplace la vidéo de fond des sites d'agence : même rendu, quelques octets au
   lieu de plusieurs mégaoctets. Sur une vitrine consultée en 4G dans la rue,
   ça n'est pas un détail.
   Réglages : --io-m1 … --io-m4 (les 4 taches) · --io-grain (0 à .5)
--------------------------------------------------------------------------- */
.io-mesh{
  position:relative; isolation:isolate;
  --io-m1:#6D4BFF; --io-m2:#FFF6A8; --io-m3:#D9C4FF; --io-m4:#3B2ED8;
  background:
    radial-gradient(46% 52% at 22% 26%, var(--io-m1) 0%, transparent 62%),
    radial-gradient(44% 48% at 80% 14%, var(--io-m2) 0%, transparent 58%),
    radial-gradient(52% 56% at 72% 84%, var(--io-m4) 0%, transparent 62%),
    radial-gradient(46% 52% at 22% 88%, var(--io-m3) 0%, transparent 58%),
    linear-gradient(140deg, var(--io-m3), var(--io-m1));
  background-size:180% 180%,170% 170%,190% 190%,175% 175%,100% 100%;
  animation:io-mesh 26s ease-in-out infinite alternate;
}
@keyframes io-mesh{
  0%  { background-position:0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 0 }
  100%{ background-position:22% 18%, 78% 14%, 74% 82%, 26% 86%, 0 0 }
}
.io-mesh::after{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  opacity:var(--io-grain,.34); mix-blend-mode:overlay;
  background-size:180px 180px;
  /* feTurbulence seul rend un bruit trop mou pour se voir : le feColorMatrix
     l'écrase en noir et blanc contrasté, sinon le grain disparaît au rendu. */
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncA type='linear' slope='2.4' intercept='-.7'/></feComponentTransfer></filter><rect width='180' height='180' filter='url(%23g)'/></svg>");
}
.io-mesh > *{ position:relative; z-index:2 }

/* ---------- 2 sexies. GRILLE DE REPÈRE --------------------------------------
   <section class="io-grille"> … </section>
   Les filets de construction laissés visibles, avec un losange aux croisements.
   Réglages : --io-col (largeur de colonne) · --io-row · --io-trait
--------------------------------------------------------------------------- */
.io-grille{ position:relative;
  --io-col:25%; --io-row:340px;
  --io-trait:rgba(0,0,0,.08); --io-noeud:rgba(0,0,0,.26); }
.io-grille::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  /* ⚠️ Une image de fond est ÉTIRÉE à `background-size` : un petit losange SVG
     répété au pas de la grille devient un losange géant. Le nœud se dessine donc
     avec un radial-gradient — l'image fait la taille de la cellule, la marque
     dedans garde ses 3,5 px. */
  background-image:
    linear-gradient(to right, var(--io-trait) 1px, transparent 1px),
    linear-gradient(to bottom, var(--io-trait) 1px, transparent 1px),
    radial-gradient(circle 3.5px at 0 0, var(--io-noeud) 98%, transparent 100%);
  background-size:var(--io-col) 100%, 100% var(--io-row), var(--io-col) var(--io-row);
  background-position:0 0, 0 0, 0 0;
}
.io-grille > *{ position:relative; z-index:1 }

/* ---------- 2 novies. HERO À PHOTO QUI S'OUVRE -------------------------------
   <header class="io-hx">
     <div class="io-hx__intro">  titre + boutons, seuls, sur ~90 svh          </div>
     <div class="io-hx__piste">                     <-- la course de défilement
       <div class="io-hx__scene">                   <-- collée, plein écran
         <figure class="io-hx__media">
           <img …>
           <figcaption class="io-hx__legende" data-split="words">16 ans de …</figcaption>
         </figure>
       </div>
     </div>
   </header>

   La photo pointe de 10 svh sous le titre, grandit jusqu'à remplir presque
   l'écran, puis rétrécit et remonte pour libérer la section suivante.
   Réglages : --io-hx-piste (course) · --io-hx-min (échelle de départ)
              --io-hx-fin (échelle de sortie) · --io-hx-h (hauteur du média)
--------------------------------------------------------------------------- */
.io-hx{ --io-hx-piste:260svh; --io-hx-min:.62; --io-hx-fin:.84; --io-hx-h:90svh;
        --io-hx-r:clamp(16px,2vw,28px) }
/* 90 svh MOINS la barre collée : sans ça la photo ne pointe que de 2 svh,
   parce que l'en-tête occupe sa place dans le flux. --io-barre est posée par le JS. */
.io-hx__intro{ min-height:calc(90svh - var(--io-barre,0px)); display:grid; align-content:center }
.io-hx__piste{ height:var(--io-hx-piste); position:relative }
.io-hx__scene{ position:sticky; top:0; height:100svh; display:grid; place-items:center;
               pointer-events:none }
.io-hx__media{
  position:relative; margin:0; width:100%; height:var(--io-hx-h);
  border-radius:var(--io-hx-r); overflow:hidden; background:#141414;
  transform-origin:50% 50%; will-change:transform; pointer-events:auto;
}
.io-hx__media > img{ width:100%; height:100%; object-fit:cover; display:block }
.io-hx__media::after{ content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(to top,rgba(6,6,6,.62) 0%,rgba(6,6,6,.18) 42%,rgba(6,6,6,.06) 100%) }
.io-hx__legende{
  position:absolute; inset:auto 0 0 0; z-index:2; padding:clamp(20px,4vw,54px);
  color:#fff; font-weight:500; letter-spacing:-.03em; line-height:1;
  font-size:clamp(28px,5.6vw,86px);
}
/* le découpage en mots pose translateY(110%) pour l'effet volet ; ici l'entrée
   se fait par l'opacité, il faut donc annuler la translation — sans ça la
   légende reste cachée dans son volet et on ne voit RIEN. */
.io-hx__legende .io-word > i{ transform:none; opacity:var(--io-o,.12) }

/* La section qui suit remonte SUR la fin de la piste : c'est elle qui « apparaît »
   pendant que la photo rétrécit. Elle doit être opaque, sinon on voit l'image
   à travers. La scène collée ne pose pas de z-index, donc la suite passe devant. */
.io-hx__suite{ position:relative; margin-top:calc(-1 * var(--io-hx-recouvre,34svh)) }

@media (max-width:760px){ .io-hx{ --io-hx-piste:200svh; --io-hx-min:.7; --io-hx-h:74svh;
                                  --io-hx-recouvre:24svh } }
@media (prefers-reduced-motion: reduce){ .io-hx__suite{ margin-top:0 } }

@media (prefers-reduced-motion: reduce){
  .io-hx__piste{ height:auto }
  .io-hx__scene{ position:static; height:auto; padding-bottom:24px }
  .io-hx__media{ transform:none !important; height:auto; aspect-ratio:16/10 }
  .io-hx__legende .io-word > i{ opacity:1 !important }
}

/* ---------- 3. CARTES EMPILÉES (sticky stack) -------------------------------
   <div class="io-stack">
     <article class="io-stack__card">…</article>  ×N
   </div>
   Chaque carte se colle, la suivante recouvre la précédente en la reculant.
--------------------------------------------------------------------------- */
.io-stack{ --io-stack-top:12vh; --io-stack-step:14px }
.io-stack__card{
  position:sticky;
  top:calc(var(--io-stack-top) + var(--i,0) * var(--io-stack-step));
  transform-origin:50% 0;
  transition:transform .5s var(--io-ease), opacity .5s var(--io-ease);
}
.io-stack__card.io-behind{ transform:scale(calc(1 - var(--io-depth,0) * .035)) }

/* ---------- 4. PARALLAXE ----------------------------------------------------
   <img data-parallax="0.18">   (0.05 discret · 0.3 marqué · négatif = inverse)
--------------------------------------------------------------------------- */
[data-parallax]{ will-change:transform }

/* ---------- 5. BANDEAU DÉFILANT (marquee) -----------------------------------
   <div class="io-marquee" data-marquee style="--io-speed:28s">
     <div class="io-marquee__row">… contenu …</div>
   </div>
   Le script duplique la piste : pas de trou, pas de saut.
--------------------------------------------------------------------------- */
.io-marquee{ overflow:hidden; display:flex; width:100%; --io-speed:30s }
.io-marquee__row{
  display:flex; flex:0 0 auto; gap:var(--io-gap,3rem);
  padding-right:var(--io-gap,3rem);
  animation:io-slide var(--io-speed) linear infinite;
}
.io-marquee:hover .io-marquee__row{ animation-play-state:var(--io-hover,running) }
@keyframes io-slide{ to{ transform:translate3d(-100%,0,0) } }

/* ---------- 6. EN-TÊTE AU SCROLL --------------------------------------------
   <header data-header>  → reçoit .io-scrolled après 40px, .io-hide en descente
--------------------------------------------------------------------------- */
[data-header]{ transition:transform .4s var(--io-ease), background-color .3s, box-shadow .3s, backdrop-filter .3s }
[data-header].io-hide{ transform:translate3d(0,-102%,0) }

/* ---------- 7. BOUTON MAGNÉTIQUE + SURVOL -----------------------------------
   <a data-magnetic data-magnetic-strength="0.25">
--------------------------------------------------------------------------- */
[data-magnetic]{ transition:transform .35s var(--io-ease) }

/* ---------- 8. JAUGE DE LECTURE ---------------------------------------------
   <div data-progress></div>  → largeur = progression de la page
--------------------------------------------------------------------------- */
[data-progress]{ transform-origin:0 50%; transform:scaleX(0); will-change:transform }

/* ---------- 9. DÉFILEMENT HORIZONTAL ----------------------------------------
   <section class="io-hscroll"><div class="io-hscroll__track">…</div></section>
--------------------------------------------------------------------------- */
.io-hscroll{ position:relative }
.io-hscroll__sticky{ position:sticky; top:0; height:100svh; display:flex; align-items:center; overflow:hidden }
.io-hscroll__track{ display:flex; gap:var(--io-gap,2rem); will-change:transform }

/* ---------- 10. COUPURE TOTALE ----------------------------------------------
   Non négociable : accessibilité + confort. Tout redevient statique et visible.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  .io-on [data-reveal],
  .io-on [data-reveal].io-in{
    opacity:1 !important; transform:none !important;
    filter:none !important; clip-path:none !important;
    transition:none !important;
  }
  .io-line > i, .io-word > i{ transform:none !important; transition:none !important }
  .io-on [data-highlight] .io-word > i{ opacity:1 !important }
  .io-marquee__row{ animation:none !important }
  .io-stack__card{ position:static !important; transform:none !important }
  .io-slides__item{ position:static !important; transform:none !important; opacity:1 !important }
  .io-rot > b{ transition:none !important }
  .io-mesh{ animation:none !important }
  [data-parallax]{ transform:none !important }
  *{ scroll-behavior:auto !important }
}

/* ---------- 11. IMPRESSION — tout visible, rien d'animé -------------------- */
@media print{
  .io-on [data-reveal]{ opacity:1 !important; transform:none !important;
    filter:none !important; clip-path:none !important; transition:none !important }
  .io-line > i, .io-word > i{ transform:none !important }
  .io-stack__card{ position:static !important; transform:none !important }
  .io-slides__item{ position:static !important; transform:none !important; opacity:1 !important }
  .io-rot > b{ transition:none !important }
  .io-mesh{ animation:none !important }
  .io-marquee__row{ animation:none !important }
  [data-header].io-hide{ transform:none !important }
}
