/* ===========================================================================
   LES ANIMATIONS — feuille additive, chargée APRÈS app.css
   ---------------------------------------------------------------------------
   Écrite le 12/09/2026 (bureau-9a) pendant qu'une autre session tenait
   app.css. D'où une feuille à part : deux sessions dans le même fichier
   s'écrasent sans message. Elle se retire en commentant UNE ligne de
   index.html — rien d'autre ne dépend d'elle.

   ELLE NE FAIT QU'AJOUTER. Aucune règle d'app.css n'est annulée ici : ce qui
   bougeait déjà — les tuiles, les modales, les toasts, les boutons, la
   cascade « #vue.entrant » — continue de bouger exactement pareil. Cette
   feuille couvre ce qui restait immobile.

   ---------------------------------------------------------------------------
   LA DOCTRINE EST CELLE D'APP.CSS, ET ELLE NE CHANGE PAS :

   1. Le mouvement explique, il ne décore pas. Un bloc qui arrive monte de
      quelques pixels — il vient d'apparaître. Une ligne survolée se décale —
      elle est sous le curseur.
   2. Court. 140 à 460 ms — relevé le 12/09 après un premier calibrage à
      160 ms que Kentin ne voyait pas. Au-delà de 460, on attend l'interface.
   3. Jamais sur ce qu'on manipule en masse. Les deux mille rangs du
      portefeuille de Sotteville se posent d'un coup ; seuls les HUIT PREMIERS
      d'une liste reçoivent une cascade, et uniquement à l'arrivée sur
      l'écran (classe « entrant », posée par rendre()).

   TROIS RÈGLES D'ÉCRITURE, payées cher dans app.css :

   — Toute durée passe par « calc(... * var(--anim-vit)) ». Sans quoi le
     curseur « Vitesse des animations » des Préférences mentirait, et le
     réglage à zéro laisserait du mouvement derrière lui.
   — Toute amplitude passe par « var(--anim-amp) ». Même raison.
   — « backwards », jamais « forwards » ni « both ». Une animation figée en
     avant garde la main sur la propriété et neutralise le :hover qui suit —
     c'est ce qui empêchait les tuiles de se soulever.

   UN PIÈGE ÉCARTÉ : pas de « scroll-behavior: smooth » global.
   restaurerDefilement() repose la position par « el.scrollTop = v ». En
   défilement doux, chaque retour sur un écran se verrait redescendre depuis
   le haut. Le fil des conversations l'a déjà, là où c'est voulu.
   =========================================================================== */

/* --- 0. Les jetons de mouvement --------------------------------------------

   Quatre durées, deux courbes. Tout le reste de la feuille s'y réfère : une
   seule ligne à changer pour retoucher le rythme d'ensemble. */
/* ⚠️ SECONDE PASSE, 12/09/2026 — « je ne les vois pas ».

   La première passe visait 1 à 6 px et 160 ms. Rien ne les bloquait :
   Windows autorise les animations, aucun réglage n'était posé en base
   (--anim-vit = 1), le serveur servait bien la feuille. Elles jouaient — sous
   le seuil de perception. Un décalage d'un pixel sur un survol ne se voit
   pas ; il se devine, au mieux.

   Les durées et les amplitudes sont donc montées d'un cran franc. On reste
   sous la barre des 460 ms : au-delà, on attend l'interface au lieu de la
   regarder. */
:root {
  --m-vif:    calc(.14s * var(--anim-vit));   /* l'appui, le retour de doigt   */
  --m-court:  calc(.22s * var(--anim-vit));   /* survol, couleur, liseré       */
  --m-moyen:  calc(.34s * var(--anim-vit));   /* un bloc qui s'ouvre           */
  --m-ample:  calc(.46s * var(--anim-vit));   /* le rail qui se replie         */

  /* --ressort vient d'app.css : cubic-bezier(.16, 1, .3, 1), le freinage
     long qui donne l'impression de glisse. --sortie est son contraire pour
     ce qui s'en va : rapide au départ, sans rebond. */
  --sortie: cubic-bezier(.4, 0, .7, .3);

  /* ⚠️ DEUX COURBES, DEUX MÉTIERS — distinction proposée par bureau-02 après
     mesure, et elle est juste. « --ressort » atteint 90 % de sa course dans le
     premier tiers puis rampe : parfait pour une APPARITION, mais sur un objet
     qui se déplace il donne l'impression qu'il colle. « --glisse » démarre
     franchement et freine court : un objet qui va d'un point à un autre y
     arrive sans traîner. Ce qui ARRIVE prend le ressort, ce qui BOUGE prend
     la glisse. */
  --glisse: cubic-bezier(.22, .61, .36, 1);
}

/* L'entrée de base : un voile qui monte de quelques pixels. Même geste que
   « entree-bloc » d'app.css, redéclaré sous un autre nom pour que retirer
   cette feuille ne prive personne du sien. */
