/* STORIES — feuille d'ANIMATIONS MAISON (feature Animations).
 *
 * Chaque @keyframes correspond à un effet du catalogue storyAnim.js (champ `css`). Ils sont
 * REJOUÉS en CSS/DOM par le viewer (A1/A2) : un calque animé devient un élément DOM qui reçoit
 * une `animation:` référençant l'un de ces noms.
 *
 * ⭐ RÈGLE PERF (S21, cf web.dev) : ces keyframes n'animent QUE `transform` et `opacity` — les
 * deux seules propriétés composées par le GPU sur un thread séparé (fluides même si le JS est
 * occupé). JAMAIS width/top/left/box-shadow/filter animés ici (déclencheraient layout/paint).
 * Exception assumée : `sa-neon` anime `text-shadow` (glow) — coûteux mais court et ponctuel
 * (apparition texte), pas une boucle ; acceptable, à surveiller sur S21.
 *
 * ⭐ LICENCE : feuille MAISON (décision user). Les keyframes classiques (fadeIn/pulse/tada…) sont
 * des gestes d'animation triviaux et non protégeables ; on n'a repris NI le fichier NI le nom
 * d'Animate.css (Hippocratic-2.1). Préfixe `sa-` (stories-anim) → aucune collision avec l'app.
 *
 * ⭐ EXTENSIBILITÉ : ajouter un effet = une entrée dans storyAnim.js + un @keyframes ici. Le
 * moteur de rejeu et le panneau ne changent pas. (burst/roll et autres effets maison viendront
 * ainsi — cf mémoire project_stories_animations §A5.)
 *
 * NB : la CLASSE d'application, les variables de durée/délai et la gestion du retrigger (reflow)
 * seront posées en A2 (moteur de rejeu). Ce fichier ne contient que les @keyframes (le socle).
 */

/* ── APPARITION (show) ─────────────────────────────────────────────────────── */
@keyframes sa-fadeIn      { from { opacity: 0; }                             to { opacity: 1; } }
@keyframes sa-fadeInUp    { from { opacity: 0; transform: translateY(12%); } to { opacity: 1; transform: none; } }
@keyframes sa-zoomIn      { from { opacity: 0; transform: scale(0.4); }      to { opacity: 1; transform: scale(1); } }
@keyframes sa-bounceIn {
  0%   { opacity: 0; transform: scale(0.3); }
  55%  { opacity: 1; transform: scale(1.08); }
  72%  { transform: scale(0.95); }
  100% { transform: scale(1); }
}
@keyframes sa-rotateIn    { from { opacity: 0; transform: rotate(-160deg) scale(0.6); } to { opacity: 1; transform: none; } }
@keyframes sa-slideInLeft { from { opacity: 0; transform: translateX(-40%); } to { opacity: 1; transform: none; } }

