/* ============================================================================
   BIBLIOTHEQUE UI JABS  -  anim.css
   Depend de : theme.css, buttons.css

   L'animation d'interface, par opposition au mouvement au defilement
   (motion.css). Ici, tout part d'une ACTION : un clic, une saisie, l'arrivee
   d'une reponse du serveur.

   Cinq regles tenues partout dans ce fichier :
   1. On n'anime que `transform` et `opacity`. Animer `width`, `top` ou
      `margin` force le navigateur a recalculer la mise en page a chaque
      image : c'est la premiere cause de saccade.
   2. Une transition d'interface dure 120 a 320 ms. Au-dela, elle se ressent
      comme une lenteur, pas comme une elegance.
   3. La courbe compte plus que la duree. Une sortie rapide puis freinee
      (`cubic-bezier(.2,0,0,1)`) parait juste ; `ease` parait mou.
   4. Ce qui entre et ce qui sort n'ont pas la meme duree : l'entree est plus
      lente que la sortie, parce qu'on regarde ce qui arrive.
   5. `prefers-reduced-motion` supprime le mouvement, jamais l'information :
      l'etat final reste atteint, instantanement.
   ========================================================================= */

/* Courbes de reference, nommees une fois pour tout le fichier. */
:root {
  --e-sortie:  cubic-bezier(.3, 0, 1, 1);      /* part vite, sans retour */
  --e-entree:  cubic-bezier(0, 0, .2, 1);      /* arrive en freinant */
  --e-douce:   cubic-bezier(.2, 0, 0, 1);      /* la plus utile, par defaut */
  --e-ressort: cubic-bezier(.34, 1.56, .64, 1);/* depasse puis revient */
}

/* ===========================================================================
   FAMILLE A  -  MICRO-INTERACTIONS
   ======================================================================== */

/* @v ANI-A1
@nom Icone qui se transforme
@usage Le menu qui devient croix, la lecture qui devient pause. Les deux etats sont le MEME objet : c'est ce qui fait comprendre que le bouton refermera ce qu'il vient d'ouvrir.
@html
<div class="demo-row">
  <button class="btn btn--icone btn--secondaire an-burger" aria-label="Ouvrir le menu" aria-expanded="false"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-burger" aria-label="Fermer le menu" aria-expanded="true"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-lecture" aria-label="Lecture" aria-pressed="false"><i></i></button>
  <button class="btn btn--icone btn--secondaire an-lecture" aria-label="Pause" aria-pressed="true"><i></i></button>
</div>
@tw les trois barres pivotent et se rejoignent ; aucune icone n'est remplacee
@end */
.an-burger i, .an-burger i::before, .an-burger i::after {
  display: block;
  width: 17px; height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 260ms var(--e-douce), opacity 160ms var(--e-douce);
}
.an-burger i { position: relative; }
.an-burger i::before, .an-burger i::after { content: ""; position: absolute; left: 0; }
.an-burger i::before { top: -6px; }
.an-burger i::after  { top: 6px; }
.an-burger[aria-expanded="true"] i { transform: rotate(45deg); }
.an-burger[aria-expanded="true"] i::before { transform: translateY(6px) rotate(-90deg); }
.an-burger[aria-expanded="true"] i::after  { opacity: 0; transform: scaleX(0); }

/* Lecture / pause : deux barres qui se rejoignent en triangle.
   Chacune porte un clip-path : accolees et decoupees en biseau elles
   forment le triangle, ecartees et pleines elles forment la pause. */
.an-lecture i { position: relative; display: block; width: 14px; height: 15px; }
.an-lecture i::before, .an-lecture i::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 5px;
  background: currentColor;
  border-radius: 1px;
  transition: clip-path 280ms var(--e-douce), transform 280ms var(--e-douce), border-radius 280ms var(--e-douce);
}
.an-lecture i::before { left: 0; }
.an-lecture i::after  { left: 9px; }
.an-lecture[aria-pressed="false"] i::before {
  clip-path: polygon(0 0, 100% 22%, 100% 78%, 0 100%);
  border-radius: 1px 0 0 1px;
}
.an-lecture[aria-pressed="false"] i::after {
  clip-path: polygon(0 22%, 100% 50%, 100% 50%, 0 78%);
  transform: translateX(-4px);
  border-radius: 0;
}
/* @endv */