@keyframes anim-monte {
  from { opacity: 0; transform: translateY(calc(16px * var(--anim-amp))); }
  to   { opacity: 1; transform: none; }
}
@keyframes anim-paraitre {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* LE LISERÉ DE L'ONGLET ACTIF N'EST PAS ICI, ET C'EST DÉLIBÉRÉ.
   app.css l'anime déjà — « nav-filet » et « nav-pose », vers la ligne 8489 —
   et son geste tient compte des deux dispositions de barre. Une seconde
   animation écrite ici l'aurait remplacée sans rien apporter : cette feuille
   vient après, elle aurait gagné à spécificité égale. Vérifié le 12/09. */

/* ===========================================================================
   1. LA NAVIGATION
   =========================================================================== */

/* Le rail qui se replie. .corps est une grille « var(--lateral) 1fr », et
   .appli.replie fait passer --lateral de 256 à 60 px : sans transition, la
   page entière saute de deux cents pixels d'un seul coup. Chromium sait
   interpoler grid-template-columns dès lors que les deux pistes gardent la
   même nature — une longueur et une fraction, ce qui est le cas ici.

   Grand écran seulement : en dessous de 981 px, la barre latérale n'est plus
   une colonne mais un tiroir, et animer un changement de DISPOSITION au
   redimensionnement ferait patiner la fenêtre. */
@media (min-width: 981px) {
  :root[data-barre="gauche"] .corps {
    transition: grid-template-columns var(--m-ample) var(--ressort);
  }
  :root[data-barre="gauche"] .lateral {
    transition: padding var(--m-ample) var(--ressort);
  }
}

/* Les entrées du menu. app.css leur donnait déjà le fond et la couleur ; il
   leur manquait le retour de doigt et le décalage du survol.

   Le geste suit la DIRECTION de la barre : dans un rail vertical une entrée
   survolée avance vers la droite, dans une barre horizontale elle se lève.
   L'inverse donnerait, sur la barre du haut, treize onglets qui se poussent
   les uns les autres. */
.nav button {
  transition: background var(--m-court), color var(--m-court),
              transform var(--m-vif) var(--ressort);
}
.nav button:hover { transform: translateX(4px); }
.nav button:active { transform: translateX(4px) scale(.96); }
/* Même raison qu'au-dessous pour la barre du haut : l'onglet ouvert ne répond
   pas à un clic qui ne navigue nulle part. */
.nav button.actif:active { transform: none; }
.nav button svg { transition: transform var(--m-court) var(--ressort), opacity var(--m-court); }
.nav button:hover svg { opacity: 1; transform: scale(1.08); }

/* LA BARRE DU HAUT EST LA DISPOSITION PAR DÉFAUT — décision de Kentin du
   11/09/2026 — et son bloc d'app.css vise
   « html:root:not([data-barre="gauche"]) .appli .lateral .nav button » :
   quatre classes et deux éléments. La règle simple ci-dessus perdait contre
   lui, mesuré le 12/09 : aucun des gestes de navigation ne s'appliquait dans
   la disposition que l'usager voit. On reprend donc son sélecteur mot pour
   mot, et on garde ses trois transitions en y ajoutant le transform. */
@media (min-width: 981px) {
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button {
    transition: background-color var(--m-court), color var(--m-court),
                box-shadow var(--m-court), transform var(--m-vif) var(--ressort);
  }
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button:hover {
    transform: translateY(-3px);
  }
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button:active {
    transform: translateY(0) scale(.94);
  }
  /* L'ONGLET OUVERT NE BOUGE PAS — NI AU SURVOL, NI À L'APPUI.

     Au survol, c'était déjà la règle : app.css a tranché que le survol ne
     repeint pas la page où l'on se trouve déjà, et un onglet fondu dans sa
     page qui se soulève ferait apparaître le raccord.

     À L'APPUI, LA MÊME RAISON VAUT, ET ELLE EST PLUS FORTE. Signalé par
     Kentin le 12/09 : « quand on appuie sur les onglets il s'enfonce, mais
     comme il s'enfonce le bas disparaît et ça fait moche ». Relevé par
     bureau-9c, puis vérifié sur ses deux gros plans : au repos la pastille
     violette porte un POINT BLANC NET en bas — l'indicateur de l'onglet
     ouvert — plus un halo ; sous scale(.94) le point n'est plus qu'une tache
     sombre et le halo a disparu.

     Les chiffres ne montraient rien : 43 px de haut contre 41, aucun
     conteneur qui rogne. C'est l'image qui tranche, une fois de plus.

     ⚠️ ON NE RÉDUIT PAS L'ENFONCEMENT, ON LE RETIRE. Passer à scale(.97)
     traiterait le symptôme — le point s'effacerait un peu moins. Compenser
     l'échelle sur le pseudo-élément marcherait, mais ajouterait une règle qui
     devrait suivre toute modification de la première. La vraie raison est
     ailleurs : CLIQUER SUR L'ONGLET OÙ L'ON EST DÉJÀ NE DÉCLENCHE AUCUNE
     NAVIGATION. Un retour tactile pour un geste sans effet est une promesse
     que l'application ne tient pas. Les onze autres onglets, eux, continuent
     de répondre — ce sont les clics qui font quelque chose. */
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button.actif:hover,
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button.actif:active {
    transform: none;
  }
}

/* Le compteur à droite d'une entrée : il change de couleur en douceur quand
   l'écran devient actif. */
.nav button .compteur {
  transition: background var(--m-court), color var(--m-court);
}

/* La barre du bas, au téléphone. Même grammaire, geste vertical : c'est vers
   le haut que pointe un pouce. */
.nav-bas button {
  transition: color var(--m-court), background var(--m-court),
              transform var(--m-vif) var(--ressort);
}
.nav-bas button:active { transform: translateY(1px) scale(.96); }

/* Les boutons de la barre de titre : historique, préférences, cadre de la
   fenêtre. Ils étaient parfaitement inertes. */
.histo button, .outils-titre button, .fenetre-actions button,
.bascule-menu, .replier-lateral {
  transition: background var(--m-court), color var(--m-court),
              opacity var(--m-court), transform var(--m-vif) var(--ressort);
}
.histo button:not(:disabled):hover,
.outils-titre button:hover { transform: scale(1.1); }
.histo button:not(:disabled):active,
.outils-titre button:active { transform: scale(.92); }
.replier-lateral:hover { transform: translateX(-2px); }
.bascule-menu:active { transform: scale(.9); }

/* L'engrenage des Préférences tourne d'un huitième de tour au survol. Le seul
   ornement de cette feuille, et il tient en deux lignes : c'est le bouton le
   plus ouvert de l'application, autant qu'il réponde. */
#ouvrir-preferences svg { transition: transform var(--m-moyen) var(--ressort); }
#ouvrir-preferences:hover svg { transform: rotate(45deg); }

/* Le logo de la marque, en haut du rail. */
.marque .pastille-logo { transition: transform var(--m-moyen) var(--ressort); }
.marque:hover .pastille-logo { transform: scale(1.06) rotate(-3deg); }

/* ===========================================================================
   2. LE CHANGEMENT D'ÉCRAN — TROISIÈME GESTE, RÉÉCRIT LE 12/09/2026
   ---------------------------------------------------------------------------
   Verdict de Kentin sur la version précédente : « quand on change d'onglet, je
   trouve ça pas beau ». Deux défauts, tous deux de conception, pas de réglage.

   1. NEUF À DOUZE BLOCS MONTAIENT EN DÉCALÉ. Mesuré : 9 blocs sur
      Statistiques, 11 sur Conversations, 12 sur Clients. Une cascade est belle
      sur trois ou quatre blocs, et sur un écran qu'on ouvre une fois. Sur
      douze, cinquante fois par jour, elle grouille : l'œil ne suit pas douze
      départs échelonnés, il voit du désordre. Monter les durées, à ce
      stade-là, n'aurait fait que rendre le désordre plus lisible.

   2. IL N'Y AVAIT PAS DE SORTIE — et c'est le vrai défaut. rendre() remplace
      le contenu d'un seul coup : l'ancien écran DISPARAISSAIT net, puis le
      nouveau se reconstruisait pièce par pièce. Ce claquement est la première
      chose qu'on voit, avant même la cascade. Une transition sans sortie n'est
      pas une transition, c'est une apparition précédée d'un trou.

   LE GESTE RETENU : UN FONDU CROISÉ, ET UN SEUL MOUVEMENT.

   L'écran qui part s'efface sur place, très vite (170 ms), pendant que celui
   qui arrive monte de douze pixels (300 ms). Les deux se CHEVAUCHENT — c'est
   ce chevauchement qui fait la fluidité, et c'est exactement ce qui manquait.
   Rien ne se décompose : la page arrive d'une pièce, comme une feuille qu'on
   pose. Un seul objet en mouvement au lieu de douze.

   Le fantôme de sortie est posé par animations.js, parce qu'un nœud retiré du
   document ne s'anime pas — même mécanisme que pour les fenêtres modales.
   =========================================================================== */

@keyframes anim-ecran-entre {
  from { opacity: 0; transform: translateY(calc(12px * var(--anim-amp))); }
  to   { opacity: 1; transform: none; }
}

/* Le léger retard laisse l'écran précédent partir en premier. Sans lui, les
   deux sont à pleine opacité au même instant et l'on voit un texte par-dessus
   un autre — le défaut classique du fondu croisé mal calé. */
#vue.entrant > .page {
  animation: anim-ecran-entre calc(.3s * var(--anim-vit)) var(--ressort) backwards;
  animation-delay: calc(.05s * var(--anim-vit));
}

/* L'écran qui s'en va. Le clone est posé en « fixed » aux coordonnées relevées
   avant le retrait : il ne dépend donc d'aucun ancêtre positionné, et rien
   n'est ajouté à #vue — une règle « position: relative » posée là changerait
   le référentiel de tout ce qui est absolu à l'intérieur. */
@keyframes anim-ecran-sort {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(calc(-10px * var(--anim-amp))); }
}
.anim-ecran-part {
  position: fixed;
  z-index: 4;
  overflow: hidden;
  pointer-events: none;
}
/* Le fantôme ne part qu'au signal : les deux moitiés du fondu croisé doivent
   démarrer ensemble, sinon l'ancien s'efface sur un écran encore vide. */
.anim-ecran-part.va {
  animation: anim-ecran-sort calc(.17s * var(--anim-vit)) var(--sortie) forwards;
}

/* ===========================================================================
   LA RETENUE — pourquoi le contenu attend avant de s'animer
   ---------------------------------------------------------------------------
   Mesuré le 12/09, page visible et peinte, images comptées pendant le geste :

     changer d'écran      143 ms sans une seule image, tâche longue de 61 ms
     Statistiques         131 ms, QUINZE trous, tâche longue de 128 ms

   Relevé SANS cette feuille : le blocage vient du rendu de l'application, pas
   des animations. Mais lancer un geste de 300 ms dans un fil qui va se figer
   143 ms, c'est le condamner à sauter — et c'est ce que Kentin voyait.

   Le contenu est donc RETENU le temps que le fil respire : opacité nulle,
   animations éteintes. animations.js lève la retenue dès que deux images se
   suivent de près, et tout démarre alors d'un trait.

   ⚠️ « animation: none » sur les descendants ET opacité nulle sur la seule
   « .page » : peindre un écran entier à opacité nulle coûterait autant que de
   le peindre, alors qu'une seule couche transparente ne coûte rien.

   ⚠️ CETTE CLASSE CACHE L'ÉCRAN. Elle ne doit JAMAIS survivre à son geste :
   animations.js la retire par trois voies — le fil libre, un filet à 700 ms,
   et un try/catch qui englobe tout. Un écran figé vaut mieux qu'un écran
   vide ; un écran vide, jamais. */
/* ⚠️ UN SÉLECTEUR UNIVERSEL A ÉTÉ ESSAYÉ ICI, ET IL A TOUT AGGRAVÉ.

   Première version : « #vue.anim-attente .page * { animation: none } », pour
   éteindre aussi les animations des descendants. Poser puis retirer cette
   classe force un recalcul de style sur CHAQUE nœud de l'écran — plusieurs
   milliers sur Statistiques. Mesuré : les tâches longues sont passées de
   128 ms à 883 ms, et le pire trou de 131 à 299 ms. Le remède était cinq fois
   pire que le mal.

   La version qui tient ne touche qu'UN élément, « .page ». Les animations des
   descendants n'ont pas besoin d'être éteintes : elles sont toutes déclarées
   sous « #vue.anim-change », et cette classe n'est posée qu'à la libération.
   Ce qui n'a pas commencé n'a pas à être arrêté. */
/* ⚠️ LA RETENUE NE VAUT QUE POUR UN CHANGEMENT D'ÉCRAN — « anim-attente-ecran »
   et non « anim-attente ». Filmé image par image le 12/09 sur la bascule d'un
   widget de l'Accueil : à quarante millisecondes, TOUTE LA PAGE était à
   l'opacité zéro pendant que les deux fantômes du widget flottaient seuls sur
   le vide. Un écran qui disparaît en entier pour un clic d'onglet interne, ça
   ne se lit pas comme une transition : ça se lit comme un bug, et c'est le mot
   qu'a employé Kentin.

   Pour une bascule, la retenue est portée par la règle que compose
   animations.js — elle ne cache que le panneau, exactement ce que l'animation
   fera bouger. La retenue et le geste couvrent le même périmètre ; c'est la
   règle que ce fichier a mis trois essais à trouver. */
#vue.anim-attente-ecran > .page {
  opacity: 0;
  animation: none;
}

/* Une bascule d'onglet interne ne doit pas rejouer l'arrivée de l'écran, même
   quand app.js pose « entrant » parce que le rendu repasse par le serveur.
   Voir animations.js : la classe vit le temps du geste. */
#vue.anim-bascule-en-cours.entrant > .page {
  animation: none;
}
/* ⚠️ ET LA CASCADE D'APP.CSS AVEC. « #vue.entrant .page > * » (app.css, vers
   la ligne 4071) fait monter le conteneur de l'écran — « .cli », « .acc »,
   « .gr-page ». Mesuré sur Clients : cliquer l'onglet « À visiter » faisait
   monter « .cli » tout entier EN PLUS du panneau, parce que le rechargement
   serveur fait poser « entrant » à app.js comme pour un écran neuf. Deux
   blocs en mouvement pour un clic d'onglet, dont l'écran complet : c'est
   exactement ce qui a été jugé horrible. */
