/* =============================================================================
   gelato-stack.css — Empilage animé « boule par boule » (La Gourmand'ice)
   -----------------------------------------------------------------------------
   Animation signature : le cornet se remplit boule par boule, chaque boule
   tombe PAR-DESSUS la pile, s'écrase à l'impact (squash) pendant que la boule
   du dessous s'aplatit (tassement), puis rebondit vers sa forme.

   100 % compositor : on n'anime QUE transform + opacity.
   Aucune propriété layout (width/top/margin) n'est animée.

   STRUCTURE DOM attendue (markup) :
     <div class="gice-stack" data-gice-stack>
       <div class="gice-scoop gice-scoop--3 <parfum>" style="--i:3">
         <div class="gice-fall">                 ← chute     (origin centre)
           <div class="gice-press">              ← tassement (origin HAUT)
             <div class="gice-squash">           ← squash    (origin BAS)
               <svg class="gice-art" viewBox="0 0 120 100"><use href="#gi-scoop"/></svg>
             </div>
           </div>
         </div>
       </div>
       … (scoop--2, scoop--1) …
       <svg class="gice-cone" viewBox="0 0 120 150"><use href="#gi-cone"/></svg>
     </div>

   - .gice-scoop  → POSITION dans le cornet (top:-Npx) + z-index + décalage final
                    latéral/angle « fait main » (--dx / --rot, variés par boule).
   - .gice-fall   → joue la CHUTE (translateY de haut→0) + le décalage latéral.
   - .gice-press  → joue le TASSEMENT (origin HAUT) quand la boule du DESSUS se
                    pose dessus. Wrapper dédié → jamais en conflit avec le squash.
   - .gice-squash → joue le SQUASH de pose (origin BAS) : la boule s'écrase sur
                    celle d'en dessous puis rebondit.
   - .gice-art    → le SVG lui-même (jamais déformé directement, sert de canvas).

   TROIS wrappers car les transform-origin diffèrent (centre / haut / bas) et
   parce que squash de pose et tassement peuvent se chevaucher dans le temps :
   chacun sur son nœud = aucune animation concurrente sur la même propriété.
============================================================================= */

.gice-stack{
  position:relative;
  width:120px;
  margin:0 auto;
  filter:drop-shadow(0 10px 14px rgba(120,60,30,.18));
}

/* Cornet : socle, toujours le plus bas dans la peinture. */
.gice-stack .gice-cone{
  display:block;
  width:120px;
  height:150px;
  position:relative;
  z-index:0;
}

/* Position de chaque boule dans le cornet (repères du sprite). */
.gice-stack .gice-scoop{
  position:absolute;
  left:0;
  width:120px;
  height:100px;
}
/* La boule 1 REPOSE sur l'ouverture (base ~y80 → cone-y≈50, juste sous l'ourlet)
   et son ventre DÉBORDE la lèvre — elle ne plonge plus dans le cornet.
   Écart constant de 34px entre boules (forme remontée à y≈80, pas y≈87). */
.gice-scoop--1{ top:-30px;  }
.gice-scoop--2{ top:-64px;  }
.gice-scoop--3{ top:-98px;  }
.gice-scoop--4{ top:-132px; }

/* Z-ORDER (le fix « boule par-dessus ») :
   plus une boule est haute dans la pile, plus son z-index est grand, donc elle
   se peint DEVANT/au sommet — jamais derrière. --i est l'index de pile (1..4). */
.gice-scoop{ z-index:calc(var(--i,1) + 1); }

/* Wrappers d'animation. TROIS rôles distincts, un élément CHACUN, pour qu'aucun
   nœud ne porte deux animations concurrentes sur la propriété transform :
     .gice-fall  → chute      (origin centre)   — translateY/X + rotate
     .gice-press → tassement  (origin HAUT)     — quand la boule du dessus se pose
     .gice-squash→ squash pose (origin BAS)     — écrasement propre de la boule
   Au repos = état final neutre (pile posée, forme normale). */