/* @v ANI-A2
@nom Coche qui se trace
@usage La confirmation d'une action reussie. Le trait se dessine au lieu d'apparaitre : le regard suit le geste et comprend que quelque chose vient de se terminer.
@html
<div class="demo-row">
  <span class="an-coche" data-an-rejouer><svg viewBox="0 0 42 42" aria-hidden="true"><circle cx="21" cy="21" r="19"/><path d="M12 21.5 18.5 28 30 15.5"/></svg></span>
  <span class="an-coche an-coche--ko" data-an-rejouer><svg viewBox="0 0 42 42" aria-hidden="true"><circle cx="21" cy="21" r="19"/><path d="M14 14 28 28M28 14 14 28"/></svg></span>
</div>
@tw stroke-dasharray + stroke-dashoffset animes de la longueur du trace a zero
@end */
.an-coche svg { width: 54px; height: 54px; overflow: visible; }
.an-coche circle, .an-coche path {
  fill: none;
  stroke: var(--c-success);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* La longueur exacte du trace sert de dasharray : le trait se dessine. */
.an-coche circle { stroke-dasharray: 120; stroke-dashoffset: 120; animation: an-tracer 620ms var(--e-douce) forwards; }
.an-coche path   { stroke-dasharray: 40;  stroke-dashoffset: 40;  animation: an-tracer 320ms var(--e-douce) 380ms forwards; }
@keyframes an-tracer { to { stroke-dashoffset: 0; } }
.an-coche--ko circle, .an-coche--ko path { stroke: var(--c-danger); }
/* @endv */

/* @v ANI-A3
@nom Case et interrupteur
@usage Le retour le plus frequent d'une interface. La coche se trace, la poignee s'etire pendant le glissement : deux details qui rendent le controle tangible.
@html
<div class="demo-col">
  <label class="coche an-coche-anim"><input type="checkbox"><span class="coche__boite"></span> Recevoir les alertes</label>
  <label class="coche an-coche-anim"><input type="checkbox" checked><span class="coche__boite"></span> Relances automatiques</label>
  <label class="bascule"><input type="checkbox" checked><span class="bascule__piste"></span> Signature obligatoire</label>
</div>
@tw la boite passe en scale(.85) au clic puis revient : le controle repond au doigt
@end */
.an-coche-anim .coche__boite { transition: background-color 160ms var(--e-douce), border-color 160ms var(--e-douce), transform 200ms var(--e-ressort); }
.an-coche-anim:active .coche__boite { transform: scale(.86); }
.an-coche-anim .coche__boite::after { transition: transform 260ms var(--e-ressort) 40ms; }
/* @endv */

/* @v ANI-A4
@nom Bouton favori
@usage Le geste le plus copie du web, et pour une raison : le depassement d'echelle donne une sensation de declic que la couleur seule ne produit pas.
@html
<div class="demo-row">
  <button class="btn btn--icone btn--secondaire an-coeur" aria-pressed="false" aria-label="Ajouter aux favoris">{{icon:star}}</button>
  <button class="btn btn--icone btn--secondaire an-coeur" aria-pressed="true" aria-label="Retirer des favoris">{{icon:star}}</button>
  <button class="btn btn--secondaire an-coeur" aria-pressed="true">{{icon:star}} 24</button>
</div>
@tw scale(1.35) puis retour en cubic-bezier(.34,1.56,.64,1)
@end */
.an-coeur svg { transition: transform 340ms var(--e-ressort), fill 200ms var(--e-douce); }
.an-coeur[aria-pressed="true"] { border-color: var(--c-warn); color: var(--c-warn); background: var(--c-warn-soft); }
.an-coeur[aria-pressed="true"] svg { fill: currentColor; animation: an-declic 420ms var(--e-ressort); }
@keyframes an-declic {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.38); }
  100% { transform: scale(1); }
}
/* @endv */

/* @v ANI-A5
@nom Champ qui prend le focus
@usage L'anneau grandit depuis le bord au lieu d'apparaitre d'un coup, et l'intitule remonte. Deux details qui font percevoir le champ comme actif plutot que simplement surligne.
@html
<div class="demo-col demo-col--large">
  <label class="an-champ"><input class="saisie" placeholder=" "><span>Nom du client</span></label>
  <label class="an-champ"><input class="saisie" value="Krissi Julien" placeholder=" "><span>Nom du client</span></label>
</div>
@tw label en translate + scale, anneau en box-shadow anime depuis 0
@end */
.an-champ { position: relative; display: block; }
.an-champ .saisie {
  padding-top: 16px;
  transition: box-shadow 180ms var(--e-douce), border-color 140ms var(--e-douce);
  height: 52px;
}
.an-champ span {
  position: absolute;
  left: 13px; top: 16px;
  font-size: var(--fs-md);
  color: var(--c-text-muted);
  pointer-events: none;
  transform-origin: 0 0;
  transition: transform 200ms var(--e-douce), color 200ms var(--e-douce);
}
.an-champ .saisie:focus + span,
.an-champ .saisie:not(:placeholder-shown) + span { transform: translateY(-9px) scale(.78); }
.an-champ .saisie:focus + span { color: var(--c-primary); }
/* @endv */

/* @v ANI-A6
@nom Bouton d'envoi
@usage Le parcours complet d'une action : repos, envoi, succes, retour au repos. La largeur ne bouge pas d'un pixel, sinon la mise en page saute a chaque etape.
@html
<div class="demo-row">
  <button class="btn btn--plein an-envoi">Envoyer le devis</button>
  <button class="btn btn--plein an-envoi is-encours"><span class="btn__spin"></span>Envoi</button>
  <button class="btn btn--plein an-envoi is-fait">{{icon:check}} Envoye</button>
</div>
@tw min-width figee avant le changement d'etat, transition sur background-color
@end */
.an-envoi { min-width: 160px; transition: background-color 260ms var(--e-douce), transform 140ms var(--e-douce); }
.an-envoi.is-encours { pointer-events: none; opacity: .9; }
.an-envoi.is-fait { background: var(--c-success); pointer-events: none; }
.an-envoi.is-fait svg { animation: an-declic 420ms var(--e-ressort); }
/* @endv */

/* ===========================================================================
   FAMILLE B  -  TRANSITIONS D'ETAT
   ======================================================================== */