#vue.anim-bascule-en-cours.entrant .page > *,
#vue.anim-bascule-en-cours.entrant .grille > .tuile {
  animation: none;
}

/* ===========================================================================
   2 bis. LA BASCULE — le contenu qui change SANS qu'on change d'écran
   ---------------------------------------------------------------------------
   Demande de Kentin, 12/09/2026 : « dans Phoning quand on passe de Agenda des
   appels à SMS, ça saccade ; je veux une belle animation fluide, et je la veux
   pour tout ».

   Rien ne couvrait ce cas. « #vue.entrant » ne vaut qu'à l'ARRIVÉE sur un
   écran ; basculer d'un sous-onglet à l'autre reste le même écran, donc pas de
   classe, donc pas d'animation — et comme tout passe par un rendu complet, le
   contenu était remplacé d'un bloc.

   LA STRUCTURE COMMANDE LE GESTE :

       .gr-page
         .entete-page           ← le titre
         .gr-barre              ← les outils
         .gr-ile
           .gr-encoche          ← LES ONGLETS : ils ne bougent pas
           .gr-corps            ← LE CONTENU : c'est lui qui se renouvelle

   Faire glisser l'île entière ferait partir les onglets qu'on vient de
   cliquer : le doigt est encore dessus, et les voir disparaître dément le
   clic. L'encoche est donc exclue par son NOM — « [class*=encoche] » — plutôt
   que par une liste d'écrans qui vieillirait.

   Même fondu croisé que pour l'écran : animations.js pose ici aussi un fantôme
   du contenu qui part. Deux gestes distincts :
   — « change »  : un onglet, un segment, un jour. Tout le contenu se renouvelle.
   — « detail »  : une ligne de liste. SEULE la fiche change ; la liste ne doit
                   pas clignoter — on vient d'y viser une ligne et on va
                   peut-être en viser une autre.
   =========================================================================== */

/* ⚠️ LE GESTE D'UN ONGLET EST HORIZONTAL, PAS VERTICAL — 12/09, 4e version.

   Mesure de la bascule d'un widget de l'Accueil : pire trou 36 ms, aucune
   tâche longue. Elle était donc DÉJÀ fluide au sens des images ; ce que Kentin
   ne trouvait pas fluide, c'était le geste.

   La faute : un fondu croisé sur place, l'ancien descendant de 10 px et le
   nouveau montant de 18 px. Deux contenus denses — le widget compte 378
   nœuds — qui se traversent en sens INVERSE pendant 170 ms, ça ne se lit pas
   comme une transition, ça se lit comme un flou. Deux listes superposées qui
   vont l'une vers l'autre ne disent rien à l'œil.

   Un onglet se lit latéralement : l'ancien sort par la gauche, le nouveau
   entre par la droite. Même direction, donc une seule idée — le contenu a
   défilé. C'est la grammaire de tous les jeux d'onglets, et elle évite le
   croisement parce que les deux vont du même côté.

   Le changement d'ÉCRAN, lui, garde son geste vertical : on ne défile pas d'un
   écran à l'autre, on en pose un nouveau. */
/* LE SENS SUIT LE CHEMIN — raffinement du 12/09, « l'animation est bien mais
   fais mieux ».

   Aller de « Tournée du jour » vers « À rappeler » et en REVENIR produisaient
   le même geste : le contenu arrivait toujours de la droite. L'interface
   n'avait donc pas de mémoire spatiale — les onglets étaient une liste, pas un
   rang. Désormais « --sens » vaut 1 quand on avance dans la barre et -1 quand
   on recule ; le contenu vient du côté d'où l'on vient, et part du côté où
   l'on va. C'est ce qui fait qu'un jeu d'onglets se retient.

   Le léger « scale » ajoute la profondeur qui manquait : le panneau qui arrive
   n'est pas seulement décalé, il vient de DERRIÈRE. Un centième et demi
   suffit — plus, et l'on voit un zoom. */
/* ⚠️ LA GLISSADE A ÉTÉ RETIRÉE LE 12/09, À LA DEMANDE DE KENTIN.

   Le panneau se déplaçait latéralement de 26 px — l'ancien sortait par un
   côté, le nouveau entrait par l'autre, dans le sens du chemin parcouru dans
   la barre. Le geste était juste, mesuré dans les deux sens, et il ne lui
   plaît pas. C'est son écran.

   Ce qui reste tient en deux propriétés : l'opacité, et une échelle qui donne
   la profondeur. Le contenu s'efface sur place et le suivant se pose au même
   endroit, en venant très légèrement de derrière. Aucun déplacement.

   « --sens » n'est plus lu par personne : le calcul qui le posait a été retiré
   d'animations.js en même temps. Le rétablir demande les deux. */
/* LA PROFONDEUR, d'après le composant d'Aceternity — 12/09, dernière passe.

   Le code de démonstration que Kentin m'a envoyé porte « [perspective:1000px] »
   sur le conteneur des onglets : tout leur effet de cartes repose sur une
   scène en TROIS DIMENSIONS, pas sur un décalage. C'est ce qui donne
   l'impression que le panneau vient de derrière l'écran plutôt que d'un côté.

   L'empilement de cartes reste hors de portée — Louis ne monte qu'un panneau à
   la fois, mesuré. Mais la perspective, elle, se transpose : le panneau entre
   en s'éloignant très légèrement sur l'axe Z et en basculant d'un degré et
   demi, comme une page qu'on repose à plat. Sans glissade : elle a été retirée
   à la demande de Kentin, et rien ici ne la ramène — aucune translation
   horizontale ou verticale, seulement de la profondeur. */
@keyframes anim-bascule {
  from {
    opacity: 0;
    transform: perspective(1000px)
               translateZ(calc(-38px * var(--anim-amp)))
               rotateX(calc(1.5deg * var(--anim-amp)));
  }
  to { opacity: 1; transform: perspective(1000px) translateZ(0) rotateX(0); }
}
@keyframes anim-panneau-sort {
  from { opacity: 1; transform: perspective(1000px) translateZ(0) rotateX(0); }
  to {
    opacity: 0;
    transform: perspective(1000px)
               translateZ(calc(-26px * var(--anim-amp)))
               rotateX(calc(-1deg * var(--anim-amp)));
  }
}
/* Le fantôme d'un panneau part sur le côté ; celui d'un écran garde
   « anim-ecran-sort ». La classe est posée par animations.js. */
.anim-ecran-part.est-panneau.va {
  animation: anim-panneau-sort calc(.17s * var(--anim-vit)) var(--sortie) forwards;
}

/* ⚠️ TROISIÈME ÉCRITURE DE LA BASCULE, 12/09 — « horrible », et c'était vrai.

   Verdict de Kentin : « passer de Tournée du jour à À rappeler dans Accueil,
   horrible ». Mesure du geste : le fantôme de sortie faisait 1543 × 1171 px
   dans une fenêtre de 1440 × 880. Cliquer un onglet large de cent pixels,
   à l'intérieur d'un widget, effaçait et remontait L'ÉCRAN ENTIER — l'en-tête,
   les tuiles, les filtres, tout.

   La cause est une erreur de niveau, la même que j'ai déjà payée deux fois :
   les onglets de l'Accueil vivent dans « .acc-onglets », lui-même DANS
   « .acc-widget ». Faute d'île sur cet écran, le code prenait la page entière
   comme conteneur à animer.

   LA RÈGLE QUI REMPLACE TOUT : ce qui s'anime, c'est CE QUI SUIT LA BARRE
   D'ONGLETS CLIQUÉE, et rien d'autre. animations.js remonte du bouton jusqu'à
   la barre la plus externe, puis marque ses frères suivants — le panneau.

     .acc-widget > .acc-onglets + le contenu     → le contenu seul
     .cli-ile   > .cli-onglets  + .cli-corps     → .cli-corps
     .gr-ile    > .gr-encoche   + .gr-corps      → .gr-corps
     .gr-ile    > .gr-encoche   + .gr-tuiles + 9 sections → les dix

   Plus aucun sélecteur à rallonge, plus de liste d'écrans à tenir à jour : la
   structure dit elle-même ce qui change. Et PLUS DE FANTÔME sur une bascule —
   un panneau qui se renouvelle n'a pas besoin qu'on efface l'ancien par-dessus
   l'écran, ça coûtait un clone et ça faisait clignoter le reste. Le fondu
   croisé reste pour le changement d'écran, où il a du sens. */

/* La règle qui anime le panneau n'est PAS écrite ici : animations.js la
   compose à partir de la barre d'onglets cliquée — « <barre> ~ * » — et la
   pose dans une feuille à lui. Un sélecteur écrit d'avance ne peut pas
   désigner « ce qui suit la barre sur laquelle on vient de cliquer ».
   Voir « regleDuPanneau » dans animations.js. */
/* Un panneau en plusieurs blocs — les dix de Statistiques — se pose d'un seul
   geste : même durée, même retard. Ce sont des parties d'UN contenu, pas dix
   contenus. Les échelonner est exactement ce qui a été jugé « pas beau » sur
   l'arrivée d'écran. */