.gice-fall,
.gice-press,
.gice-squash{ display:block; width:120px; height:100px; will-change:transform; }
.gice-fall{   transform-origin:50% 50%; transform:translateY(0) translateX(var(--dx,0px)) rotate(var(--rot,0deg)); }
.gice-press{  transform-origin:50% 12%; transform:scaleX(1) scaleY(1); } /* le contact vient du HAUT */
.gice-squash{ transform-origin:50% 92%; transform:scaleX(1) scaleY(1); } /* s'écrase par le BAS      */
.gice-art{    display:block; width:120px; height:100px; }

/* -----------------------------------------------------------------------------
   ÉTAT INITIAL avant déclenchement (.is-armed) :
   tout est invisible, en haut, pré-étiré (prêt à tomber).
----------------------------------------------------------------------------- */
.gice-stack.is-armed .gice-fall{
  opacity:0;
  transform:translateY(-130px) translateX(var(--dx-from,0px)) rotate(var(--rot-from,0deg));
}
.gice-stack.is-armed .gice-squash{ transform:scaleX(.90) scaleY(1.14); } /* pré-stretch de vol */

/* -----------------------------------------------------------------------------
   DÉCLENCHEMENT (.is-playing) : chute + squash de pose, par boule.
   Le délai par boule (--delay) est posé en inline-style par le JS d'orchestration
   (cascade : chaque boule attend que la précédente soit posée).
----------------------------------------------------------------------------- */
.gice-stack.is-playing .gice-scoop .gice-fall{
  animation:gice-fall var(--fall-dur,.46s) cubic-bezier(.55,.06,.78,.36) var(--delay,0s) both;
  /* cubic-bezier ease-in marqué = accélération « gravité ».
     both : applique le keyframe de départ pendant le --delay (boule cachée en haut)
     ET fige le keyframe final (translateY 0) jusqu'à la fin de la lecture. */
}
.gice-stack.is-playing .gice-scoop .gice-squash{
  animation:gice-squash var(--squash-dur,.62s) cubic-bezier(.22,1,.36,1)
            calc(var(--delay,0s) + var(--fall-dur,.46s)) both;
  /* démarre PILE à l'atterrissage (delay + durée de chute) ; both → pré-stretch
     maintenu pendant l'attente, et retour propre à scale(1,1) à la fin. */
}

/* La boule DU DESSOUS encaisse le poids : aplatissement par le HAUT, sur SON
   propre wrapper .gice-press → aucune collision avec son squash de pose déjà fini.
   Classe posée par le JS pile au moment du contact. forwards : reste à scale(1,1). */
.gice-stack .gice-scoop.is-pressed .gice-press{
  animation:gice-press var(--press-dur,.5s) cubic-bezier(.22,1,.36,1) forwards;
}

/* =============================================================================
   KEYFRAMES
   -----------------------------------------------------------------------------
   gice-fall   : chute verticale + décalage latéral/angle (origin centre).
                 Le scoop part de -130px (hors cornet), descend à 0.
   gice-squash : déformation à l'impact (origin bas) — stretch en vol →
                 écrasement (scaleY↓ scaleX↑) → rebond/overshoot → repos.
   gice-press  : aplatissement de la boule du dessous au contact (origin haut).
============================================================================= */

@keyframes gice-fall{
  0%{
    opacity:0;
    transform:translateY(-130px) translateX(var(--dx-from,0px)) rotate(var(--rot-from,0deg));
  }
  18%{ opacity:1; }              /* apparaît vite, en pleine chute */
  100%{
    opacity:1;
    transform:translateY(0) translateX(var(--dx,0px)) rotate(var(--rot,0deg));
  }
}

@keyframes gice-squash{
  /* À 0 % la chute vient de finir : la boule est encore en stretch de vol. */
  0%   { transform:scaleX(.90) scaleY(1.14); } /* étirée par la vitesse        */
  28%  { transform:scaleX(1.22) scaleY(.74); } /* IMPACT : s'écrase fort en bas */
  52%  { transform:scaleX(.92)  scaleY(1.09); } /* rebond : se ré-étire un peu  */
  72%  { transform:scaleX(1.05) scaleY(.96); } /* contre-rebond léger          */
  88%  { transform:scaleX(.985) scaleY(1.015);}
  100% { transform:scaleX(1)    scaleY(1);    } /* repos, forme normale         */
}