/* @v ANI-B1
@nom Accordeon fluide
@usage Le repliement le plus demande et le plus souvent rate. `interpolate-size` rend `height: auto` animable : plus besoin de mesurer la hauteur en JavaScript.
@large
@html
<div class="an-accordeon">
  <details open><summary>Quelles aides puis-je obtenir ?<i></i></summary><div><p>MaPrimeRenov, les CEE et la TVA a 5,5 % selon vos revenus et le logement. Nous montons le dossier et deduisons les aides du devis.</p></div></details>
  <details><summary>Combien de temps dure la pose ?<i></i></summary><div><p>Une journee pour un chauffe-eau, deux a trois jours pour une pompe a chaleur avec depose de l'ancienne chaudiere.</p></div></details>
  <details><summary>Intervenez-vous en urgence ?<i></i></summary><div><p>Oui, 7 jours sur 7, avec un delai moyen de 2 heures en Ile-de-France.</p></div></details>
</div>
@tw interpolate-size: allow-keywords + transition-behavior: allow-discrete
@end */
/* Debloque l'animation vers `auto`. Sans cette ligne, la hauteur saute. */
.an-accordeon { interpolate-size: allow-keywords; }
.an-accordeon details { border-bottom: 1px solid var(--c-border); }
.an-accordeon summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semi);
  cursor: pointer;
  list-style: none;
}
.an-accordeon summary::-webkit-details-marker { display: none; }
.an-accordeon summary i {
  position: relative;
  width: 14px; height: 14px;
  flex: none;
}
.an-accordeon summary i::before, .an-accordeon summary i::after {
  content: "";
  position: absolute;
  inset: 6px 0;
  background: var(--c-text-muted);
  border-radius: 1px;
  transition: transform 280ms var(--e-douce);
}
.an-accordeon summary i::after { transform: rotate(90deg); }
.an-accordeon details[open] summary i::after { transform: rotate(0); }
.an-accordeon details::details-content {
  block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition: block-size 320ms var(--e-douce), opacity 240ms var(--e-douce), content-visibility 320ms allow-discrete;
}
.an-accordeon details[open]::details-content { block-size: auto; opacity: 1; }
.an-accordeon p { margin: 0 0 var(--sp-4); font-size: var(--fs-md); line-height: 1.65; color: var(--c-text-soft); max-width: 68ch; }
/* Repli pour les navigateurs sans ::details-content : le contenu apparait
   sans animation, ce qui reste parfaitement utilisable. */
@supports not selector(details::details-content) {
  .an-accordeon details > div { animation: an-deplier 300ms var(--e-douce); }
  @keyframes an-deplier { from { opacity: 0; transform: translateY(-6px); } }
}
/* @endv */

/* @v ANI-B2
@nom Indicateur d'onglet glissant
@usage Le trait glisse d'un onglet a l'autre au lieu de sauter. Il montre le chemin parcouru, ce qui aide a comprendre ou l'on va dans une serie d'etapes.
@large
@html
<div class="an-onglets" style="--i:1;--n:4" data-an-onglets>
  <button aria-selected="false">Resume</button>
  <button aria-selected="true">Documents</button>
  <button aria-selected="false">Interventions</button>
  <button aria-selected="false">Facturation</button>
  <span class="an-onglets__trait"></span>
</div>
@tw translateX(calc(var(--i) * 100%)) sur un trait de largeur 1/n
@end */
.an-onglets { position: relative; display: flex; border-bottom: 1px solid var(--c-border); }
.an-onglets button {
  flex: 1;
  padding: var(--sp-3);
  border: none;
  background: transparent;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--c-text-muted);
  cursor: pointer;
  transition: color 200ms var(--e-douce);
}
.an-onglets button[aria-selected="true"] { color: var(--c-primary-ink); font-weight: var(--fw-semi); }
.an-onglets__trait {
  position: absolute;
  bottom: -1px; left: 0;
  height: 2px;
  width: calc(100% / var(--n));
  border-radius: 2px 2px 0 0;
  background: var(--c-primary);
  transform: translateX(calc(var(--i) * 100%));
  transition: transform 320ms var(--e-douce);
}
/* @endv */

/* @v ANI-B3
@nom Entree d'une modale
@usage `@starting-style` permet d'animer l'apparition d'un element qui vient d'etre insere, sans classe ajoutee en JavaScript ni double `requestAnimationFrame`.
@large
@html
<div class="modale-scene an-modale-scene">
  <div class="modale__voile"></div>
  <div class="modale an-modale">
    <div class="modale__tete"><h3>Envoyer le devis</h3><button class="btn btn--fermer" aria-label="Fermer">{{icon:x}}</button></div>
    <div class="modale__corps"><p>Le voile se fond, la fenetre monte de 12 px en grandissant depuis 0,96.</p></div>
    <div class="modale__pied"><button class="btn btn--secondaire">Annuler</button><button class="btn btn--plein">Envoyer</button></div>
  </div>
</div>
@tw @starting-style { opacity: 0; transform: translateY(12px) scale(.96) }
@end */
.an-modale {
  animation: an-modale-entree 280ms var(--e-douce) both;
}
@keyframes an-modale-entree {
  from { opacity: 0; transform: translateY(12px) scale(.96); }
}
.an-modale-scene .modale__voile { animation: an-fondu 240ms var(--e-douce) both; }
@keyframes an-fondu { from { opacity: 0; } }
/* Sur un vrai <dialog> ou un popover, @starting-style evite toute classe JS :
   dialog[open] { opacity: 1 } @starting-style { dialog[open] { opacity: 0 } } */
/* @endv */

