/*
 * Charte des outils EEIF : le mouvement. Jetons, ressorts et primitives communes, d'après la
 * synthèse validée le 25/09/2026 (docs/charte.md, « Mouvement »). Écrit ici, à reporter dans la
 * charte. Chargé après complements.css par <x-ei.page>, avec ressort.js et mouvement.js.
 *
 * Règles :
 * - n'animer que transform, opacity et filter : une taille ou une position change par FLIP
 *   (EIMouvement.reagencer) ou par View Transition, jamais par width, height, top ni left ;
 * - pas de will-change sur un élément qui contient du texte mis à l'échelle ;
 * - aucun attribut style : le JavaScript passe par le CSSOM (CSP stricte, Trusted Types) ;
 * - chaque mouvement a sa version réduite (bas de ce fichier) : fondu court, ou rien.
 */

/* ---------- Jetons ---------- */

:root {
    --duree-appui: 100ms;        /* Appui : échelle 0,97. */
    --duree-couleur: 150ms;      /* Couleur d'un bouton ou d'une pastille, survol compris. */
    --duree-sortie: 90ms;        /* Contenu qui sort : fondu, flou, 8 px vers l'arrière. */
    --duree-entree: 200ms;       /* Contenu qui entre : fondu, flou vers net, 8 px dans le sens du parcours. */
    --delai-entree: 80ms;        /* L'entrée attend que la sortie soit invisible : jamais deux textes lisibles ensemble. */
    --attente-indicateur: 150ms; /* Rien ne s'affiche avant : pas d'éclair sur un réseau rapide. */
    --attente-squelette: 300ms;  /* Délai avant un squelette. */
    --decalage-liste: 40ms;      /* Entrées décalées, cinq au plus, pour une première arrivée seulement. */

    /* Trois ressorts de masse 1 (raideur / amortissement), solution exacte, en 24 points : la
       courbe finit sur la cible et sa durée en fait partie (les deux jetons changent ensemble).
       Les mêmes, en JavaScript : ressort.js. */
    --ressort-vif: linear(0, 0.0368, 0.1214, 0.2267, 0.3363, 0.4409, 0.5358, 0.6191, 0.6903, 0.7502, 0.7998, 0.8405, 0.8735, 0.9001, 0.9214, 0.9384, 0.9518, 0.9628, 0.9723, 0.9804, 0.9873, 0.9929, 0.9971, 1); /* 1850 / 86, sans dépassement : relâchement d'un appui, menus. */
    --duree-vif: 160ms;
    --ressort-standard: linear(0, 0.0344, 0.1144, 0.2151, 0.3212, 0.4237, 0.5177, 0.601, 0.6731, 0.7343, 0.7855, 0.8278, 0.8625, 0.8907, 0.9135, 0.9318, 0.9464, 0.9584, 0.9689, 0.9779, 0.9856, 0.9919, 0.9968, 1); /* 560 / 47, sans dépassement : états, remplacements, réagencements. */
    --duree-etat: 280ms;
    --ressort-ample: linear(0, 0.0351, 0.1204, 0.232, 0.3532, 0.4725, 0.5828, 0.6802, 0.763, 0.8312, 0.8856, 0.9277, 0.9592, 0.9819, 0.9975, 1.0076, 1.0135, 1.016, 1.0155, 1.0131, 1.0097, 1.006, 1.0026, 1); /* 270 / 26, dépassement de 1,6 % : élément partagé qui voyage. */
    --duree-partage: 400ms;
    --sortie: cubic-bezier(0, 0, 0.2, 1); /* Une sortie s'efface dès le premier instant. */

    /* Amplitudes, mises à zéro en mouvement réduit. */
    --flou-contenu: 2px;         /* 0 sur un site public qui couvre l'écran d'un téléphone (Signal'EI). */
    --distance-contenu: 8px;
    --echelle-appui: 0.97;
}

/* ---------- Page ---------- */

/* Rien ne déborde en largeur, même pendant un mouvement (une carte lancée, un élément qui voyage) :
   clip ne crée pas de défilement, donc position: sticky continue de marcher. */
.ei-page { overflow-x: clip; }

/* ---------- Appui et couleurs ---------- */