@keyframes gice-press{
  /* La boule du dessous : reçoit le choc, s'enfonce du haut, se détend. */
  0%   { transform:scaleX(1)    scaleY(1);    }
  22%  { transform:scaleX(1.10) scaleY(.90);  } /* tassée par le poids du dessus */
  55%  { transform:scaleX(.97)  scaleY(1.03); } /* léger rebond                  */
  78%  { transform:scaleX(1.02) scaleY(.99);  }
  100% { transform:scaleX(1)    scaleY(1);    }
}

/* =============================================================================
   MODE SCROLL-DRIVEN (.gice-stack--build, piloté par data-gice-scroll)
   -----------------------------------------------------------------------------
   Ici la cascade n'est PAS jouée d'un bloc : chaque boule est posée/retirée
   INDIVIDUELLEMENT selon la progression du scroll (le JS pose/retire la classe
   .is-set boule par boule). On réutilise EXACTEMENT les keyframes fall/squash
   /press existants — seul le déclencheur change (scroll, pas timer).

     état par défaut (pas .is-set) → boule cachée en haut, pré-étirée (= armée)
     .is-set                       → la boule tombe + s'écrase (fall + squash)
     .is-pressed (boule du dessous)→ tassement au moment de l'impact

   Aucune propriété layout animée : translate/scale/opacity only.
============================================================================= */

/* Repos d'une boule NON encore posée : invisible, en haut, pré-stretchée. */
.gice-stack--build .gice-scoop:not(.is-set) .gice-fall{
  opacity:0;
  transform:translateY(-130px) translateX(var(--dx-from,0px)) rotate(var(--rot-from,0deg));
}
.gice-stack--build .gice-scoop:not(.is-set) .gice-squash{ transform:scaleX(.90) scaleY(1.14); }

/* Boule posée par le scroll : rejoue la chute + le squash d'impact (une fois). */
.gice-stack--build .gice-scoop.is-set .gice-fall{
  animation:gice-fall var(--fall-dur,.46s) cubic-bezier(.55,.06,.78,.36) both;
}
.gice-stack--build .gice-scoop.is-set .gice-squash{
  animation:gice-squash var(--squash-dur,.62s) cubic-bezier(.22,1,.36,1) var(--fall-dur,.46s) both;
}
/* Tassement de la boule du dessous au contact (posé par le JS). */
.gice-stack--build .gice-scoop.is-set.is-pressed .gice-press{
  animation:gice-press var(--press-dur,.5s) cubic-bezier(.22,1,.36,1) forwards;
}

/* Si le moteur scroll est indisponible (no-JS) : pile complète, statique. */
.no-js .gice-stack--build .gice-scoop .gice-fall,
.no-js .gice-stack--build .gice-scoop .gice-squash{ opacity:1; transform:none; }

/* -----------------------------------------------------------------------------
   ACCESSIBILITÉ — prefers-reduced-motion : pile finale STATIQUE, zéro mouvement.
----------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  .gice-stack.is-armed .gice-fall,
  .gice-stack.is-armed .gice-squash{ opacity:1; transform:none; }
  .gice-stack.is-playing .gice-scoop .gice-fall,
  .gice-stack.is-playing .gice-scoop .gice-squash,
  .gice-stack .gice-scoop.is-pressed .gice-squash{ animation:none; transform:none; opacity:1; }
  .gice-fall,.gice-squash{ will-change:auto; }

  /* Mode scroll : on ignore .is-set / .is-pressed → cornet complet, figé. */
  .gice-stack--build .gice-scoop .gice-fall,
  .gice-stack--build .gice-scoop:not(.is-set) .gice-fall,
  .gice-stack--build .gice-scoop .gice-squash,
  .gice-stack--build .gice-scoop:not(.is-set) .gice-squash{
    animation:none; transform:none; opacity:1;
  }
}