/* @v ANI-B4
@nom Reordonnancement de liste
@usage La technique FLIP : on mesure les positions avant et apres, on applique la difference a rebours, puis on la laisse revenir a zero. C'est ce qui permet d'animer un changement de mise en page sans animer la mise en page.
@large
@html
<div class="an-flip" data-an-flip>
  <div class="demo-row" style="margin-bottom:12px">
    <button class="btn btn--secondaire btn--sm" data-an-melanger>Reordonner</button>
    <span class="champ__aide">Les lignes glissent vers leur nouvelle place.</span>
  </div>
  <ul class="liste">
    <li data-cle="a"><a href="#"><span class="personne__avatar">JK</span><span class="liste__bloc"><strong>Krissi Julien</strong><small>Neuilly-sur-Marne</small></span><strong>4 280 EUR</strong></a></li>
    <li data-cle="b"><a href="#"><span class="personne__avatar personne__avatar--vert">MS</span><span class="liste__bloc"><strong>Martin Sophie</strong><small>Colombes</small></span><strong>1 890 EUR</strong></a></li>
    <li data-cle="c"><a href="#"><span class="personne__avatar">DP</span><span class="liste__bloc"><strong>Durand Paul</strong><small>Le Raincy</small></span><strong>12 400 EUR</strong></a></li>
    <li data-cle="d"><a href="#"><span class="personne__avatar personne__avatar--vert">BA</span><span class="liste__bloc"><strong>Bernard Alice</strong><small>Gagny</small></span><strong>6 750 EUR</strong></a></li>
  </ul>
</div>
@tw JS : First, Last, Invert, Play (voir js/anim.js)
@end */
.an-flip li { transition: transform 420ms var(--e-douce); }
/* @endv */

/* @v ANI-B5
@nom Suppression d'une ligne
@usage La ligne se retire vers la droite puis sa hauteur se referme. En deux temps : sans cela, les lignes du dessous sautent avant que l'oeil ait compris ce qui partait.
@large
@html
<div class="an-suppr" data-an-suppr>
  <ul class="liste">
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-103</strong><small>Signe le 12 aout</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer">{{icon:x}}</button></a></li>
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-118</strong><small>Envoye le 29 aout</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer">{{icon:x}}</button></a></li>
    <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-121</strong><small>Brouillon</small></span><button class="btn btn--icone btn--fantome btn--sm" data-an-retirer aria-label="Retirer">{{icon:x}}</button></a></li>
  </ul>
  <button class="btn btn--secondaire btn--sm" data-an-restaurer style="margin-top:12px">Restaurer</button>
</div>
@tw etape 1 : translateX + opacity ; etape 2 : grid-template-rows 1fr vers 0fr
@end */
.an-suppr li { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 280ms var(--e-douce) 180ms; }
.an-suppr li > * { overflow: hidden; }
.an-suppr li.part > a { animation: an-partir 200ms var(--e-sortie) both; }
.an-suppr li.part { grid-template-rows: 0fr; }
@keyframes an-partir { to { opacity: 0; transform: translateX(34px); } }
/* @endv */

/* @v ANI-B6
@nom Bandeau qui s'ouvre
@usage Une alerte inseree en haut de page. Elle pousse le contenu au lieu de le recouvrir, et la poussee est animee : personne ne perd sa ligne de lecture.
@large
@html
<div class="an-bandeau" data-an-bandeau>
  <button class="btn btn--secondaire btn--sm" data-an-basculer>Afficher ou masquer</button>
  <div class="an-bandeau__zone">
    <div class="alerte alerte--attention"><span>{{icon:alerte}}</span><div><strong>3 devis attendent une relance</strong><p>Envoyes il y a plus de 7 jours.</p></div></div>
  </div>
  <p class="mv-note">Le contenu suivant est pousse vers le bas, sans saut.</p>
</div>
@tw grid-template-rows: 0fr vers 1fr, la seule facon d'animer une hauteur inconnue
@end */
.an-bandeau__zone {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 320ms var(--e-douce), margin 320ms var(--e-douce);
  margin: var(--sp-3) 0;
}
.an-bandeau__zone > * { overflow: hidden; }
.an-bandeau.is-ferme .an-bandeau__zone { grid-template-rows: 0fr; margin: 0; }
/* @endv */

/* ===========================================================================
   FAMILLE C  -  TRACES ET FORMES
   ======================================================================== */

/* @v ANI-C1
@nom Trace de ligne
@usage Un logo, une signature ou un pictogramme qui se dessine. Le trace suit l'ordre des chemins dans le SVG : c'est lui qui donne l'impression d'une main qui ecrit.
@large
@html
<div class="demo-row" data-an-rejouer>
  <svg class="an-trace" viewBox="0 0 120 60" aria-hidden="true">
    <path d="M8 44 L8 16 L26 44 L26 16"/><path d="M40 16 L40 44 L58 44"/>
    <path d="M72 16 L72 44"/><path d="M86 16 Q104 16 104 30 Q104 44 86 44 L86 16"/>
  </svg>
  <svg class="an-trace an-trace--signature" viewBox="0 0 140 50" aria-hidden="true">
    <path d="M8 36 Q22 6 32 30 Q40 48 50 20 Q58 -2 66 32 Q72 46 84 26 Q96 8 104 30 Q110 44 132 18"/>
  </svg>
</div>
@tw stroke-dasharray: 1 ; stroke-dashoffset anime avec pathLength="1"
@end */
.an-trace { height: 62px; overflow: visible; }
.an-trace path {
  fill: none;
  stroke: var(--c-primary);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* pathLength normalise la longueur a 1 : plus besoin de mesurer le trace. */
  pathLength: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: an-tracer1 1.5s var(--e-douce) forwards;
}
.an-trace path:nth-child(2) { animation-delay: .3s; }
.an-trace path:nth-child(3) { animation-delay: .6s; }
.an-trace path:nth-child(4) { animation-delay: .8s; }
@keyframes an-tracer1 { to { stroke-dashoffset: 0; } }
.an-trace--signature path { stroke: var(--c-text); stroke-width: 2.4; animation-duration: 2.2s; }
/* @endv */