/* ⚠️ CE QUI A ÉTÉ RETIRÉ ICI, ET QU'IL NE FAUT PAS REMETTRE SANS RAISON :

   — la cascade sur « .page > * > * » et sur le contenu des îles (jusqu'à douze
     blocs échelonnés) — c'est elle que Kentin a jugée laide ;
   — la cascade sur les huit premières lignes d'une liste : le bloc qui les
     porte s'anime déjà, et deux couches sur le même contenu se lisent comme un
     flou. Même raison que pour les lignes de tableau, plus bas ;
   — la cascade sur les colonnes et les volets.

   Tout cela faisait plusieurs objets en mouvement pour un seul geste de
   l'usager. Un geste, un mouvement. */


/* ===========================================================================
   2 ter. LES ONGLETS EUX-MÊMES — d'après « Tabs » d'Aceternity UI
   ---------------------------------------------------------------------------
   Référence choisie par Kentin le 12/09 : ui.aceternity.com/components/tabs.
   Le composant fait quatre choses. Deux se transposent, deux non, et la
   mesure dit pourquoi plutôt qu'un avis.

   ✅ LA PILULE QUI SE DÉPLACE. Chez Aceternity, l'onglet actif porte un fond
      qui coulisse d'un onglet à l'autre (layoutId de Motion). Ici la mesure
      donne l'INVERSE : les onglets inactifs portent un fond, rgb(16,21,40),
      et l'actif est un CREUX — fond transparent, texte blanc pur. Ce qui doit
      se déplacer est donc le trou, pas la pilule.

      On l'obtient sans rien ajouter au DOM : une transition sur la couleur de
      fond suffit. L'ancien actif reprend son fond pendant que le nouveau perd
      le sien, et l'œil lit un creux qui glisse. Aucun élément mobile, donc
      aucun conflit avec le fond que d'autres sessions dessinent sur ces
      boutons.

   ✅ LE REBOND DE L'ONGLET PRIS. Aceternity fait plonger l'onglet actif —
      « y: [0, 40, 0] » — et c'est la signature du composant, ce qui le rend
      reconnaissable. Quarante pixels sur un onglet de 43 px de haut le
      ferait sortir de sa barre : ramené à six, le geste reste lisible sans
      casser la rangée.

   ❌ L'EMPILEMENT DE CARTES EN ARRIÈRE-PLAN (scale: 1 - idx * 0.1) et
   ❌ L'ÉVENTAIL AU SURVOL (top: idx * -60px) sont HORS DE PORTÉE, et ce n'est
      pas un choix. Ils supposent que TOUS les panneaux soient montés en même
      temps, l'un derrière l'autre. Mesuré sur l'Accueil : « .acc-widget-corps »
      compte UN seul élément — Louis ne rend que l'onglet demandé, le serveur
      renvoie ce contenu-là et pas les autres. Les empiler exigerait de changer
      le rendu dans app.js, qui appartient à deux autres sessions et dépasse de
      loin une feuille d'animations.
   =========================================================================== */

/* Le creux qui se déplace. Visé large — « onglets », « encoche », segments —
   pour couvrir les trois familles de barres sans lister les écrans. */
[class*="onglets"] button,
[class*="encoche"] button,
.segmente button,
.onglets button {
  transition: background-color var(--m-moyen) var(--ressort),
              border-color var(--m-moyen) var(--ressort),
              color var(--m-court);
}

/* Le rebond de l'onglet qu'on vient de prendre. La classe est posée par
   animations.js sur le bouton devenu actif, après le rendu. */
@keyframes anim-onglet-pris {
  0%   { transform: translateY(0); }
  40%  { transform: translateY(calc(6px * var(--anim-amp))); }
  100% { transform: translateY(0); }
}
.anim-onglet-pris {
  animation: anim-onglet-pris calc(.42s * var(--anim-vit)) var(--ressort);
}

/* ===========================================================================
   3. LISTES, RANGS, TABLEAUX
   =========================================================================== */

/* Le rang survolé se décale de deux pixels. app.css l'avait prévu — son bloc
   « prefers-reduced-motion » neutralise déjà « .rang:hover { transform } » —
   mais la règle qui produisait le décalage avait disparu. La voici rendue.

   Sur .liste seulement : les rangs d'une île de conversation ou d'un volet
   sélectionné ont leur propre grammaire, et deux mouvements superposés font
   un tremblement. */
.liste > .rang {
  transition: background var(--m-court), transform var(--m-vif) var(--ressort);
}
.liste > .rang:hover { transform: translateX(4px); }
.liste > .rang:active { transform: translateX(4px) scale(.994); }

/* LES RANGS DES ÉCRANS REFAITS. « .cli-rang » (239 lignes sur Clients),
   « .gr-rang » (Équipe, À traiter, Phoning) : la refonte leur a donné une
   couleur de survol SANS transition — le fond sautait d'une ligne à l'autre à
   chaque passage de souris. C'est le geste le plus fréquent de
   l'application, et le seul qui claquait.

   RIEN NE SE DÉPLACE ICI, et c'est la doctrine qui le commande : « jamais sur
   ce qu'on manipule en masse ». Ces lignes portent en plus une poignée de
   glisser-déposer ; un rang qui se décale sous le curseur au moment où l'on
   vise sa poignée se manque. Seule la couleur bouge. */
.cli .cli-rang,
.gr-page .gr-rang {
  transition: background var(--m-court), color var(--m-court);
}
/* La poignée de déplacement se révèle en fondu au lieu d'apparaître. */
.cli .cli-rang .poignee-rang,
.gr-page .gr-rang .poignee-rang {
  transition: opacity var(--m-court), background var(--m-court), color var(--m-court);
}

/* Le monogramme grossit d'un cheveu avec sa ligne. */
.rang .ava { transition: transform var(--m-court) var(--ressort), background var(--m-court), color var(--m-court); }
.rang:hover .ava { transform: scale(1.06); }

/* Les cellules d'un tableau : app.css pose la couleur de survol, sans
   transition — le fond sautait d'une ligne à l'autre à chaque passage de
   souris. Sur td, pas sur tr : c'est là que la couleur est posée. */
.tableau tbody td { transition: background var(--m-court), color var(--m-court); }

/* Une pastille de statut suit son rang. */
.puce { transition: background var(--m-court), color var(--m-court), border-color var(--m-court); }

/* ===========================================================================
   4. CARTES ET BLOCS
   ---------------------------------------------------------------------------
   app.css a tranché : « le chapeau d'une carte au survol : rien ne bouge,
   mais le titre s'éclaircit ». On ne le dément pas — pas de carte qui se
   soulève. On ajoute seulement l'ombre, qui se lit sans que rien ne se
   déplace.
   =========================================================================== */
/* ⚠️ LES CLASSES D'AVANT LA REFONTE NE SUFFISENT PLUS, et c'est chiffré.
   Relevé par bureau-02 sur les treize écrans, blocs à surface visibles :

     gr-ile        10   AUCUNE transition
     tuile          8   la mienne
     carte          7   la mienne
     veille-tuile   4   AUCUNE
     sx-carte       4   AUCUNE
     gr-carte       2   AUCUNE

   Les blocs les plus nombreux de l'application — ceux des écrans refaits —
   étaient parfaitement immobiles. Viser « .carte » et « .tuile » seuls, c'est
   faire bouger une minorité et s'entendre dire que rien n'a changé : le même
   piège que ma première passe à un pixel.

   ⚠️ ET C'EST UNE DÉROGATION EXPLICITE À APP.CSS, qui avait tranché : « le
   chapeau d'une carte au survol : rien ne bouge, mais le titre s'éclaircit ».
   J'avais respecté cet arbitrage et dérogé pour les seules tuiles. Kentin est
   revenu dessus le 12/09 — « j'adore le slide et la fluidité de la nav barre,
   je veux la même chose sur tous les widgets » — et c'est son écran. La
   doctrine d'app.css reste vraie pour qui la lira : elle est simplement
   remplacée ici, à sa demande, et à cette date.

   ⚠️ ⚠️ LA RÈGLE DU TIERS — UN CONTENANT N'EST PAS UN WIDGET.

   Première version : j'avais mis les ÎLES dans la liste. bureau-02 l'a mesuré
   et le défaut est sérieux — le curseur est presque toujours au-dessus de
   l'île, puisqu'elle porte le contenu de l'écran. Le soulèvement n'était donc
   pas un geste qu'on déclenche, mais un état permanent qu'on ne voit jamais
   apparaître... et qui SE DÉFAIT chaque fois qu'on monte vers la barre. Toute
   la page redescendait de 3 px puis remontait au retour : six allers-retours
   vers la navigation dans une minute de travail, six sauts de l'écran entier.

   Mesuré ensuite chez moi, part de la fenêtre occupée par le plus grand bloc :

     .gr-ile (Statistiques)   376 %   ← elle défile, plus haute que l'écran
     .acc-widget               70 %
     .cli-feuille              65 %
     .cli-ile                  61 %
     .sx-graphe                42 %
     .sx-carte                 27 %   ✔
     .tuile                     5 %   ✔
     .sx-mesure                 2 %   ✔

   LA RÈGLE QUI EN SORT, et elle vaut au-delà de ce bloc : UN OBJET QU'ON
   DÉSIGNE NE COUVRE PAS PLUS DU TIERS DE L'ÉCRAN. Au-delà, ce n'est plus une
   cible, c'est le fond sur lequel les cibles sont posées — et lui donner une
   réaction au survol revient à faire réagir la page elle-même.

   C'est aussi, je crois, ce que voulait dire l'arbitrage d'app.css que je
   citais plus haut : « rien ne bouge » visait des SURFACES, pas des pastilles.
   Sur Clients et « À traiter », il ne reste donc aucune cible — ces écrans
   n'ont pas de widget au sens de petit bloc, et mieux vaut rien qu'un saut de
   page.

   LA GESTUELLE EST CELLE DE LA CAPSULE, reprise de navbar.css : le TRANSFORM
   seul, jamais la couleur de fond. bureau-02 a mesuré qu'ajouter
   « background-color » à une transition faisait disparaître le fond peint de
   sa capsule pendant que le style calculé annonçait la bonne couleur. On ne
   met donc dans la liste que ce qui se compose : transform et box-shadow. */