/* Appui : échelle 0,97 en 100 ms, relâchement sur le ressort vif. Couleurs en fondu court. */
.ei-btn, .ei-bouton-icone {
    transition: transform var(--duree-vif) var(--ressort-vif),
                background-color var(--duree-couleur) ease-out,
                color var(--duree-couleur) ease-out,
                border-color var(--duree-couleur) ease-out;
}
.ei-btn:active, .ei-bouton-icone:active {
    transform: scale(var(--echelle-appui));
    transition-duration: var(--duree-appui);
    transition-timing-function: ease-out;
}
.ei-btn:is([disabled], [aria-disabled="true"]):active { transform: none; }
.ei-lien-action, .ei-menu__item, .ei-outils__item { transition: background-color var(--duree-couleur) ease-out, color var(--duree-couleur) ease-out; }

/* ---------- Bouton à états (x-ei.bouton, mouvement.js) ---------- */

/* Libellé, attente, succès et erreur superposés dans la même case de grille : la largeur est celle
   du plus long et ne change jamais. data-etat choisit ce qui se voit : envoi (aria-busy, rien ne
   change avant 150 ms), attente, succes, erreur ; sans data-etat, le repos. */
.ei-btn__pile { display: inline-grid; }
.ei-btn__pile > span {
    grid-area: 1 / 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--espace-2);
    transition: opacity var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                transform var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                filter var(--duree-entree) var(--ressort-standard) var(--delai-entree);
}
/* Ce qui n'est pas l'état courant sort : 90 ms, dès le premier instant. */
.ei-btn__pile > span:not(.ei-btn__libelle),
.ei-btn:is([data-etat="attente"], [data-etat="succes"], [data-etat="erreur"]) .ei-btn__libelle {
    opacity: 0;
    transform: translateY(calc(var(--distance-contenu) / -2));
    filter: blur(var(--flou-contenu));
    transition: opacity var(--duree-sortie) var(--sortie),
                transform var(--duree-sortie) var(--sortie),
                filter var(--duree-sortie) var(--sortie);
}
.ei-btn__pile > span:not(.ei-btn__libelle) { transform: translateY(calc(var(--distance-contenu) / 2)); }
.ei-btn[data-etat="attente"] .ei-btn__attente,
.ei-btn[data-etat="succes"] .ei-btn__succes,
.ei-btn[data-etat="erreur"] .ei-btn__erreur {
    opacity: 1;
    transform: none;
    filter: none;
    transition: opacity var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                transform var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                filter var(--duree-entree) var(--ressort-standard) var(--delai-entree);
}
.ei-btn--primaire[data-etat="succes"] { background: var(--succes); color: var(--sur-bleu); }
.ei-btn:is(.ei-btn--primaire, .ei-btn--danger)[data-etat="erreur"] { background: var(--danger); color: var(--sur-danger); }
.ei-btn--secondaire[data-etat="succes"] { border-color: var(--succes); color: var(--succes); }
.ei-btn--secondaire[data-etat="erreur"] { border-color: var(--danger); color: var(--danger); }
.ei-btn[aria-busy="true"] { cursor: progress; }
/* L'indicateur ne tourne que lorsqu'il se voit : aucune boucle cachée. */
.ei-btn__attente .ei-spinner { animation: none; }
.ei-btn[data-etat="attente"] .ei-btn__attente .ei-spinner { animation: ei-tourne 0.8s linear infinite; }

/* ---------- Pastille de statut ---------- */

/* x-ei.badge dont le mot change sur place (morph htmx, JavaScript) : les mots sont superposés, la
   largeur ne bouge pas ; seul celui qui n'est pas aria-hidden se voit. La couleur change en 150 ms,
   puis le mot : l'ancien sort, le nouveau entre.
   <span class="ei-badge ei-badge--succes ei-pastille"><span aria-hidden="true">En attente</span><span>✓ Validé</span></span> */
.ei-badge { transition: background-color var(--duree-couleur) ease-out, color var(--duree-couleur) ease-out; }
.ei-pastille { display: inline-grid; }
.ei-pastille > * {
    grid-area: 1 / 1;
    text-align: center;
    white-space: nowrap;
    transition: opacity var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                transform var(--duree-entree) var(--ressort-standard) var(--delai-entree),
                filter var(--duree-entree) var(--ressort-standard) var(--delai-entree);
}
.ei-pastille > [aria-hidden="true"] {
    opacity: 0;
    transform: translateY(calc(var(--distance-contenu) / 2));
    filter: blur(var(--flou-contenu));
    transition: opacity var(--duree-sortie) var(--sortie),
                transform var(--duree-sortie) var(--sortie),
                filter var(--duree-sortie) var(--sortie);
}