/* Effets MAISON (apparition) */
/* driftIn — « pose de carte » : glisse du bas avec une rotation qui se redresse. */
@keyframes sa-driftIn {
  0%   { opacity: 0; transform: translateY(22%) rotate(-6deg); }
  70%  { opacity: 1; transform: translateY(-2%) rotate(1.5deg); }
  100% { opacity: 1; transform: none; }
}
/* popShine — pop pétillant : dépassement + brève montée d'éclat (opacity qui pulse à l'arrivée). */
@keyframes sa-popShine {
  0%   { opacity: 0; transform: scale(0.5); }
  45%  { opacity: 1; transform: scale(1.14); }
  62%  { opacity: 0.85; transform: scale(0.96); }
  80%  { opacity: 1; transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}

/* neon — texte : ALLUMAGE D'ENSEIGNE (le tube qui peine à s'amorcer puis s'établit). ⭐ Le CŒUR reste
 * BLANC EN PERMANENCE (color n'est PAS animé → aucun conflit avec l'inline blanc de repos ; c'était
 * la cause du scintillement coloré↔blanc). C'est le HALO qui VACILLE (via opacity), comme le gaz d'un
 * néon qui s'amorce : coupures franches puis le glow qui monte et se stabilise. Le halo utilise
 * var(--sa-neon-color)/var(--sa-neon-blur) (posés par fillDomTextLayer) → couleur choisie. Le rendu
 * FINAL (100%) = exactement l'état de repos néon (blanc + halo), donc anim seule OU anim + statique
 * finissent identiques, sans bascule. On anime `opacity` du node : le texte blanc reste, seul son
 * éclat (halo) papillote — mais comme text-shadow ne s'anime pas en opacity, on module l'INTENSITÉ
 * du halo en variant sa présence (none ↔ halo) sur des paliers, cœur blanc figé. */
/* ⭐ Coupures FRANCHES : text-shadow s'interpole en douceur entre 2 keyframes → un fondu quasi
 * invisible (retour user : « vacillement ultra court/invisible »). On DOUBLE chaque palier (deux %
 * quasi collés avec la MÊME valeur) → l'état est TENU, puis le changement se fait sur l'intervalle
 * SUIVANT très court = un vrai saut on/off perceptible, indépendant de l'easing. Le halo plein
 * (état final) est atteint tôt (55%) puis MAINTENU jusqu'à 100% pour qu'on voie bien l'enseigne
 * établie avant la fin de l'apparition. `--neon-on` = le halo ; défini via variable pour ne pas
 * répéter la valeur. */
/* ⭐ ALLUMAGE ÉTALÉ (retour user : « multiples flashs ultra rapides, ne dure pas 1,3 s »). Avant les
 * flashs étaient tous concentrés dans les 400 premières ms (0-31%) → rafale rapide puis plus rien.
 * Ici les coupures sont ESPACÉES sur toute la durée : des temps « allumé » LONGS entre de brèves
 * coupures (un néon qui hésite lentement), la montée du halo est PROGRESSIVE, et l'enseigne n'est
 * pleinement établie qu'à ~85%. Coupures franches (paliers doublés) mais bien réparties dans le temps. */
@keyframes sa-neon {
  0%,   8%  { text-shadow: none; }                                                             /* éteint */
  9%,  22%  { text-shadow: 0 0 3px #fff, 0 0 calc(var(--sa-neon-blur) * 0.6) var(--sa-neon-color); } /* 1er allumage faible, TENU longtemps */
  23%, 30%  { text-shadow: none; }                                                             /* coupure */
  31%, 52%  { text-shadow: 0 0 3px #fff, 0 0 var(--sa-neon-blur) var(--sa-neon-color); }        /* re-allumage franc, TENU longtemps */
  54%, 60%  { text-shadow: none; }                                                             /* dernière hésitation */
  62%, 85%  { text-shadow: 0 0 3px #fff, 0 0 var(--sa-neon-blur) var(--sa-neon-color); }        /* ça tient, monte */
  85%, 100% { text-shadow: 0 0 3px #fff, 0 0 var(--sa-neon-blur) var(--sa-neon-color),
                           0 0 calc(var(--sa-neon-blur) * 2) var(--sa-neon-color); }            /* établi : halo plein */
}

/* ── MOUVEMENT (move) : attention/emphase, boucle ou une fois ───────────────── */
@keyframes sa-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.14); } }
@keyframes sa-tada {
  0%   { transform: scale(1); }
  10%, 20% { transform: scale(0.9) rotate(-4deg); }
  30%, 50%, 70%, 90% { transform: scale(1.1) rotate(4deg); }
  40%, 60%, 80%      { transform: scale(1.1) rotate(-4deg); }
  100% { transform: scale(1) rotate(0); }
}
@keyframes sa-shake { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-6px); } 40%, 80% { transform: translateX(6px); } }
@keyframes sa-wobble {
  0%, 100% { transform: none; }
  25% { transform: translateX(-10%) rotate(-4deg); }
  50% { transform: translateX(7%)  rotate(3deg); }
  75% { transform: translateX(-4%) rotate(-2deg); }
}
@keyframes sa-jello {
  0%, 100% { transform: none; }
  30% { transform: skewX(-10deg) skewY(-6deg); }
  45% { transform: skewX(7deg)  skewY(4deg); }
  60% { transform: skewX(-4deg) skewY(-2deg); }
  75% { transform: skewX(2deg)  skewY(1deg); }
}
@keyframes sa-heartBeat {
  0%, 100% { transform: scale(1); }
  14% { transform: scale(1.22); }
  28% { transform: scale(1); }
  42% { transform: scale(1.22); }
  70% { transform: scale(1); }
}

/* Effets MAISON (mouvement continu, doux) */
/* float — flottement vertical lent (élégant pour un sticker/emoji). */
@keyframes sa-float { 0%, 100% { transform: translateY(-4%); } 50% { transform: translateY(4%); } }
/* breathe — respiration très subtile (texte « vivant » sans agitation). */
@keyframes sa-breathe { 0%, 100% { transform: scale(0.985); } 50% { transform: scale(1.015); } }

/* ── DISPARITION (hide) : joue à un moment choisi avant la fin ──────────────── */
@keyframes sa-fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes sa-flyOut  { from { opacity: 1; transform: none; } to { opacity: 0; transform: translate(50%, -35%) scale(0.7); } }
@keyframes sa-zoomOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.4); } }

/* Curseur clignotant de l'effet « machine à écrire » (reveal char). Un fin trait vertical qui
 * suit la frappe, retiré à la fin de la révélation (runtime storyAnimPlayer). */
.sa-caret {
  display: inline-block;
  width: 0.08em;
  height: 1em;
  margin-left: 0.04em;
  background: currentColor;
  vertical-align: -0.1em;
  animation: sa-caret-blink 0.7s steps(1) infinite;
}
@keyframes sa-caret-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

/* Accessibilité : respecter la préférence « animations réduites ». Le runtime (storyAnimPlayer)
 * ne pose PAS d'animation quand cette préférence est active (à gérer côté JS) ; ce garde-fou CSS
 * neutralise en plus toute animation résiduelle sur un calque animé et fige le curseur. */
@media (prefers-reduced-motion: reduce) {
  .story-dom-layer { animation: none !important; }
  .sa-caret { animation: none !important; opacity: 1; }
}