/* ⚠️ TROIS CIBLES RETIRÉES ICI le 12/09 — voir la note « TROISIÈME CRITÈRE »
   plus bas. « .gr-carte-tete » vivait TOUJOURS dans « .gr-carte » (3 écrans
   sur 3), et « .sx-carte » / « .sx-mesure » ne comptaient plus un seul élément
   sur les treize onglets. */
/* ═══ TROISIÈME CRITÈRE DE LA RÈGLE DE SURVOL : L'EMBOÎTEMENT ═══════════════
   Ajouté le 12/09/2026 sur une remarque de bureau-9c, et vérifié par programme
   sur les treize onglets (« cibles-emboitees.js »). La règle complète tient
   maintenant en trois conditions, dont AUCUNE ne suffit seule :

     1. SURFACE     un bloc qu'on désigne ne couvre pas plus du tiers de
                    l'écran, sinon c'est la page qui bouge et non l'objet.
     2. AIR AUTOUR  un bloc collé bord à bord à ses voisins fait onduler la
                    pile quand il monte (cas « .st-etage », 4 bandeaux).
     3. EMBOÎTEMENT un bloc qui en contient un autre DÉJÀ VISÉ n'est jamais une
                    cible : deux mouvements pour un seul geste.

   Le balayage a trouvé SIX manquements dans mes propres règles, présents
   depuis le premier jour et qu'aucune relecture n'avait vus :

     emboîtement   .gr-carte  ⊃ .gr-carte-tete   Tournée, Équipe, Portefeuilles
                   .carte     ⊃ .veille-tuile    Conversations
     surface       .carte     867x1439   98 %    Conversations
                   .carte    1265x747    75 %    Administration
                   .carte    1349x390    42 %    Statistiques
                   .gr-encart 1009x452   36 %    Équipe

   ⚠️ « .carte » À 98 % DE L'ÉCRAN, c'est très exactement le défaut que Kentin
   a décrit en disant que toute la page sautait. La classe n'est pas en cause —
   elle est bonne vingt fois sur vingt et une ; ce sont CERTAINES INSTANCES qui
   sont énormes. Le CSS ne sait pas mesurer une surface, donc il faut qu'on la
   lui dise : « animations.js » pose « data-anim-ample » sur toute cible qui
   dépasse le tiers, et les deux règles ci-dessous l'excluent.

   ⚠️ ET J'AI CRU QUE LA SURFACE SUFFIRAIT À DÉNOUER L'EMBOÎTEMENT. Elle ne
   suffit pas. Sur Conversations, la carte qui porte les tuiles de veille n'est
   PAS celle de 98 % — c'en est une autre, petite, donc jamais marquée
   « ample ». Survoler une tuile faisait monter la tuile ET sa carte : deux
   mouvements pour un geste. Le troisième critère exige donc sa propre marque,
   « data-anim-parent », posée sur toute cible qui en contient une autre. Elle
   ne se mesure pas, elle se lit dans l'arbre, et elle dépend du CONTENU —
   raison pour laquelle elle est recalculée à chaque rendu plutôt qu'écrite à
   la main dans un sélecteur qui aurait vieilli en silence.

   L'ARBITRAGE, quand deux cibles s'emboîtent : LE CONTENEUR PERD, L'ENFANT
   GARDE. C'est l'enfant qu'on désigne du doigt ; le conteneur n'est qu'un
   cadre, et le faire bouger déplace des choses que personne ne visait.

   MESURES DE CONTRÔLE (12/09, fenêtre 1440x880, Conversations) :
     marque posée sur   carte 867x1439 98 %, carte 1265x747 75 %,
                        carte 1215x391 38 %, gr-encart 1009x452 36 %
     aucun bloc > 33 % n'échappe à la marque
     carte marquée au survol   :hover vrai, transform NONE  ✔
     coût du marquage          8 blocs, 0,1 ms médian, 0,2 ms au pire
                               (les tâches longues de 182 ms relevées à côté
                               ne viennent donc PAS d'ici — vérifié, pas
                               supposé)

   ✔ « .veille-tuile » VÉRIFIÉE, PANNEAU OUVERT — et la façon dont elle a
   résisté vaut mieux que le résultat. Je l'avais d'abord portée « non
   mesurée » : les quatre tuiles de Conversations gardent une boîte de 327x83,
   elles sont dans le cadre, et pourtant « elementFromPoint » à leur centre ne
   les rendait jamais. J'ai cherché du côté du défilement — bonne piste,
   mauvaise cause. bureau-9c a nommé le vrai coupable en dix secondes :

     LE PANNEAU EST REPLIÉ. Trois niveaux plus haut, « div.dedans » est à
     height 0 avec overflow:hidden. La tuile est ROGNÉE, pas recouverte, et
     « getBoundingClientRect » n'en sait rien — il rend la taille qu'elle
     aurait si on la dépliait.

   Après un clic sur le bandeau « 5 conversation(s) en attente », même point,
   même mesure : atteinte ✔, « :hover » vrai, transform matrix(…, 0, -3),
   retour à « none » en sortant. La règle s'applique telle quelle.

   🔑 LA SIGNATURE EST MÉCANISABLE, ET C'EST LÀ L'ACQUIS : UN ÉLÉMENT QUI A UNE
   BOÎTE MAIS N'EST DANS AUCUNE PILE EST ROGNÉ PAR UN ANCÊTRE, IL N'EST PAS
   RECOUVERT. Remonter les parents en cherchant une hauteur nulle avec
   overflow hidden le dit en une ligne. C'est dans « rognage.js » et dans
   « cibles-emboitees.js ». Même animal que le « <details> fermé donne un faux
   recouvert » de bureau-85 le 11/09 — autre balise, piège identique. */
.carte,
.tuile,
.gr-carte,
.veille-tuile,
.gr-encart,
/* ⚠️ STATISTIQUES A CHANGÉ DE GRAMMAIRE le 12/09 : bureau-9c a porté les six
   écrans d'Enzo, ses blocs « .sx- » ont disparu et mes sélecteurs étaient
   devenus inertes — sans qu'aucune erreur ne le signale, exactement le piège
   que j'avais anticipé en le prévenant avant son chantier.

   Les nouvelles cibles, MESURÉES sur les six sous-onglets (part de la fenêtre
   occupée par le plus grand bloc de chaque classe) :

     GARDÉES     .st-kpis > *   5 tuiles de chiffres   251x167   4 %
                 .st-alerte     les motifs de blocage            4 %
                 .st-objectif   2 cartes d'objectif    319x162   4 %
                 .st-vendeur    la carte d'un vendeur  316x602  15 %

     ÉCARTÉES    .st-matrice    48 %  ┐ au-dessus du tiers : ce sont des fonds,
                 .st-vendeurs   45 %  │ pas des cibles qu'on désigne
                 .st-entonnoir  37 %  ┘
                 .st-case       67 blocs  ┐ trop nombreux — c'est une grille,
                 .st-comp       36 blocs  ┘ donc des lignes, pas des surfaces
                 .st-jauges     IMBRIQUÉ dans « .st-vendeur », déjà visé : le
                                faire monter ferait monter un bloc DANS un bloc
                                qui monte — le défaut corrigé plus haut.
                 .st-qui        c'est une CELLULE DE TABLEAU (tr < tbody), pas
                                une surface. La doctrine d'app.css l'interdit.
                 .st-etage      1299x73, quatre bandeaux CONTIGUS pleine
                                largeur. Sous le tiers, donc admissible à la
                                règle de surface — mais empilé bord à bord :
                                en faire monter un fait onduler la pile. C'est
                                le défaut que Kentin a appelé « horrible » sur
                                les onglets. La surface ne suffit pas ; encore
                                faut-il que le bloc ait de l'air autour.

     ⚠️ CORRECTION D'UNE AFFIRMATION FAUSSE ÉCRITE ICI MÊME quelques minutes
        plus tôt : j'avais écarté « .st-objectif » et « .st-qui » en les disant
        imbriqués dans « .st-vendeur ». Mesuré ensuite : c'est FAUX pour les
        deux (« closest('.st-vendeur') » rend null ; leurs parents réels sont
        « grille < section » et « tr < tbody »). Seul « .st-jauges » l'est.
        Écrire une raison qu'on n'a pas vérifiée coûte plus cher que ne rien
        écrire — le prochain l'aurait crue. */
.st-kpis > *,
.st-alerte,
.st-objectif,
.st-vendeur
{
  transition: transform var(--m-court) var(--glisse),
              box-shadow var(--m-moyen) var(--glisse);
}
.gr-carte:not([data-anim-ample]):not([data-anim-parent]):hover,
.veille-tuile:not([data-anim-ample]):not([data-anim-parent]):hover,
.gr-encart:not([data-anim-ample]):not([data-anim-parent]):hover,
.st-kpis > *:hover,
.st-alerte:hover,
.st-objectif:hover,
.st-vendeur:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 24px rgba(0, 0, 0, .16);
}
/* La carte garde en plus ses couleurs d'origine, qui ne coûtent rien tant
   qu'elles ne sont pas dans la même transition que le transform. */