/* ---------- Menus, dialogues et messages ---------- */

/* Ce qui apparaît. Menu du compte et sélecteur d'outils (details.ei-menu) : 160 ms depuis leur
   déclencheur (fondu, échelle 0,96 à 1, ressort vif), fermeture en fondu court là où
   ::details-content existe (sinon, d'un coup). Dialogue : 280 ms, fond en fondu. Un message posé
   au chargement (confirmation, erreur générale) porte ei-entree : fondu, 4 px, flou vers net. */
.ei-menu > .ei-menu__panneau {
    transform-origin: top right;
    transition: opacity var(--duree-vif) var(--ressort-vif), transform var(--duree-vif) var(--ressort-vif);
}
.ei-menu:not([open]) > .ei-menu__panneau { opacity: 0; transform: translateY(-4px) scale(0.96); }
.ei-menu::details-content { transition: content-visibility 120ms allow-discrete; }
@starting-style {
    .ei-menu[open] > .ei-menu__panneau { opacity: 0; transform: translateY(-4px) scale(0.96); }
}
.ei-dialog, .ei-dialog::backdrop {
    opacity: 0;
    transition: opacity var(--duree-etat) var(--ressort-standard),
                transform var(--duree-etat) var(--ressort-standard),
                overlay var(--duree-sortie) allow-discrete,
                display var(--duree-sortie) allow-discrete;
}
.ei-dialog { transform: translateY(var(--distance-contenu)); }
.ei-dialog[open], .ei-dialog[open]::backdrop { opacity: 1; transform: none; }
@starting-style {
    .ei-dialog[open], .ei-dialog[open]::backdrop { opacity: 0; }
    .ei-dialog[open] { transform: translateY(var(--distance-contenu)); }
}
.ei-entree { animation: ei-entree var(--duree-entree) var(--ressort-standard) both; }

/* ---------- View Transitions : règles de base ---------- */

/* Entre deux pages du même outil comme dans une page (htmx : hx-swap « transition:true »). La
   racine n'est pas capturée : seul ce qui est nommé s'anime, l'en-tête et la barre latérale ne font
   jamais d'image double, et la page reste cliquable pendant la transition. Sans View Transition
   (Firefox entre deux pages), la page change d'un coup, comme avant. */
@view-transition { navigation: auto; }
:root { view-transition-name: none; }
::view-transition { pointer-events: none; }

/* Sens du parcours : ei-vt-precedent sur <html> le temps d'une transition (étape ou page
   précédente, retour arrière), posée par mouvement.js. */
:root { --ei-sens: 1; }
:root.ei-vt-precedent { --ei-sens: -1; }

/* Remplacement de contenu : le conteneur reste, seul son contenu change. Nom posé en CSS pour un
   élément unique (view-transition-name et view-transition-class: ei-remplace), en JavaScript pour
   le reste (EIMouvement.nommer). L'ancien sort en 90 ms, 8 px vers l'arrière ; le nouveau entre à
   80 ms, en 200 ms, 8 px dans le sens du parcours : jamais deux textes lisibles ensemble. Si le
   conteneur change de taille, c'est son groupe qui la change (280 ms), jamais height ; les images
   gardent leur taille, donc le texte n'est jamais mis à l'échelle.
   ei-sur-place : un élément change d'état sans bouger (ligne modifiée) : même fondu, sans décalage
   latéral. */
::view-transition-group(*.ei-remplace), ::view-transition-group(*.ei-sur-place) {
    animation-duration: var(--duree-etat);
    animation-timing-function: var(--ressort-standard);
}
::view-transition-old(*.ei-remplace), ::view-transition-new(*.ei-remplace),
::view-transition-old(*.ei-sur-place), ::view-transition-new(*.ei-sur-place) { inline-size: auto; block-size: auto; }
::view-transition-old(*.ei-remplace) { animation: ei-sortie-sens var(--duree-sortie) var(--sortie) both; }
::view-transition-new(*.ei-remplace) { animation: ei-entree-sens var(--duree-entree) var(--ressort-standard) var(--delai-entree) both; }
::view-transition-old(*.ei-sur-place) { animation: ei-sortie var(--duree-sortie) var(--sortie) both; }
::view-transition-new(*.ei-sur-place) { animation: ei-entree var(--duree-entree) var(--ressort-standard) var(--delai-entree) both; }