/* @v ANI-C2
@nom Jauge circulaire
@usage Un taux qui se remplit a l'arrivee. La valeur chiffree est ecrite dans le balisage : elle reste juste si l'animation ne joue pas.
@html
<div class="demo-row">
  <span class="an-jauge" style="--p:.74"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>74 %</em></span>
  <span class="an-jauge an-jauge--ok" style="--p:1"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>100 %</em></span>
  <span class="an-jauge an-jauge--bas" style="--p:.28"><svg viewBox="0 0 44 44" aria-hidden="true"><circle cx="22" cy="22" r="19"/><circle cx="22" cy="22" r="19" class="v"/></svg><em>28 %</em></span>
</div>
@tw pathLength="1" + stroke-dashoffset: calc(1 - var(--p))
@end */
.an-jauge { position: relative; display: inline-grid; place-items: center; width: 84px; height: 84px; }
.an-jauge svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.an-jauge circle { fill: none; stroke-width: 4; stroke: var(--c-surface-3); }
.an-jauge .v {
  stroke: var(--c-primary);
  stroke-linecap: round;
  pathLength: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: an-remplir 1.1s var(--e-douce) forwards;
}
@keyframes an-remplir { to { stroke-dashoffset: calc(1 - var(--p)); } }
.an-jauge em { font-style: normal; font-size: var(--fs-md); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; }
.an-jauge--ok .v  { stroke: var(--c-success); }
.an-jauge--bas .v { stroke: var(--c-danger); }
/* @endv */

/* @v ANI-C3
@nom Barres qui montent
@usage Un histogramme qui se construit a l'arrivee dans le champ. Les barres partent de zero et se decalent legerement : le decalage suffit a donner un rythme.
@large
@html
<div class="an-barres" data-an-rejouer>
  <div style="--h:42%;--i:0"><i></i><small>avr</small></div>
  <div style="--h:58%;--i:1"><i></i><small>mai</small></div>
  <div style="--h:51%;--i:2"><i></i><small>juin</small></div>
  <div style="--h:74%;--i:3"><i></i><small>juil</small></div>
  <div style="--h:63%;--i:4"><i></i><small>aout</small></div>
  <div style="--h:88%;--i:5" class="c"><i></i><small>sept</small></div>
</div>
@tw transform-origin: bottom + scaleY(0) vers scaleY(1), decale de 60 ms par barre
@end */
.an-barres { display: flex; align-items: flex-end; gap: var(--sp-3); height: 150px; width: 100%; }
.an-barres > div { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px; height: 100%; }
.an-barres i {
  display: block;
  width: 100%;
  height: var(--h);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  background: var(--c-primary-soft);
  transform-origin: 50% 100%;
  /* scaleY plutot que height : aucun recalcul de mise en page. */
  animation: an-monter 700ms var(--e-douce) both;
  animation-delay: calc(var(--i) * 70ms);
}
.an-barres .c i { background: var(--c-primary); }
@keyframes an-monter { from { transform: scaleY(0); } }
.an-barres small { font-size: var(--fs-xs); color: var(--c-text-muted); }
/* @endv */