.carte {
  transition: transform var(--m-court) var(--glisse),
              box-shadow var(--m-moyen) var(--glisse),
              border-color var(--m-court);
}
.carte:not([data-anim-ample]):not([data-anim-parent]):hover { transform: translateY(-3px); box-shadow: 0 10px 24px rgba(0, 0, 0, .16); }

/* LA TUILE SE SOULÈVE POUR DE BON. app.css lui donne deux pixels — invisible.
   Quatre, plus une ombre portée : c'est le premier bloc de chaque écran, il
   doit répondre. Le chiffre garde son propre geste, écrit dans app.css. */
.tuile:not([data-anim-ample]):not([data-anim-parent]):hover { transform: translateY(-4px); box-shadow: 0 10px 24px rgba(0, 0, 0, .16); }
.grille > .tuile { transition: transform var(--m-court) var(--glisse),
                               box-shadow var(--m-moyen),
                               border-color var(--m-court),
                               background var(--m-court); }
.carte > .chapeau h2 { transition: color var(--m-court); }

/* Le bloc qu'on prend pour le déplacer — la manipulation directe des écrans
   refaits. Il doit suivre la main sans inertie : pas de transition sur le
   transform pendant le geste, elle se remettrait entre le curseur et le
   bloc. Seule l'ombre bouge. */
[draggable="true"] { transition: box-shadow var(--m-court), opacity var(--m-court); }

/* ===========================================================================
   5. BOÎTES, CALQUES, NOTIFICATIONS
   ---------------------------------------------------------------------------
   La modale monte déjà (app.css, « monte »), le voile se fond, le toast
   glisse. Ce qui manquait : le CONTENU de la boîte, qui arrivait tout fait
   derrière un cadre en mouvement.

   La SORTIE d'une boîte n'est pas ici : un nœud retiré du DOM ne s'anime
   pas en CSS, et la retarder casserait les tests de présence d'app.js
   (« if (!$('#calques .voile')) »). C'est animations.js qui s'en charge.
   =========================================================================== */

/* Les blocs d'une modale se posent l'un après l'autre, très vite, derrière le
   cadre. Cinq au maximum : une fenêtre de préférences en compte trente, et
   trente retards font une seconde d'attente. */
.modale > .contenu > *:nth-child(-n+5) {
  animation: anim-monte calc(.26s * var(--anim-vit)) var(--ressort) backwards;
}
.modale > .contenu > *:nth-child(1) { animation-delay: calc(.07s * var(--anim-vit)); }
.modale > .contenu > *:nth-child(2) { animation-delay: calc(.1s  * var(--anim-vit)); }
.modale > .contenu > *:nth-child(3) { animation-delay: calc(.13s * var(--anim-vit)); }
.modale > .contenu > *:nth-child(4) { animation-delay: calc(.155s * var(--anim-vit)); }
.modale > .contenu > *:nth-child(5) { animation-delay: calc(.18s * var(--anim-vit)); }

/* Le pied d'une modale — les deux boutons de décision — arrive en dernier.
   On lit la question avant de voir la réponse. */
.modale > footer {
  animation: anim-monte calc(.26s * var(--anim-vit)) var(--ressort) backwards;
  animation-delay: calc(.11s * var(--anim-vit));
}

/* LA FENÊTRE QUI S'OUVRE. app.css la fait monter de 14 px en deux dixièmes
   (« monte ») — un geste que Kentin ne voyait pas. On reprend le sien, plus
   ample et un peu plus long, par un sélecteur à deux classes qui l'emporte
   sur « .modale » seul. */
@keyframes anim-modale-entre {
  from {
    opacity: 0;
    transform: translateY(calc(26px * var(--anim-amp)))
               scale(calc(1 - .045 * var(--anim-amp)));
  }
  to { opacity: 1; transform: none; }
}
.voile .modale {
  animation: anim-modale-entre calc(.36s * var(--anim-vit)) var(--ressort);
}

/* La notification arrive de plus loin sur le côté. */
@keyframes anim-toast-entre {
  from { opacity: 0; transform: translateX(calc(46px * var(--anim-amp))); }
  to   { opacity: 1; transform: none; }
}
.toasts .toast {
  animation: anim-toast-entre calc(.34s * var(--anim-vit)) var(--ressort);
}

/* Un menu flottant s'ouvre depuis le bord qui l'a appelé plutôt que depuis
   son milieu : on voit d'où il sort. app.css lui donne déjà son animation
   d'entrée ; on ne fait qu'ancrer la transformation. */
.flottant { transform-origin: top center; }
.flottant.vers-haut { transform-origin: bottom center; }

/* Le voile en fond de modale : son flou se pose au lieu d'apparaître. Un
   backdrop-filter qui s'allume d'un coup se voit comme un clignement.
   Attention : l'animer coûte une passe de composition à chaque image —
   d'où la durée courte, et rien d'autre dans la même règle. */
@keyframes anim-flou {
  from { backdrop-filter: blur(0px); }
  to   { backdrop-filter: blur(3px); }
}
.voile:not(.seuil):not(.verrou) {
  animation: fondu calc(.16s * var(--anim-vit)) ease,
             anim-flou calc(.22s * var(--anim-vit)) ease;
}

/* Le toast qui s'en va : animations.js lui pose « .part », et il se retire
   par où il est arrivé. Défini ici, joué là-bas. */
@keyframes anim-toast-part {
  from { opacity: 1; transform: none; max-height: 120px; }
  to   { opacity: 0; transform: translateX(calc(24px * var(--anim-amp))); max-height: 0; }
}
.toast.part {
  animation: anim-toast-part calc(.2s * var(--anim-vit)) var(--sortie) forwards;
  pointer-events: none;
  overflow: hidden;
}

/* Le fantôme d'une boîte qui se ferme — voir animations.js. Il est inerte :
   aucun clic ne l'atteint, et il disparaît de lui-même. */
/* LA SPÉCIFICITÉ EST LA DIFFICULTÉ ICI, et elle se paie en silence.

   Le fantôme est une COPIE du voile : il en garde toutes les classes, donc la
   règle d'entrée ci-dessus — trois classes, « .voile:not(.seuil):not(.verrou) »
   — s'applique encore à lui. Une classe de plus ne suffit pas à la battre :
   le fantôme rejouerait son arrivée au lieu de s'en aller. D'où le sélecteur
   doublé, qui compte quatre classes dans le cas courant et laisse l'ordre des
   feuilles trancher pour le seuil et le verrou. */
.anim-fantome,
.anim-fantome.voile:not(.seuil):not(.verrou) {
  position: fixed; inset: 0; z-index: 69;
  pointer-events: none;
  animation: anim-boite-part calc(.17s * var(--anim-vit)) var(--sortie) forwards;
}
@keyframes anim-boite-part {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.anim-fantome > .modale {
  animation: anim-modale-part calc(.17s * var(--anim-vit)) var(--sortie) forwards;
}
@keyframes anim-modale-part {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(calc(10px * var(--anim-amp))) scale(calc(1 - .015 * var(--anim-amp))); }
}

/* ===========================================================================
   6. BOUTONS, CHAMPS, CASES — les micro-gestes
   =========================================================================== */

/* Le halo de focus au clavier. app.css pose le trait, il arrivait sec. */
.btn:focus-visible, .nav button:focus-visible, select:focus-visible {
  transition: box-shadow var(--m-court), border-color var(--m-court),
              background var(--m-court), transform var(--m-vif) var(--ressort);
}

/* Un bouton principal porte une ombre courte au survol : c'est le seul de
   l'écran qu'on veut voir répondre de loin. */
.btn.primaire { transition: background var(--m-court), border-color var(--m-court),
                            transform var(--m-vif) var(--ressort), box-shadow var(--m-moyen); }
.btn.primaire:hover:not(:disabled) {
  box-shadow: 0 4px 14px color-mix(in srgb, var(--acc) 28%, transparent);
}
.btn.danger:hover:not(:disabled) {
  box-shadow: 0 4px 14px color-mix(in srgb, var(--danger) 24%, transparent);
}

/* Un bouton ordinaire se lève de deux pixels au lieu d'un, et s'enfonce plus
   franchement sous le doigt. Les deux règles d'app.css (« .btn:hover » et
   « .btn:active ») ne comptent qu'une classe ; celles-ci aussi, mais cette
   feuille vient après. */
.btn:hover:not(:disabled) { transform: translateY(-2px); }
.btn:active:not(:disabled) { transform: translateY(1px) scale(.94); }

/* L'icône d'un bouton suit le bouton. */
.btn svg { transition: transform var(--m-court) var(--ressort); }
.btn:hover:not(:disabled) svg { transform: scale(1.08); }

/* Un bouton désactivé qui redevient cliquable : le passage se voit. */
.btn:disabled { transition: opacity var(--m-moyen), transform var(--m-vif); }

/* La coche d'une case apparaissait d'un coup, à pleine taille. Elle se pose
   maintenant en grandissant depuis son centre — c'est le seul endroit de
   l'interface où un rebond franc se justifie : on vient d'appuyer dessus. */
.case .boite svg {
  transition: opacity var(--m-vif), transform var(--m-court) var(--ressort);
  transform: scale(.4);
}
.case input:checked + .boite svg { transform: scale(1); }
.case input:checked + .boite { transform: scale(1.06); }
.case .boite { transition: background var(--m-court), border-color var(--m-court),
                           box-shadow var(--m-court), transform var(--m-vif) var(--ressort); }
.case:active .boite { transform: scale(.92); }
.cases .case { transition: background var(--m-court), border-color var(--m-court),
                           transform var(--m-vif) var(--ressort), box-shadow var(--m-court); }
.cases .case:active { transform: scale(.985); }