/* La même page, rouverte (erreur de saisie, code renvoyé, action sur une fiche : ei-vt-meme-page,
   posée par mouvement.js) : un remplacement ne rejoue ni sa sortie ni son entrée, il change d'un
   coup ; seul ce qui a bougé glisse (retour à l'élément). */
:root.ei-vt-meme-page::view-transition-old(*.ei-remplace),
:root.ei-vt-meme-page::view-transition-old(*.ei-sur-place),
:root.ei-vt-meme-page::view-transition-old(*.ei-scene) { animation: none; opacity: 0; }
:root.ei-vt-meme-page::view-transition-new(*.ei-remplace),
:root.ei-vt-meme-page::view-transition-new(*.ei-sur-place),
:root.ei-vt-meme-page::view-transition-new(*.ei-scene) { animation: none; }

/* ---------- Élément partagé ---------- */

/* Le même élément, nommé dans ses deux états (une ligne, puis sa fiche) : data-ei-partage="nom"
   dans le Blade, sur l'un et l'autre ; mouvement.js pose le nom par le CSSOM sur l'élément cliqué
   et sur son pendant, puis le retire. Une seule image pendant le trajet, la plus grande (celle de
   la fiche : la nouvelle à l'aller, l'ancienne au retour), mise à l'échelle : ni double image, ni
   petite image agrandie et floue. D'où la règle : mêmes proportions dans les deux états (une photo
   garde son rapport, un texte garde sa taille ; sinon on ne le partage pas). Trajet sur le ressort
   ample (400 ms) à l'aller, standard (280 ms) au retour (ei-vt-retour). Coins arrondis portés par
   la paire d'images : data-ei-partage-classe="ei-arrondi" (rayon md) ou "ei-rond". */
::view-transition-group(*.ei-partage) {
    animation-duration: var(--duree-partage);
    animation-timing-function: var(--ressort-ample);
}
:root.ei-vt-retour::view-transition-group(*.ei-partage) {
    animation-duration: var(--duree-etat);
    animation-timing-function: var(--ressort-standard);
}
::view-transition-old(*.ei-partage), ::view-transition-new(*.ei-partage) { animation: none; height: 100%; object-fit: cover; }
:root:not(.ei-vt-retour)::view-transition-old(*.ei-partage),
:root.ei-vt-retour::view-transition-new(*.ei-partage) { opacity: 0; }
/* Sans pendant sur l'autre page (hors de l'écran, ou absent) : il sort, ou entre, comme la scène. */
::view-transition-old(*.ei-partage):only-child { opacity: 1; animation: ei-sortie var(--duree-sortie) var(--sortie) both; }
::view-transition-new(*.ei-partage):only-child { opacity: 1; animation: ei-entree var(--duree-entree) var(--ressort-standard) 120ms both; }
::view-transition-image-pair(*.ei-partage) { overflow: clip; }
::view-transition-image-pair(*.ei-arrondi) { border-radius: var(--rayon-md); }
::view-transition-image-pair(*.ei-rond) { border-radius: 50%; }

/* Le reste de la scène (<main>, nommé ei-contenu) pendant qu'un élément voyage : il sort en 90 ms,
   le nouveau contenu entre à 120 ms. ei-deplace : un élément qui change de place sans changer
   (une ligne voisine) : une seule image, la nouvelle, sur le ressort standard. */
::view-transition-group(*.ei-scene) { animation-duration: var(--duree-etat); }
::view-transition-old(*.ei-scene) { animation: ei-sortie var(--duree-sortie) var(--sortie) both; }
::view-transition-new(*.ei-scene) { animation: ei-entree var(--duree-entree) var(--ressort-standard) 120ms both; }
::view-transition-group(*.ei-deplace) {
    animation-duration: var(--duree-etat);
    animation-timing-function: var(--ressort-standard);
}
::view-transition-old(*.ei-deplace) { animation: none; opacity: 0; }
::view-transition-new(*.ei-deplace) { animation: none; }
::view-transition-old(*.ei-deplace):only-child { opacity: 1; animation: ei-sortie var(--duree-sortie) var(--sortie) both; }
::view-transition-new(*.ei-deplace):only-child { animation: ei-entree var(--duree-entree) var(--ressort-standard) var(--delai-entree) both; }

/* ---------- Squelette qui devient le contenu ---------- */