/* @v ANI-C4
@nom Forme qui se deforme
@usage Un fond organique qui respire. Les valeurs de `border-radius` sont interpolees entre trois etats : le mouvement parait continu sans jamais boucler visiblement.
@html
<div class="demo-row">
  <span class="an-blob"></span>
  <span class="an-blob an-blob--2"></span>
  <span class="an-blob an-blob--3"></span>
</div>
@tw huit valeurs de border-radius animees en boucle, durees premieres entre elles
@end */
.an-blob {
  width: 110px; height: 110px;
  background: linear-gradient(140deg, #d8853f, #a8543c);
  animation: an-morph 11s ease-in-out infinite;
}
.an-blob--2 { background: linear-gradient(140deg, #3f7ea8, #3a4a8a); animation-duration: 13s; animation-direction: reverse; }
.an-blob--3 { background: linear-gradient(140deg, #3f9e7a, #2f6b5a); animation-duration: 17s; }
@keyframes an-morph {
  0%, 100% { border-radius: 62% 38% 46% 54% / 54% 46% 54% 46%; }
  33%      { border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; }
  66%      { border-radius: 54% 46% 38% 62% / 62% 54% 46% 38%; }
}
/* @endv */

/* ===========================================================================
   FAMILLE D  -  ATTENTES
   ======================================================================== */

/* @v ANI-D1
@nom Six indicateurs de chargement
@usage Choisir selon la duree : un tourniquet sous 2 secondes, des points jusqu'a 5, une barre au-dela, et un squelette quand la forme du contenu est connue d'avance.
@large
@html
<div class="an-chargements">
  <div><span class="btn__spin an-spin"></span><small>Tourniquet &mdash; moins de 2 s</small></div>
  <div><span class="an-points"><i></i><i></i><i></i></span><small>Points &mdash; jusqu'a 5 s</small></div>
  <div><span class="an-barre"><i></i></span><small>Barre indeterminee &mdash; au-dela</small></div>
  <div><span class="an-pulse"></span><small>Pulsation &mdash; attente passive</small></div>
  <div><span class="an-onde-c"><i></i><i></i><i></i><i></i></span><small>Onde &mdash; traitement par lots</small></div>
  <div><span class="an-squelette"></span><small>Squelette &mdash; forme connue</small></div>
</div>
@tw six formes, une intention chacune ; jamais deux dans le meme ecran
@end */
.an-chargements { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--sp-5); }
.an-chargements > div { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); text-align: center; }
.an-chargements small { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.4; }
.an-spin { width: 26px; height: 26px; border-width: 2.5px; border-color: var(--c-surface-3); border-top-color: var(--c-primary); }

.an-points { display: inline-flex; gap: 6px; height: 26px; align-items: center; }
.an-points i { width: 8px; height: 8px; border-radius: 50%; background: var(--c-primary); animation: an-rebond 1.2s var(--e-douce) infinite; }
.an-points i:nth-child(2) { animation-delay: .15s; }
.an-points i:nth-child(3) { animation-delay: .3s; }
@keyframes an-rebond { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-8px); opacity: 1; } }

.an-barre { display: block; width: 100%; height: 5px; border-radius: var(--r-full); background: var(--c-surface-3); overflow: hidden; }
.an-barre i { display: block; width: 38%; height: 100%; border-radius: inherit; background: var(--c-primary); animation: an-va-vient 1.4s var(--e-douce) infinite; }
@keyframes an-va-vient { 0% { transform: translateX(-105%); } 100% { transform: translateX(300%); } }

.an-pulse { width: 26px; height: 26px; border-radius: 50%; background: var(--c-primary); animation: an-respire 1.6s var(--e-douce) infinite; }
@keyframes an-respire { 0%, 100% { transform: scale(.8); opacity: .45; } 50% { transform: scale(1); opacity: 1; } }

.an-onde-c { display: inline-flex; gap: 4px; height: 26px; align-items: center; }
.an-onde-c i { width: 4px; height: 22px; border-radius: 2px; background: var(--c-primary); transform-origin: 50% 50%; animation: an-onde-b 1s var(--e-douce) infinite; }
.an-onde-c i:nth-child(2) { animation-delay: .1s; }
.an-onde-c i:nth-child(3) { animation-delay: .2s; }
.an-onde-c i:nth-child(4) { animation-delay: .3s; }
@keyframes an-onde-b { 0%, 100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }

.an-squelette {
  display: block; width: 100%; height: 26px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--c-surface-2) 25%, var(--c-surface-3) 50%, var(--c-surface-2) 75%);
  background-size: 200% 100%;
  animation: an-scintille 1.4s ease-in-out infinite;
}
@keyframes an-scintille { to { background-position: -200% 0; } }
/* @endv */

/* @v ANI-D2
@nom Squelette calque sur le contenu
@usage Le squelette prend la forme exacte de ce qui arrive : avatar rond, deux lignes, une valeur a droite. Toute autre forme fait sauter la page a l'arrivee des donnees.
@large
@html
<div class="an-bascule-charge" data-an-charger>
  <button class="btn btn--secondaire btn--sm" data-an-recharger style="margin-bottom:12px">Recharger</button>
  <ul class="liste">
    <li><div class="squel-ligne"><span class="an-squelette squel-rond"></span><span class="squel-bloc"><span class="an-squelette" style="height:13px;width:45%;border-radius:3px"></span><span class="an-squelette" style="height:10px;width:65%;border-radius:3px"></span></span><span class="an-squelette" style="height:13px;width:70px;border-radius:3px"></span></div></li>
    <li><div class="squel-ligne"><span class="an-squelette squel-rond"></span><span class="squel-bloc"><span class="an-squelette" style="height:13px;width:38%;border-radius:3px"></span><span class="an-squelette" style="height:10px;width:55%;border-radius:3px"></span></span><span class="an-squelette" style="height:13px;width:70px;border-radius:3px"></span></div></li>
  </ul>
</div>
@tw memes dimensions que le contenu reel, sinon la page saute
@end */
.an-bascule-charge .liste li { animation: an-fondu-entree 320ms var(--e-douce) both; }
.an-bascule-charge .liste li:nth-child(2) { animation-delay: 60ms; }
@keyframes an-fondu-entree { from { opacity: 0; transform: translateY(6px); } }
/* @endv */