/* L'étiquette d'un champ prend la couleur d'accent quand on écrit dedans.
   Rien ne bouge : sur un formulaire de quinze lignes, quinze étiquettes qui
   sautent seraient insupportables. C'est la couleur qui dit où l'on est. */
.champ > label { transition: color var(--m-court); }
.champ:focus-within > label { color: var(--acc-vif); }

/* Le segment choisi : app.css anime déjà le fond et l'appui. On ajoute la
   graisse du texte, qui changeait d'un coup. */
.segmente button, .onglets button { transition-property: background, color, transform, font-weight; }

/* ===========================================================================
   7. REPLIS ET ACCORDÉONS
   ---------------------------------------------------------------------------
   La hauteur d'un <details> n'est pas animable tant que le contenu n'a pas de
   hauteur connue ; interpoler « auto » demande interpolate-size, dont on ne
   dépend pas ici. Ce qui s'anime, c'est le CONTENU : il se pose au lieu
   d'apparaître, et l'œil lit l'ouverture pareillement.
   =========================================================================== */
.repli > summary, details > summary {
  transition: color var(--m-court), background var(--m-court);
}
.repli[open] > *:not(summary),
details[open] > *:not(summary) {
  animation: anim-monte var(--m-moyen) var(--ressort) backwards;
}

/* Le chevron d'un repli pivote — quel que soit le nom qu'on lui a donné. */
.repli > summary svg, details > summary svg {
  transition: transform var(--m-moyen) var(--ressort);
}
.repli[open] > summary svg, details[open] > summary svg { transform: rotate(90deg); }

/* Un bandeau déroulant d'app.css garde son animation ; on lui ajoute
   seulement de quoi se refermer proprement si la classe se retire. */
.deroulant { transition: grid-template-rows var(--m-moyen) var(--ressort); }

/* ===========================================================================
   8. CE QUI CHARGE
   =========================================================================== */

/* Le voile de chargement posé par-dessus un bloc : il s'installe en fondu
   plutôt que de noircir d'un coup. */
.voile-chargement { animation: anim-paraitre calc(.18s * var(--anim-vit)) ease; }

/* Un bloc en cours de chargement respire. Très faiblement — 100 % à 62 % —
   et lentement : c'est une attente, pas une alarme. */
@keyframes anim-respire {
  0%, 100% { opacity: 1; }
  50%      { opacity: .62; }
}
.chargeant-doux, [aria-busy="true"] {
  animation: anim-respire calc(1.4s * var(--anim-vit)) ease-in-out infinite;
}

/* ===========================================================================
   9. LE RETRAIT
   ---------------------------------------------------------------------------
   Un seul bloc pour tout arrêter. Deux voies y mènent : le réglage système
   de l'usager, et le curseur des Préférences — ce dernier passe par
   --anim-vit à zéro, qui annule déjà toutes les durées ci-dessus puisque
   chacune est un calc(). Ce bloc-ci couvre le premier cas.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .corps, .lateral { transition: none; }
  /* Le sélecteur long de la barre du haut a besoin du sien : rien de moins
     spécifique ne l'éteindrait. */
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button,
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button:hover,
  html:root:not([data-barre="gauche"]) .appli .lateral .nav button:active {
    transition: none;
    transform: none;
  }
  .nav button, .nav button svg, .nav-bas button,
  .histo button, .outils-titre button, .fenetre-actions button,
  .bascule-menu, .replier-lateral, #ouvrir-preferences svg,
  .marque .pastille-logo, .liste > .rang, .rang .ava, .btn svg,
  .case .boite, .case .boite svg, .cases .case {
    transition: none;
  }
  .nav button:hover, .nav button:hover svg, .nav button:active,
  .nav-bas button:active, .histo button:not(:disabled):hover,
  .outils-titre button:hover, .outils-titre button:active,
  .replier-lateral:hover, .bascule-menu:active,
  #ouvrir-preferences:hover svg, .marque:hover .pastille-logo,
  .liste > .rang:hover, .liste > .rang:active, .rang:hover .ava,
  .btn:hover:not(:disabled) svg, .case:active .boite,
  .cases .case:active, .case input:checked + .boite {
    transform: none;
  }
  .case .boite svg { transform: scale(1); }

  [class*="onglets"] button, [class*="encoche"] button, .segmente button,
  .cli .cli-rang, .gr-page .gr-rang,
  .cli .cli-rang .poignee-rang, .gr-page .gr-rang .poignee-rang { transition: none; }
  .anim-panneau, .anim-onglet-pris,
  .voile .modale, .toasts .toast,
  #vue.entrant > .page,
  .anim-ecran-part, .anim-ecran-part.va,
  .modale > .contenu > *, .modale > footer,
  .repli[open] > *:not(summary), details[open] > *:not(summary),
  .voile-chargement, .chargeant-doux, [aria-busy="true"] {
    animation: none;
  }
  .voile:not(.seuil):not(.verrou) { animation: fondu .01s linear; }
  .repli[open] > summary svg, details[open] > summary svg { transition: none; }
  .toast.part, .anim-fantome, .anim-fantome > .modale {
    animation-duration: .01s;
  }
  .btn.primaire:hover:not(:disabled), .btn.danger:hover:not(:disabled) {
    box-shadow: none;
  }
  .btn:hover:not(:disabled), .btn:active:not(:disabled),
  .tuile:hover, .carte { transform: none; box-shadow: none; }
  .grille > .tuile, .carte, .gr-carte, .veille-tuile,
  .gr-encart, .st-kpis > *, .st-alerte, .st-objectif, .st-vendeur { transition: none; }
  .gr-carte:hover, .veille-tuile:hover,
  .gr-encart:hover, .carte:hover,
  .st-kpis > *:hover, .st-alerte:hover, .st-objectif:hover,
  .st-vendeur:hover { transform: none; box-shadow: none; }
}

/* ===========================================================================
   LES FAISCEAUX DE LA LAMPE — DETTE SOLDÉE, RIEN À VOIR ICI
   ---------------------------------------------------------------------------
   Une surcharge « .cx-fond .cx-vague.v1, .v2 { display: none } » a vécu ici le
   12/09, le temps d'éteindre les deux jets de lumière dont Kentin ne voulait
   pas. Elle était provisoire faute de pouvoir joindre la session propriétaire
   du bloc — je la cherchais sous sa marque, « bureau-3f », alors qu'elle
   s'adresse sous son nom de session, « bureau-a8 ». Deux espaces de noms qu'on
   confond : les marques de bloc survivent aux sessions, les adresses non.

   bureau-a8 a repris l'extinction dans son propre bloc (app.css ~l. 19355),
   et ma surcharge est donc retirée — vérifié avant de la supprimer, pas après.

   ⚠️ ET SURTOUT : NE SUPPRIMEZ PAS SA RÈGLE EN CROYANT NETTOYER. « v1 » et
   « v2 » ne sont à personne de nous deux : elles existent dans app.js depuis
   bureau-e0, où elles dessinent deux longues courbes lumineuses. Retirer le
   « display: none » ne les éteindrait pas — ça les ferait REVENIR sous leur
   forme d'origine. C'est pour cette raison qu'elle est écrite comme une
   extinction explicite et non comme une suppression.
   =========================================================================== */

/* ===========================================================================
   L'ANNEAU LUMINEUX — d'après « Glowing Effect » d'Aceternity UI
   ---------------------------------------------------------------------------
   Demandé par Kentin le 12/09/2026, et tranché par lui en deux temps :

     1. « je veux cet effet PARTOUT » ;
     2. puis, entre trois options : L'ANNEAU N'APPARAÎT QU'AU SURVOL — rien au
        repos, surface nue ;
     3. puis, entre surfaces et lignes de liste : LES SURFACES.

   Le deuxième arbitrage est le meilleur des trois, et ce n'est ni moi ni
   bureau-9c qui l'avions proposé. Poser un anneau permanent aurait réintroduit
   le cadre que la refonte de Statistiques venait justement de retirer — trois
   niveaux d'encadrement imbriqués. Au survol seulement, l'anneau cesse d'être
   une bordure décorée : il DEVIENT le signal. L'épure est gardée et l'effet
   est partout.

   Le troisième évite l'absurde : 436 éléments bordés, dont 225 lignes de
   visites sur le seul écran Statistiques. Un anneau lumineux sur chaque ligne
   d'une liste de 225 n'est plus un effet, c'est un survol coloré qui a coûté
   cher. Les lignes gardent leur propre réponse — un fond qui se déplace.

   ---------------------------------------------------------------------------
   COMMENT L'ANNEAU EXISTE SANS BORDURE

   Une couche en surimpression porte un dégradé conique dont l'origine suit le
   curseur, et un masque ne garde que le pourtour : deux masques superposés,
   l'un sur la boîte entière, l'autre sur la boîte moins le remplissage, et
   « mask-composite » ne conserve que leur différence. Il reste un anneau de
   deux pixels, épousant le rayon de la surface par « border-radius: inherit ».

   La surface n'a donc besoin d'AUCUNE bordure préalable — c'est ce qui lève
   l'objection de bureau-9c, et ce qui permet de poser l'effet sur ses tuiles
   épurées sans leur remettre un cadre.

   ⚠️ « --start » (l'angle) et « --active » (0 ou 1) sont posés par
   animations.js sur la SEULE surface survolée — voir « L'ANNEAU LUMINEUX QUI
   SUIT LE CURSEUR » là-bas, pour le choix de l'écouteur unique et la mise en
   cache de la géométrie.

   ⚠️ CE RENVOI A ÉTÉ FAUX PENDANT PLUSIEURS HEURES, et il faut le dire ici :
   ce bloc CSS a été écrit avant le JS, et le commentaire annonçait au présent
   un code qui n'existait pas. La feuille était chargée, la règle dans la
   cascade, et ZÉRO élément portait la classe. Kentin a redemandé deux fois
   avant que bureau-9c ne mesure et ne trouve. Un commentaire qui affirme
   l'existence de ce qui manque coûte plus cher qu'un code non commenté : le
   suivant cherche le défaut là où il n'est pas.
   =========================================================================== */