/* Des blocs fixes en surface-creuse, à la géométrie exacte du contenu (un rond pour un avatar rond,
   des lignes à la hauteur du texte), sans reflet qui boucle : il apparaît en fondu, puis le
   contenu le remplace. <template id="…"> cloné par mouvement.js après 300 ms d'attente. */
.ei-squelette { display: block; border-radius: var(--rayon-xs); background: var(--surface-creuse); animation: ei-fondu-entree var(--duree-entree) ease-out both; }
.ei-squelette--rond { border-radius: 50%; }
.ei-squelette--ligne { height: 0.75em; margin-block: 0.4em; }

/* ---------- Retour à l'élément après une action ---------- */

/* L'élément d'où est parti le formulaire, retrouvé à sa place sur la page rouverte : une teinte
   derrière son contenu, qui s'estompe en 400 ms une fois les voisins en place (fixe en mouvement
   réduit, retirée au premier geste). Seule l'opacité s'anime. */
.ei-repere { position: relative; isolation: isolate; }
.ei-repere::after {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--espace-1));
    z-index: -1;
    border-radius: var(--rayon-md);
    background: var(--bleu-doux);
    pointer-events: none;
    animation: ei-repere 400ms ease-out var(--duree-etat) both;
}
@keyframes ei-repere { from { opacity: 1; } to { opacity: 0; } }

@keyframes ei-sortie { to { opacity: 0; filter: blur(var(--flou-contenu)); } }
@keyframes ei-entree { from { opacity: 0; transform: translateY(calc(var(--distance-contenu) / 2)); filter: blur(var(--flou-contenu)); } }
@keyframes ei-sortie-sens { to { opacity: 0; transform: translateX(calc(var(--ei-sens) * -1 * var(--distance-contenu))); filter: blur(var(--flou-contenu)); } }
@keyframes ei-entree-sens { from { opacity: 0; transform: translateX(calc(var(--ei-sens) * var(--distance-contenu))); filter: blur(var(--flou-contenu)); } }
@keyframes ei-fondu-sortie { to { opacity: 0; } }
@keyframes ei-fondu-entree { from { opacity: 0; } }

/* ---------- Mouvement réduit ---------- */

/* Réglage du système (prefers-reduced-motion), reflété par la classe ei-mouvement-reduit sur <html>
   (mouvement.js), que le JavaScript lit aussi. Ni translation, ni échelle, ni flou, ni entrée
   décalée, ni défilement animé ; les couleurs gardent un fondu court ; le focus n'est jamais animé. */
@media (prefers-reduced-motion: reduce) {
    :root { --flou-contenu: 0px; --distance-contenu: 0px; --echelle-appui: 1; --decalage-liste: 0ms; }
    .ei-btn[data-etat="attente"] .ei-btn__attente .ei-spinner { animation-duration: 2.4s; }
    .ei-repere::after { animation: none; }
    .ei-menu > .ei-menu__panneau, .ei-menu:not([open]) > .ei-menu__panneau { transform: none; }
    /* View Transitions : les groupes sautent à leur place, les images passent en fondu de 120 ms,
       l'ancienne puis la nouvelle (60 ms chacune) : jamais deux textes lisibles ensemble. */
    ::view-transition-group(*) { animation-duration: 0s !important; }
    ::view-transition-old(*) { animation: ei-fondu-sortie 60ms linear both !important; }
    ::view-transition-new(*) { animation: ei-fondu-entree 60ms linear 60ms both !important; }
}
:root.ei-mouvement-reduit { --flou-contenu: 0px; --distance-contenu: 0px; --echelle-appui: 1; --decalage-liste: 0ms; }
:root.ei-mouvement-reduit .ei-btn[data-etat="attente"] .ei-btn__attente .ei-spinner { animation-duration: 2.4s; }
:root.ei-mouvement-reduit .ei-repere::after { animation: none; }
:root.ei-mouvement-reduit .ei-menu > .ei-menu__panneau, :root.ei-mouvement-reduit .ei-menu:not([open]) > .ei-menu__panneau { transform: none; }
:root.ei-mouvement-reduit::view-transition-group(*) { animation-duration: 0s !important; }
:root.ei-mouvement-reduit::view-transition-old(*) { animation: ei-fondu-sortie 60ms linear both !important; }
:root.ei-mouvement-reduit::view-transition-new(*) { animation: ei-fondu-entree 60ms linear 60ms both !important; }