/* @v ANI-D3
@nom Progression par etapes
@usage Un traitement long en plusieurs phases. Le trait de liaison se remplit entre deux etapes : c'est lui qui donne le sentiment d'avancer, plus que la pastille.
@large
@html
<ol class="an-etapes" style="--fait:2">
  <li class="f"><i>{{icon:check}}</i><div><strong>Devis genere</strong><small>Termine</small></div></li>
  <li class="f"><i>{{icon:check}}</i><div><strong>Converti en Factur-X</strong><small>Termine</small></div></li>
  <li class="c"><i><span class="btn__spin an-spin"></span></i><div><strong>Envoi au client</strong><small>En cours</small></div></li>
  <li><i>4</i><div><strong>Attente de signature</strong><small>A venir</small></div></li>
</ol>
@tw le trait de liaison est un ::after en scaleY anime, pas une hauteur
@end */
.an-etapes { list-style: none; margin: 0; padding: 0; }
.an-etapes li { position: relative; display: flex; gap: var(--sp-4); padding-bottom: var(--sp-6); }
.an-etapes li:last-child { padding-bottom: 0; }
.an-etapes li::before {
  content: "";
  position: absolute;
  left: 15px; top: 32px; bottom: 0;
  width: 2px;
  background: var(--c-border);
}
.an-etapes li:last-child::before { display: none; }
.an-etapes li::after {
  content: "";
  position: absolute;
  left: 15px; top: 32px; bottom: 0;
  width: 2px;
  background: var(--c-success);
  transform-origin: 50% 0;
  transform: scaleY(0);
  transition: transform 500ms var(--e-douce);
}
.an-etapes li.f::after { transform: scaleY(1); }
.an-etapes li:nth-child(2).f::after { transition-delay: 300ms; }
.an-etapes li > i {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  border-radius: 50%;
  background: var(--c-surface-3);
  color: var(--c-text-muted);
  font-style: normal; font-size: var(--fs-xs); font-weight: var(--fw-bold);
  z-index: 1;
  transition: background-color 300ms var(--e-douce), color 300ms var(--e-douce);
}
.an-etapes li > i svg { width: 15px; height: 15px; }
.an-etapes li.f > i { background: var(--c-success); color: #fff; }
.an-etapes li.c > i { background: var(--c-primary-soft); }
.an-etapes li.c > i .an-spin { width: 16px; height: 16px; border-width: 2px; }
.an-etapes strong { display: block; font-size: var(--fs-md); font-weight: var(--fw-semi); }
.an-etapes small { font-size: var(--fs-xs); color: var(--c-text-muted); }
/* @endv */

/* ===========================================================================
   FAMILLE E  -  ATTIRER L'ATTENTION
   ======================================================================== */

/* @v ANI-E1
@nom Secousse d'erreur
@usage Un envoi refuse. La secousse est breve et de faible amplitude : au-dela de 6 px et 400 ms, elle passe de « attention » a « agacant ».
@html
<div class="demo-row">
  <button class="btn btn--destructif an-secousse" data-an-rejouer>Mot de passe incorrect</button>
  <div class="champ is-erreur an-secousse" data-an-rejouer style="max-width:220px"><input class="saisie" value="dupont@exemple"></div>
</div>
@tw translateX de +-5 px, 5 alternances, 380 ms
@end */
.an-secousse { animation: an-secouer 400ms var(--e-douce); }
@keyframes an-secouer {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-6px); }
  30% { transform: translateX(5px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  80% { transform: translateX(-2px); }
}
/* @endv */

/* @v ANI-E2
@nom Surlignage d'arrivee
@usage Une ligne qui vient d'etre ajoutee ou modifiee par quelqu'un d'autre. Le fond se teinte puis s'efface en deux secondes : assez pour etre vu, assez court pour ne pas rester.
@large
@html
<ul class="liste" data-an-rejouer>
  <li class="an-neuf"><a href="#"><span class="liste__bloc"><strong>Devis 2026-124</strong><small>Ajoute a l'instant par Sophie M.</small></span><span class="doc-etiq doc-etiq--ok">Nouveau</span></a></li>
  <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-121</strong><small>Il y a 2 heures</small></span></a></li>
  <li><a href="#"><span class="liste__bloc"><strong>Devis 2026-118</strong><small>Hier</small></span></a></li>
</ul>
@tw background-color anime de la teinte vers transparent en 2 s
@end */
.an-neuf { animation: an-surligner 2.4s var(--e-douce) forwards; }
@keyframes an-surligner {
  0%   { background: var(--c-success-soft); }
  30%  { background: var(--c-success-soft); }
  100% { background: transparent; }
}
/* @endv */

/* @v ANI-E3
@nom Reflet de passage
@usage Un eclat qui traverse une carte pour signaler une nouveaute. A n'utiliser qu'une fois par ecran, et jamais en boucle : le regard s'y accroche et ne lit plus le reste.
@large
@html
<div class="cartes-grille">
  <div class="carte an-reflet" data-an-rejouer><div class="carte__corps"><h3 class="carte__titre">Formule Pro</h3><p class="carte__texte">Relances automatiques et application mobile.</p></div></div>
  <div class="carte"><div class="carte__corps"><h3 class="carte__titre">Formule Essentielle</h3><p class="carte__texte">Devis et factures illimites.</p></div></div>
</div>
@tw ::after en gradient translate de -120% a 120%, une seule fois
@end */
.an-reflet { position: relative; overflow: hidden; }
.an-reflet::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--c-primary) 22%, transparent) 50%, transparent 70%);
  transform: translateX(-120%);
  animation: an-balayer 1.6s var(--e-douce) .4s;
  pointer-events: none;
}
@keyframes an-balayer { to { transform: translateX(120%); } }
/* @endv */

/* @v ANI-E4
@nom Halo d'appel
@usage Un bouton qu'on veut faire remarquer sans le faire clignoter. L'anneau se dilate et s'efface : le mouvement est en peripherie de vision, il n'empeche pas de lire.
@html
<div class="demo-row" style="padding:16px 0">
  <button class="btn btn--plein btn--lg btn--pilule an-halo">Demander un rappel</button>
  <span class="an-point-halo" aria-hidden="true"></span>
</div>
@tw un anneau en ::after, scale(1) vers scale(1.4) avec opacity vers 0
@end */
.an-halo { position: relative; }
.an-halo::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--c-primary);
  animation: an-halo 2.2s var(--e-douce) infinite;
  pointer-events: none;
}
@keyframes an-halo {
  0%   { opacity: .7; transform: scale(1); }
  70%  { opacity: 0; transform: scale(1.3); }
  100% { opacity: 0; transform: scale(1.3); }
}
.an-point-halo { position: relative; width: 10px; height: 10px; border-radius: 50%; background: var(--c-danger); }
.an-point-halo::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  background: var(--c-danger);
  animation: an-halo 1.8s var(--e-douce) infinite;
}
/* @endv */