/* ⚠️ L'ANNEAU NE VIT PLUS QUE SUR L'ÉCRAN DE CONNEXION — décision de Kentin,
   12/09 : « finalement enlève Glowing Effect, ça fait pas pro ; laisse-le pour
   l'écran de connexion mais le reste enlève-le ».

   Mesuré avant de borner, parce que « le laisser » n'a de sens que s'il y
   était : la vitrine du seuil porte UNE surface visée par ce bloc — la carte
   du formulaire, 468 × 726. L'effet a donc quelque chose à habiller, et le
   retrait ailleurs ne laisse pas un écran vide de tout.

   « body:has(.cx-vitrine) » est le motif que nous employons tous pour désigner
   le seuil SANS attraper la demande de code qui suit la connexion — là,
   app.css garde délibérément la barre cliquable. Un seul motif pour « au
   seuil », chez tout le monde.

   ⚠️ CE QUI NE DOIT PAS DISPARAÎTRE AVEC LE BLOC, et qui vaut au-delà de cet
   effet : Kentin avait tranché, entre trois options, que l'anneau ne se
   montrerait QU'AU SURVOL. C'était meilleur que ce que bureau-9c et moi
   proposions, et pour une raison générale — UN ANNEAU PERMANENT RÉINTRODUIT LE
   CADRE QUE L'ÉPURE VENAIT DE RETIRER. Statistiques avait justement perdu
   trois niveaux d'encadrement imbriqués. Au survol seulement, l'anneau cesse
   d'être une bordure décorée : il devient le signal. À se rappeler le jour où
   quelqu'un voudra poser une bordure lumineuse quelque part. */
body:has(.cx-vitrine) .anim-anneau {
  position: relative;
}
body:has(.cx-vitrine) .anim-anneau::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 2;
  /* ⚠️ ÉPAISSEUR ET ARC RELEVÉS APRÈS CAPTURE. Première version : 2 px et 42°
     d'arc. Mesuré, tout était juste — ::after à l'opacité 1, dégradé conique
     posé, angle suivi au dixième de degré. Photographié, on distinguait à
     peine un éclaircissement. C'est la leçon du jour, une fois de plus : un
     instrument qui dit « allumé » ne dit pas « visible ».

     Trois pixels, un arc de 70°, et un blanc au sommet de la lueur plutôt que
     l'accent seul : c'est le contraste avec le fond qui fait exister un
     anneau, pas sa couleur déclarée. */
  padding: 3px;                       /* l'épaisseur de l'anneau */
  border-radius: inherit;
  pointer-events: none;
  background: conic-gradient(
    from calc(var(--start, 0) * 1deg),
    transparent 0deg,
    color-mix(in srgb, var(--acc) 55%, transparent) 12deg,
    color-mix(in srgb, var(--acc) 70%, #fff) 28deg,
    #fff 35deg,
    color-mix(in srgb, var(--acc) 70%, #fff) 42deg,
    color-mix(in srgb, var(--acc) 55%, transparent) 58deg,
    transparent 70deg);
  /* La lueur déborde légèrement de l'arête : sans elle, l'anneau est un trait,
     et un trait ne brille pas. */
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--acc) 60%, transparent));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity calc(.22s * var(--anim-vit, 1)) ease;
}
/* Au repos, rien : « --active » vaut 0 et l'anneau n'existe pas. */
body:has(.cx-vitrine) .anim-anneau.anneau-actif::after {
  opacity: var(--active, 0);
}

/* Un halo très léger accompagne l'anneau sur les grandes surfaces — pas sur
   les petites, où il déborderait plus qu'il n'éclairerait. */
body:has(.cx-vitrine) .anim-anneau.anneau-actif.anneau-large::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    120px 120px at var(--sx, 50%) var(--sy, 50%),
    color-mix(in srgb, var(--acc) 13%, transparent) 0%,
    transparent 70%);
  opacity: var(--active, 0);
  transition: opacity calc(.22s * var(--anim-vit, 1)) ease;
}

/* --- LE RETRAIT ------------------------------------------------------------
   Cet effet suit le curseur en permanence : c'est précisément le genre
   d'animation qui ne s'arrête jamais, donc celui qu'il faut savoir éteindre.
   animations.js ne pose aucun écouteur quand --anim-vit vaut zéro ; ce bloc
   couvre le réglage système. */
@media (prefers-reduced-motion: reduce) {
  body:has(.cx-vitrine) .anim-anneau::after,
  body:has(.cx-vitrine) .anim-anneau.anneau-actif::after,
  body:has(.cx-vitrine) .anim-anneau.anneau-actif.anneau-large::before {
    display: none;
  }
}

/* ===========================================================================
   LE LOGO DMF SE COMPOSE À L'ARRIVÉE SUR LA PAGE DE CONNEXION
   ---------------------------------------------------------------------------
   Demande de Kentin, 12/09/2026 : « quand on arrive sur la page de connexion,
   l'animation du triangle DMF se lance ».

   ⚠️ ELLE EXISTAIT DÉJÀ, MAIS AILLEURS DANS LE TEMPS. app.css la décrit en
   détail — le triangle se réduit à 26 %, « DMF » sort de derrière lui, le
   trait se déploie dessous, et tout s'immobilise. Seulement elle est attachée
   à « body.cx-charge », que app.js pose AU MOMENT DE LA CONNEXION (l. 19544).
   À l'arrivée sur la page, le logo est donc au repos : le triangle flotte,
   orange, et le mot attend derrière lui, sa boîte découpée à zéro.

   Mesuré avant d'écrire : à 200 ms comme à 3,2 s, « .cx-nom » porte bien le
   texte DMF, opacité 1 — c'est son clip-path qui le cache. L'animation ne
   manquait pas, elle n'était pas déclenchée.

   ⚠️ ON N'EMPRUNTE PAS « cx-charge », ET C'EST LE POINT DÉLICAT. Cette classe
   ne fait pas que révéler le logo : elle bascule le fond de l'orange au bleu,
   allume la légende « chargement » sous le triangle, et lance le fil de
   progression. La poser à l'arrivée ferait dire à l'écran qu'il charge quelque
   chose alors qu'il attend une saisie. On reprend donc les mêmes gestes, aux
   mêmes durées et aux mêmes courbes, sous une classe à nous — et rien d'autre.

   LE RETOUR EST GRATUIT : app.css fait vivre les valeurs de repos sur l'état
   de repos justement pour que le logo revienne tout seul quand la classe
   part. Retirer la marque au bout de trois secondes suffit à replier le mot et
   à rendre au triangle sa taille. La séquence de connexion, elle, reste
   entière pour le moment où l'on entre vraiment.
   =========================================================================== */

/* Le triangle recule et se réduit — mêmes valeurs que body.cx-charge. */
body.anim-logo-arrivee .cx-echelle {
  transform: translateX(-91.5px) scale(.26);
  transition: transform calc(1s * var(--anim-vit)) cubic-bezier(.22, .78, .18, 1)
              calc(.85s * var(--anim-vit));
}
/* Le mot s'ouvre de gauche à droite, et glisse à l'intérieur de sa boîte :
   deux gestes à la même durée et au même retard, sinon on en voit deux. */
body.anim-logo-arrivee .cx-nom {
  -webkit-clip-path: inset(0 0 0 0);
          clip-path: inset(0 0 0 0);
  transition: clip-path calc(.9s * var(--anim-vit)) cubic-bezier(.22, .78, .18, 1)
              calc(1.15s * var(--anim-vit));
}
body.anim-logo-arrivee .cx-nom span {
  transform: translateX(0);
  transition: transform calc(.9s * var(--anim-vit)) cubic-bezier(.22, .78, .18, 1)
              calc(1.15s * var(--anim-vit));
}
/* Le trait sous le logo, en dernier. */
body.anim-logo-arrivee .cx-trait {
  opacity: 1;
  -webkit-clip-path: inset(0 0 0 0);
          clip-path: inset(0 0 0 0);
  transition: clip-path calc(1s * var(--anim-vit)) cubic-bezier(.22, .78, .18, 1)
              calc(1.65s * var(--anim-vit)),
              opacity calc(.45s * var(--anim-vit)) ease calc(1.65s * var(--anim-vit));
}
/* ET TOUT S'ARRÊTE — la règle d'app.css que je reprends telle quelle : un logo
   complet est une image posée, pas une animation d'attente. Sans elle, le
   triangle continuerait de dériver sous son propre nom. */
body.anim-logo-arrivee .cx-flotte,
body.anim-logo-arrivee .cx-marque,
body.anim-logo-arrivee .cx-sol,
body.anim-logo-arrivee .cx-face.avant::after { animation: none; }

/* La légende « chargement » NE DOIT PAS apparaître : rien ne charge. Elle est
   muette au repos et le reste — on ne la nomme ici que pour dire qu'on ne la
   touche pas volontairement. */

@media (prefers-reduced-motion: reduce) {
  body.anim-logo-arrivee .cx-echelle,
  body.anim-logo-arrivee .cx-nom,
  body.anim-logo-arrivee .cx-nom span,
  body.anim-logo-arrivee .cx-trait { transition: none; }
}