/* ===========================================================================
   FAMILLE F  -  TEXTE
   ======================================================================== */

/* @v ANI-F1
@nom Machine a ecrire
@usage L'ouverture d'une page ou une demonstration. Le texte complet est dans le balisage : la lecture assistee et les moteurs de recherche voient la phrase entiere.
@large
@html
<p class="an-frappe" data-an-frappe>Nous posons des pompes a chaleur depuis 2009.</p>
@tw JS : on decoupe en caracteres, le curseur est un ::after clignotant
@end */
.an-frappe {
  margin: 0;
  font-size: clamp(20px, 3vw, 30px);
  font-weight: 700;
  letter-spacing: -.02em;
  min-height: 1.4em;
}
.an-frappe::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: .95em;
  margin-left: 3px;
  vertical-align: -.12em;
  background: var(--c-primary);
  animation: an-curseur 1s steps(2) infinite;
}
@keyframes an-curseur { 50% { opacity: 0; } }
/* @endv */

/* @v ANI-F2
@nom Mots qui defilent
@usage Une accroche dont un mot change. Le conteneur garde la largeur du mot le plus long, sinon la phrase se reorganise a chaque changement.
@large
@html
<p class="an-defile-mots">
  Nous posons des
  <span class="an-defile" data-an-defile><i>pompes a chaleur</i><i>chauffe-eau</i><i>chaudieres</i><i>climatisations</i></span>
  depuis 2009.
</p>
@tw hauteur figee + translateY par pas d'une ligne, en boucle
@end */
.an-defile-mots { margin: 0; font-size: clamp(19px, 2.8vw, 28px); font-weight: 700; letter-spacing: -.02em; line-height: 1.5; }
.an-defile {
  display: inline-grid;
  vertical-align: bottom;
  height: 1.5em;
  overflow: hidden;
  color: var(--c-primary);
}
.an-defile i {
  grid-area: 1 / 1;
  font-style: normal;
  white-space: nowrap;
  transition: transform 520ms var(--e-douce), opacity 320ms var(--e-douce);
}
/* @endv */

/* @v ANI-F3
@nom Texte qui se decode
@usage Un titre qui se stabilise a partir de caracteres aleatoires. Tres marque : a reserver a une page unique, et jamais sur une information qu'il faut lire vite.
@large
@html
<p class="an-decode" data-an-decode="RENOVATION ENERGETIQUE">RENOVATION ENERGETIQUE</p>
@tw JS : chaque lettre se fixe apres n tirages, de gauche a droite
@end */
.an-decode {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(17px, 3vw, 30px);
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--c-text);
  min-height: 1.3em;
}
/* @endv */

/* @v ANI-F4
@nom Compteur qui grimpe
@usage Un chiffre de preuve. La valeur finale est dans le balisage, l'animation ne fait que la rejouer : si le script ne s'execute pas, le chiffre reste juste.
@large
@html
<div class="an-chiffres" data-an-rejouer>
  <div><strong data-an-compte="1240">1 240</strong><span>installations depuis 2009</span></div>
  <div><strong data-an-compte="4.8" data-an-dec="1">4,8</strong><span>note moyenne sur 312 avis</span></div>
  <div><strong data-an-compte="98">98</strong><span>pour cent de chantiers dans les delais</span></div>
</div>
@tw JS : sortie amortie en 1 - (1 - t)^3, chiffres tabulaires
@end */
.an-chiffres { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--sp-5); text-align: center; }
.an-chiffres strong { display: block; font-size: 42px; font-weight: 800; letter-spacing: -.035em; font-variant-numeric: tabular-nums; line-height: 1; }
.an-chiffres span { display: block; margin-top: 8px; font-size: var(--fs-sm); color: var(--c-text-muted); line-height: 1.45; }
/* @endv */

/* ---------------------------------------------------------------------------
   MOUVEMENT REDUIT
   On supprime le mouvement, jamais l'information : les etats finaux restent
   atteints, instantanement.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .an-coche circle, .an-coche path,
  .an-trace path, .an-jauge .v { animation: none; stroke-dashoffset: 0; }
  .an-jauge .v { stroke-dashoffset: calc(1 - var(--p)); }
  .an-barres i { animation: none; }
  .an-halo::after, .an-point-halo::after, .an-reflet::after { animation: none; opacity: 0; }
  .an-frappe::after { animation: none; }
}

/* Bouton de relecture des demonstrations (catalogue uniquement). */
.an-rejouer {
  position: absolute;
  top: 8px; right: 8px;
  z-index: 3;
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  background: var(--c-surface);
  color: var(--c-text-soft);
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: var(--fw-semi);
  cursor: pointer;
  opacity: 0;
  transition: opacity 160ms var(--e-douce);
}
.fiche__scene:hover .an-rejouer, .an-rejouer:focus-visible { opacity: 1; }
.an-rejouer:hover { background: var(--c-surface-2); color: var(--c-text); }
