/*#########################################################################
Champ « AT Button » — habillage par défaut, NEUTRE et autonome.
##########################################################################

Cette feuille n'existe que pour le cas « attps-get-field seul ». Quand
attps-builder est présent, il remplace le registre de presets par le sien
(classes `atb-btn-*`) et coupe ce fichier via le filtre
`atgf/button/enqueue_css` : c'est alors SON CSS qui peint les boutons.

Rien n'est codé en dur : chaque couleur passe par une variable `--at-*`
redéfinissable, avec un repli utilisable tel quel.

Structure : `.at-btn` (base) + une classe par axe — couleur (`-primary`…),
type (`-normal|soft|outline|ghost`), taille (`-xs`…`-xl`), arrondi
(`-none`…`-full`), survol (`-hover-*`).
*/

.at-btn {
    /* Palette de repli — surchargeable au niveau du thème. */
    --at-primary: #2f6fed;
    --at-secondary: #5b6472;
    --at-accent: #f0a202;
    --at-neutral: #3d4451;
    --at-info: #2b9fd6;
    --at-warning: #e2a03f;
    --at-danger: #d64545;
    --at-success: #3aa06a;
    --at-light: #ffffff;

    --at-btn-current: var(--at-primary);
    --at-btn-bg: var(--at-btn-current);
    --at-btn-text: var(--at-light);

    /* Géométrie — TOUT est en variable, y compris ce que les classes de taille
       règlent. C'est ce qui permet à un projet hôte (attps-builder) de plaquer
       sa charte en ne fournissant que des VALEURS, sans jamais redéfinir un
       sélecteur ni inventer un second vocabulaire de classes. */
    /* Défaut d'arrondi via un jeton dédié, JAMAIS via `--at-btn-radius` : les
       classes `.at-btn-radius-*` posent cette dernière, et une feuille hôte
       chargée après (le pont du builder) l'écraserait à spécificité égale — le
       réglage d'arrondi ne servirait plus à rien. Même raison que pour les
       jetons de taille. */
    --at-btn-radius: var(--at-btn-radius-default, 0.375rem);
    --at-btn-border: 2px;

    /* Couleur de bordure : la couleur COURANTE du bouton. Au repos elle
       coïncide avec le fond des types pleins (bordure invisible, l'aplat suffit)
       et au survol, où le fond s'éclaircit et le texte prend cette couleur, le
       cerclage apparaît de lui-même. Jeton à part entière : un hôte peut
       renverser la règle sans toucher au sélecteur. */
    --at-btn-border-color: var(--at-btn-current);
    --at-btn-gap: 0.5em;

    /* La base reprend les jetons de la taille `md`. C'est ce qui permet à un
       hôte de ne redéfinir QUE des jetons : s'il posait `--at-btn-padding-y`
       ici, sa feuille étant chargée après, il écraserait aussi .at-btn-lg et
       consorts — même spécificité, source postérieure. */
    --at-btn-padding-y: var(--at-btn-size-md-py, 0.625em);
    --at-btn-padding-x: var(--at-btn-size-md-px, 1.25em);
    --at-btn-font-size: var(--at-btn-size-md-font, 1rem);
    --at-btn-font-weight: 600;
    --at-btn-leading: 1.2;
    --at-btn-duration: 0.25s;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--at-btn-gap);
    padding: var(--at-btn-padding-y) var(--at-btn-padding-x);
    border: var(--at-btn-border) solid var(--at-btn-border-color);
    border-radius: var(--at-btn-radius);
    background-color: var(--at-btn-bg);
    color: var(--at-btn-text);
    font-size: var(--at-btn-font-size);
    font-weight: var(--at-btn-font-weight);
    line-height: var(--at-btn-leading);
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--at-btn-duration), color var(--at-btn-duration),
        border-color var(--at-btn-duration), transform var(--at-btn-duration);
}

.at-btn__text {
    display: inline-block;
}

.at-btn__icon {
    display: inline-flex;
    align-items: center;
    /* currentColor : les SVG inline et les dashicons suivent la couleur du texte. */
    color: currentColor;
}

.at-btn__icon svg,
.at-btn__icon img {
    display: block;
    width: 1.2em;
    height: 1.2em;
}

/* Le glyphe d'une icône de fonte est posé par un `::before` : c'est du TEXTE
   dans une boîte de 1,2 em, et sa chasse lui est propre — `fa-plus` est bien
   plus étroit que `fa-arrow-right`. Aligné au bord gauche par défaut, il
   paraissait décalé, ce qui se voit surtout sur une forme ronde où l'on attend
   un centrage parfait. Le conteneur devient donc une boîte flex qui centre son
   glyphe dans les deux axes, quelle que soit sa chasse. */
.at-btn__icon .dashicons,
.at-btn__icon i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.2em;
    height: 1.2em;
    font-size: 1.2em;
    line-height: 1;
}

/* SVG inline : hériter de la couleur du bouton.
   `fill: currentColor` sur le parent ne suffit pas — un SVG exporté porte
   presque toujours un attribut `fill` en dur sur la balise racine ET sur ses
   enfants, qui gagne. On force donc les deux niveaux, en épargnant les
   `fill="none"` (contours) qu'il ne faut surtout pas remplir. */
.at-btn__icon svg[fill]:not([fill="none"]),
.at-btn__icon svg *[fill]:not([fill="none"]) {
    fill: currentColor;
}

.at-btn__icon svg[stroke]:not([stroke="none"]),
.at-btn__icon svg *[stroke]:not([stroke="none"]) {
    stroke: currentColor;
}

/* --------------------------------------------------------------------- */
/* Couleurs                                                              */
/* --------------------------------------------------------------------- */

.at-btn-primary {
    --at-btn-current: var(--at-primary);
}

.at-btn-secondary {
    --at-btn-current: var(--at-secondary);
}

.at-btn-accent {
    --at-btn-current: var(--at-accent);
}

.at-btn-neutral {
    --at-btn-current: var(--at-neutral);
}

.at-btn-info {
    --at-btn-current: var(--at-info);
}

.at-btn-warning {
    --at-btn-current: var(--at-warning);
}

.at-btn-danger {
    --at-btn-current: var(--at-danger);
}

.at-btn-success {
    --at-btn-current: var(--at-success);
}

/* --------------------------------------------------------------------- */
/* Types                                                                 */
/* --------------------------------------------------------------------- */

.at-btn-normal {
    --at-btn-bg: var(--at-btn-current);
    --at-btn-text: var(--at-light);
}

/* `soft` garde une bordure de la couleur de son fond pâle : c'est un aplat
   doux, pas un bouton cerclé. */
.at-btn-soft {
    --at-btn-bg: color-mix(in oklab, var(--at-btn-current) 15%, var(--at-light));
    --at-btn-text: var(--at-btn-current);
    --at-btn-border-color: var(--at-btn-bg);
}

.at-btn-outline {
    --at-btn-bg: transparent;
    --at-btn-text: var(--at-btn-current);
    --at-btn-border-color: var(--at-btn-current);
}

/* `ghost` n'a par définition aucune bordure. */
.at-btn-ghost {
    --at-btn-bg: transparent;
    --at-btn-text: var(--at-btn-current);
    --at-btn-border-color: transparent;
}

/* --------------------------------------------------------------------- */
/* Tailles                                                               */
/* --------------------------------------------------------------------- */

/* Chaque taille ne fait que POSER des valeurs : un hôte redéfinit
   `--at-btn-size-lg-*` et récupère sa propre échelle, classes inchangées. */

.at-btn-xs {
    --at-btn-font-size: var(--at-btn-size-xs-font, 0.75rem);
    --at-btn-padding-y: var(--at-btn-size-xs-py, 0.4em);
    --at-btn-padding-x: var(--at-btn-size-xs-px, 0.9em);
}

.at-btn-sm {
    --at-btn-font-size: var(--at-btn-size-sm-font, 0.875rem);
    --at-btn-padding-y: var(--at-btn-size-sm-py, 0.5em);
    --at-btn-padding-x: var(--at-btn-size-sm-px, 1.1em);
}

.at-btn-md {
    --at-btn-font-size: var(--at-btn-size-md-font, 1rem);
    --at-btn-padding-y: var(--at-btn-size-md-py, 0.625em);
    --at-btn-padding-x: var(--at-btn-size-md-px, 1.25em);
}

.at-btn-lg {
    --at-btn-font-size: var(--at-btn-size-lg-font, 1.125rem);
    --at-btn-padding-y: var(--at-btn-size-lg-py, 0.7em);
    --at-btn-padding-x: var(--at-btn-size-lg-px, 1.5em);
}

.at-btn-xl {
    --at-btn-font-size: var(--at-btn-size-xl-font, 1.25rem);
    --at-btn-padding-y: var(--at-btn-size-xl-py, 0.8em);
    --at-btn-padding-x: var(--at-btn-size-xl-px, 1.75em);
}

/* --------------------------------------------------------------------- */
/* Arrondis                                                              */
/* --------------------------------------------------------------------- */

/* Namespacés (`-radius-`) : sans ça, `radius=sm` produirait `.at-btn-sm`,
   qui est la classe de TAILLE. Cf. le préfixe d'axe dans Button_Options. */
.at-btn-radius-none {
    --at-btn-radius: 0;
}

.at-btn-radius-sm {
    --at-btn-radius: var(--at-btn-radius-scale-sm, 0.25rem);
}

.at-btn-radius-md {
    --at-btn-radius: var(--at-btn-radius-scale-md, 0.375rem);
}

.at-btn-radius-lg {
    --at-btn-radius: var(--at-btn-radius-scale-lg, 0.5rem);
}

.at-btn-radius-full {
    --at-btn-radius: 100vw;
}

/* --------------------------------------------------------------------- */
/* Forme                                                                 */
/*                                                                       */
/* Déclarées APRÈS les arrondis : le cercle doit pouvoir imposer le sien  */
/* quel que soit l'arrondi choisi par ailleurs.                          */
/* --------------------------------------------------------------------- */

/* La largeur reste celle du contenu, la hauteur s'y égale. Un bouton à icône
   seule devient donc une pastille, et un bouton à libellé un grand disque —
   c'est la définition même d'une forme carrée ou ronde, pas un défaut.

   Le padding repasse à une valeur unique : l'écart horizontal du bouton normal
   (deux fois le vertical) déséquilibrerait la forme, le contenu se retrouvant
   trop haut dans le cercle. */
.at-btn:where(.at-btn-shape-square, .at-btn-shape-circle) {
    aspect-ratio: 1;
    padding: var(--at-btn-padding-y);
}

/* Spécificité 0,2,0 : l'emporte sur les classes d'arrondi (0,1,0), quel que
   soit l'ordre des fichiers. Une forme ronde n'est pas négociable. */
.at-btn.at-btn-shape-circle {
    --at-btn-radius: 50%;
}

/* --------------------------------------------------------------------- */
/* Bordure                                                               */
/*                                                                       */
/* Sa COULEUR suit le texte (cf. --at-btn-border-color plus haut) ; ces   */
/* classes n'en règlent que l'épaisseur.                                  */
/* --------------------------------------------------------------------- */

.at-btn-border-none {
    --at-btn-border: 0;
}

.at-btn-border-thin {
    --at-btn-border: 1px;
}

.at-btn-border-normal {
    --at-btn-border: 2px;
}

.at-btn-border-thick {
    --at-btn-border: 3px;
}

/* Utilitaire hors axes : un bouton qui occupe toute la largeur. */
.at-btn-full {
    width: 100%;
}

/* --------------------------------------------------------------------- */
/* Survol                                                                */
/*                                                                       */
/* Le défaut (aucune classe `-hover-*`) EST le comportement « normal ».   */
/* Un effet géométrique le remplace ; la garde :where() reproduit le      */
/* contrat déjà en place côté attps-builder.                             */
/* --------------------------------------------------------------------- */

/* ⚠️ PIÈGE À NE PAS REFAIRE : ne JAMAIS réécrire ici les variables qui
   alimentent background-color / color. Dans un même bloc, `--at-btn-bg:
   var(--at-btn-text)` résout sur la NOUVELLE valeur de `--at-btn-text` déclarée
   juste après — fond et texte finissaient identiques, bouton illisible. On pose
   donc directement les propriétés finales. */

.at-btn:hover:where(:not([class*="at-btn-hover-"]), .at-btn-hover-normal) {
    transform: scale(1.02);
}

/* Types pleins (normal, ou aucun type) : le fond s'éclaircit, le texte prend
   la couleur. */
.at-btn:where(:not(.at-btn-soft):not(.at-btn-outline):not(.at-btn-ghost)):hover:where(:not([class*="at-btn-hover-"]), .at-btn-hover-normal) {
    background-color: var(--at-btn-text);
    color: var(--at-btn-current);
    border-color: var(--at-btn-current);
}

/* Types clairs ou transparents : le fond se remplit de la couleur, le texte
   passe en clair. */
.at-btn:where(.at-btn-soft, .at-btn-outline, .at-btn-ghost):hover:where(:not([class*="at-btn-hover-"]), .at-btn-hover-normal) {
    background-color: var(--at-btn-current);
    color: var(--at-light);
}

/* `ghost` n'a pas de bordure : on ne lui en fait pas apparaître une au survol. */
.at-btn:where(.at-btn-soft, .at-btn-outline):hover:where(:not([class*="at-btn-hover-"]), .at-btn-hover-normal) {
    border-color: var(--at-btn-current);
}

/* Socle commun des effets.

   La garde est enfermée dans `:where()`, dont la spécificité est NULLE : ces
   règles pèsent donc autant que `.at-btn` seul, et un effet peut les surcharger
   avec sa simple classe `.at-btn-hover-…` déclarée plus bas. Sans ça la garde
   valait 0,3,0 et écrasait silencieusement les effets — trois symptômes déjà
   rencontrés : calque peint en permanence malgré un `content: none`, couche
   décorative repassée dans le flux, débord latéral ignoré. */
.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal)) {
    position: relative;
    overflow: hidden;


    --at-btn-fx-color: var(--at-btn-text);
    --at-btn-fx-text: var(--at-btn-current);
    --at-btn-fx-duration: 0.35s;
    --at-btn-fx-ease: cubic-bezier(0.7, 0, 0.2, 1);
}

/* Types clairs ou transparents : c'est l'overlay qui apporte la couleur. */
.at-btn:where(.at-btn-soft, .at-btn-outline, .at-btn-ghost):where([class*="at-btn-hover-"]:not(.at-btn-hover-normal)) {
    --at-btn-fx-color: var(--at-btn-current);
    --at-btn-fx-text: var(--at-light);
}

/* La bordure NE suit PAS le calque : elle garde la couleur courante du bouton,
   au repos comme au survol. Deux essais ont montré pourquoi :
   la faire fondre vers la couleur du calque la fait purement disparaître (le
   bouton n'a plus aucun contour au survol) ; la laisser telle quelle donne le
   cerclage attendu. `overflow: hidden` découpant à la boîte de padding, le
   calque ne peut de toute façon jamais la recouvrir — c'était déjà le
   comportement de buttons-hover.css. */

/* `soft` fait exception : sa bordure a la couleur de son fond PÂLE, qui n'a
   plus de sens une fois l'aplat coloré posé. Elle suit donc le calque. */
/* Par défaut elle suit le calque, mais via un jeton dédié : un effet qui ne
   peint AUCUN aplat (atlas) n'a aucune raison de faire changer la bordure, et
   le ramène simplement à sa couleur de repos. */
.at-btn.at-btn-soft:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal)):hover {
    border-color: var(--at-btn-fx-border, var(--at-btn-fx-color));
}

/* Contenu du bouton au-dessus du calque. `at-btn__layer` en est exclu : c'est
   la marque des couches décoratives ajoutées par un effet (bande défilante…),
   qui se positionnent elles-mêmes. Sans cette exception, ce sélecteur — plus
   spécifique que la classe de la couche — leur reposerait `position: relative`
   et les ferait rentrer dans le flux, étirant le bouton. */
.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal))>*:not(.at-btn__layer),
.at-btn__group>* {
    position: relative;
    z-index: 1;
}

.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal))::before {
    content: '';
    position: absolute;
    /* Le calque dépasse de 2px SOUS la zone découpée. Son arête basse coïncide
       sinon exactement avec celle de `overflow: hidden`, et les deux ne
       s'arrondissent pas au même sous-pixel : il restait un liseré du fond
       visible en bas une fois l'animation terminée (le même arrondi qui
       imposait un rattrapage en pixels du temps du translate). Ce dépassement
       est hors champ : il ne se voit jamais, y compris à l'échelle 0.

       Le débord vaut pour les QUATRE côtés, et pas seulement le bas : sur un
       bouton arrondi SANS bordure, l'arête du calque et celle de la découpe
       tombent au même endroit, et l'addition de leurs deux adoucissements
       laisse un liseré de la couleur du dessous le long de l'arrondi. En
       débordant, le calque couvre pleinement la zone découpée : il ne reste
       qu'un seul bord adouci, celui de la découpe. Une bordure, elle, masquait
       le défaut en peignant par-dessus.

       Le débord LATÉRAL passe par un jeton : un effet qui déforme le calque
       (biais, pointe) doit pouvoir l'élargir bien davantage. Il ne peut pas le
       faire en redéclarant `inset` — ce sélecteur est plus spécifique que sa
       propre classe et gagnerait. */
    inset: -2px var(--at-btn-fx-bleed, -2px) -2px;
    z-index: 0;
    background-color: var(--at-btn-fx-color);
    pointer-events: none;
}

.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal)):hover {
    color: var(--at-btn-fx-text);
}

/* ---- Liseré d'arrondi ------------------------------------------------
   Sur un bouton arrondi SANS bordure, il restait un liseré de la couleur du
   fond le long de la courbe : le fond du bouton et le calque qui le recouvre
   ont chacun leur bord adouci au même endroit, et il subsiste toujours une
   fraction du fond en dessous (mesuré : un pixel à 55 d'écart de bleu). Une
   bordure masquait le défaut en peignant par-dessus — d'où son apparition dans
   ce seul cas.

   Ni un débord du calque, ni `isolation: isolate`, ni `will-change` n'y
   changent quoi que ce soit (mesurés à 55). La seule issue est de supprimer le
   CONTRASTE : une fois le calque en place, le fond du bouton prend sa couleur,
   et il n'y a plus rien à laisser filtrer (mesuré à 0).

   Le passage se fait par une ANIMATION plutôt que par une transition : il faut
   qu'il survienne une fois la couverture terminée (d'où le retard), mais qu'il
   soit défait IMMÉDIATEMENT au départ du survol — tant que le calque couvre
   encore. Une transition retardée le serait dans les deux sens et ferait
   apparaître la couleur du calque pendant que celui-ci se retire. Une animation
   liée à `:hover` disparaît, elle, avec l'état. Et elle n'interfère avec aucune
   des transitions réglées finement par chaque effet.

   Réservé aux effets qui posent réellement un aplat : ceux qui n'en posent pas
   (atlas, pandora) verraient leur fond changer sans raison. */
.at-btn:where(.at-btn-hover-pan,
    .at-btn-hover-hyperion,
    .at-btn-hover-mimas,
    .at-btn-hover-telesto,
    .at-btn-hover-calypso,
    .at-btn-hover-bestia,
    .at-btn-hover-pallene):hover {
    animation: at-btn-fx-settle 0.01s linear forwards var(--at-btn-fx-settle-delay, var(--at-btn-fx-duration));
}

@keyframes at-btn-fx-settle {
    to {
        background-color: var(--at-btn-fx-color);
    }
}

/* ---- Contenu d'un seul tenant ---------------------------------------
   Les effets qui font sortir puis revenir le contenu (hyperion, telesto) le
   déplacent D'UN BLOC : sans cette enveloppe, chaque partie traversait son
   propre masque, donc parcourait sa propre largeur — verticalement cela ne se
   voit pas (icônes et libellé ont la même hauteur), mais horizontalement le
   libellé filait trois fois plus vite que ses icônes.

   La cascade, elle, suppose exactement l'inverse : des parties autonomes qui
   partent l'une après l'autre. L'enveloppe s'efface donc (`display: contents`)
   dès qu'une cascade est demandée, et les parties retrouvent leurs masques
   individuels. Les deux modes sont ainsi complémentaires plutôt que
   concurrents. */

.at-btn__group {
    display: inline-flex;
    align-items: center;
    gap: var(--at-btn-gap);
}

.at-btn:where([class*="at-btn-stagger-"])>.at-btn__group {
    display: contents;
}

/* Distance de sortie, exprimée en part de l'élément qui bouge.

   En mode GROUPE, le masque est le bouton entier : le contenu doit franchir en
   plus le padding, soit environ 1,7 fois sa propre taille. La valeur tient pour
   toutes les tailles, padding et interligne étant tous deux en `em` — leur
   rapport ne bouge donc pas. Sur l'axe horizontal, un libellé long a besoin de
   moins que ça (le padding y pèse relativement moins) : dépasser est sans
   conséquence, le trajet supplémentaire se joue hors champ.

   En mode CASCADE, chaque partie a son propre masque, serré autour d'elle :
   105 % suffisent, et davantage la ferait sortir trop loin pour rien. */
.at-btn:where(.at-btn-hover-hyperion, .at-btn-hover-telesto, .at-btn-hover-calypso, .at-btn-hover-skoll, .at-btn-hover-greip, .at-btn-hover-narvi) {
    --at-btn-fx-travel: 170%;
}

.at-btn:where([class*="at-btn-stagger-"]) {
    --at-btn-fx-travel: 105%;
}

/* ---- Cascade (axe `stagger`) ----------------------------------------
   Pour les effets qui animent les parties du bouton séparément. Le rang est
   pris sur la POSITION RÉELLE, pas sur le rôle : le libellé est premier quand
   il n'y a pas d'icône à gauche, et la cascade démarre alors bien à zéro.

   `--at-btn-fx-stagger` n'a volontairement PAS de valeur par défaut ici : elle
   est donnée en repli à chaque lecture (`var(…, 0s)`). La poser sur ce sélecteur
   — plus spécifique que les classes de l'axe — l'aurait rendue impossible à
   surcharger. */

.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal))>:nth-child(1),
.at-btn__group>:nth-child(1) {
    --at-btn-fx-delay: 0s;
}

.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal))>:nth-child(2),
.at-btn__group>:nth-child(2) {
    --at-btn-fx-delay: var(--at-btn-fx-stagger, 0s);
}

.at-btn:where([class*="at-btn-hover-"]:not(.at-btn-hover-normal))>:nth-child(3),
.at-btn__group>:nth-child(3) {
    --at-btn-fx-delay: calc(var(--at-btn-fx-stagger, 0s) * 2);
}

.at-btn-stagger-sm {
    --at-btn-fx-stagger: 0.04s;
}

.at-btn-stagger-md {
    --at-btn-fx-stagger: 0.07s;
}

.at-btn-stagger-lg {
    --at-btn-fx-stagger: 0.12s;
}

/* pan — l'overlay monte depuis le bas et remplit le bouton. */
/* `scaleY` plutôt qu'un `translateY(100%)` : pour un aplat de couleur, une
   hauteur qui croît depuis le bas est visuellement identique à un bloc qui
   remonte — mais un élément mis à l'échelle 0 n'a plus de surface, il ne peut
   donc pas déborder. Le translate laissait passer un liseré d'un sous-pixel en
   bas, la hauteur du bouton étant fractionnaire (padding en `em`) : 100 % d'une
   hauteur fractionnaire et le rectangle de découpe ne s'arrondissent pas
   pareil. D'où le rattrapage en pixels, désormais inutile. */
.at-btn-hover-pan::before {
    transform: scaleY(0);
    transform-origin: bottom;
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* Léger dépassement volontaire (1.08 et non 1).
   La courbe est très lente en fin de course : les derniers pour-cent du
   remplissage s'étirent, laissant un mince liseré de fond encore coloré en
   haut. Collé à la bordure de la même couleur, il se lit comme une bordure
   épaisse — puis disparaît d'un coup, d'où l'impression que le cerclage
   « change de taille » à la fin. En dépassant de 8 %, le calque a fini de
   couvrir bien avant la fin de la transition : le reste du mouvement se joue
   hors champ, découpé par `overflow: hidden`. La bordure, elle, n'est jamais
   recouverte. */
.at-btn-hover-pan:hover::before {
    transform: scaleY(1.08);
}

/* hyperion — le calque balaie horizontalement pendant que le libellé fait un
   tour vertical : il sort par le haut, et sa suite revient par le bas.

   L'original (Codrops) fait DISPARAÎTRE un aplat noir en comptant sur
   `mix-blend-mode: difference` pour recolorer le texte pixel par pixel. Ce blend
   est incompatible avec une palette (il impose noir/blanc), donc on garde notre
   convention — le calque ARRIVE au survol — et on résout la lisibilité par le
   timing : la couleur du texte bascule d'un coup à l'instant précis où le
   libellé est hors champ, entre les deux keyframes. Il n'est donc jamais peint
   de la mauvaise couleur.

   L'origine de l'échelle change entre repos et survol : à l'aller le calque
   grandit depuis la gauche, au retour il se rétracte vers la droite. Les deux
   mouvements vont dans le même sens, comme une bande qui traverse. */

.at-btn-hover-hyperion {
    --at-btn-fx-duration: 0.3s;

    /* Durée du demi-tour du libellé. */
    --at-btn-fx-roll: 0.2s;

    /* La couleur bascule quand le LIBELLÉ est hors champ — c'est lui qu'il ne
       faut jamais peindre de la mauvaise couleur. Sans cascade, tout le contenu
       est hors champ au même instant et la bascule est invisible. */
    --at-btn-fx-text-delay: var(--at-btn-fx-roll);

    /* Le `color` passe en bascule sèche (0s) retardée, au lieu du fondu de la
       base. Les autres propriétés gardent le réglage d'origine — la liste est
       reprise telle quelle, une transition abrégée réinitialise le reste. */
    transition: background-color var(--at-btn-duration), color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration), transform var(--at-btn-duration);
}

.at-btn-hover-hyperion::before {
    transform: scaleX(0);
    transform-origin: 100% 50%;
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* Même dépassement volontaire que `pan`, pour la même raison : la fin de courbe
   est très lente, et le dernier pour-cent laisserait un liseré de fond visible
   sur le bord. Le débord est découpé par `overflow: hidden`. */
.at-btn-hover-hyperion:hover::before {
    transform: scaleX(1.06);
    transform-origin: 0% 50%;
}

/* Le masque, c'est le span de texte ; l'élément qui bouge, son enveloppe
   interne (posée par le template quand l'effet la réclame). Le padding-bas
   rattrapé en marge négative laisse la place aux jambages (g, p, j) sans
   changer la mise en page : sans lui, `overflow: hidden` les couperait. */
/* Enfants DIRECTS uniquement : une couche décorative (la bande d'atlas) porte
   les mêmes classes de contenu, un sélecteur descendant irait masquer et animer
   ses icônes aussi. */
/* Masques partagés par les effets dont le contenu sort du champ et revient
   (hyperion, telesto). Le padding bas rattrapé en marge négative laisse la place
   aux jambages (g, p, j) sans changer la mise en page : sans lui,
   `overflow: hidden` les couperait. */
.at-btn:where(.at-btn-hover-hyperion, .at-btn-hover-telesto, .at-btn-hover-calypso, .at-btn-hover-skoll, .at-btn-hover-greip, .at-btn-hover-narvi) .at-btn__group>.at-btn__text {
    overflow: hidden;
    padding-bottom: 0.15em;
    margin-bottom: -0.15em;
}

.at-btn:where(.at-btn-hover-hyperion, .at-btn-hover-telesto, .at-btn-hover-calypso, .at-btn-hover-skoll, .at-btn-hover-greip, .at-btn-hover-narvi) .at-btn__text-inner {
    display: inline-block;
}

/* Les icônes roulent avec le libellé : tout le contenu du bouton part et
   revient d'un bloc. Aucun markup en plus — l'enveloppe `at-btn__icon` fait le
   masque et son enfant (i / svg / img / dashicon) est ce qui bouge. Étant
   enfant d'un `inline-flex`, il est blockifié : `transform` s'y applique quelle
   que soit sa nature.

   Bénéfice secondaire : les icônes sont hors champ au moment où la couleur
   bascule, elles ne peuvent donc plus la changer à découvert pendant que le
   calque n'a pas fini de balayer. */
.at-btn:where(.at-btn-hover-hyperion, .at-btn-hover-telesto, .at-btn-hover-calypso, .at-btn-hover-skoll, .at-btn-hover-greip, .at-btn-hover-narvi) .at-btn__group>.at-btn__icon {
    overflow: hidden;
}

/* Avec une cascade, le libellé ne part plus à l'instant zéro : il attend son
   rang, décalé d'un cran s'il y a une icône avant lui. La bascule de couleur
   suit ce rang réel. Sans cascade, `var(…, 0s)` annule le calcul et les deux
   règles disent la même chose. */
.at-btn-hover-hyperion:has(>.at-btn__icon--left) {
    --at-btn-fx-text-delay: calc(var(--at-btn-fx-roll) + var(--at-btn-fx-stagger, 0s));
}

/* Par défaut c'est l'ENVELOPPE qui bouge, donc tout le contenu ensemble, à une
   seule vitesse. En cascade, ce sont les parties, chacune dans son masque —
   l'enveloppe étant alors en `display: contents`, elle n'a plus de boîte et la
   première règle devient sans effet. */
.at-btn-hover-hyperion:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-hyperion:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-hyperion:hover .at-btn__icon>* {
    animation: at-btn-roll-out var(--at-btn-fx-roll) forwards var(--at-btn-fx-delay, 0s),
        at-btn-roll-in var(--at-btn-fx-roll) forwards calc(var(--at-btn-fx-roll) + var(--at-btn-fx-delay, 0s));
}

@keyframes at-btn-roll-out {
    to {
        transform: translate3d(0, calc(var(--at-btn-fx-travel, 105%) * -1), 0);
    }
}

@keyframes at-btn-roll-in {
    from {
        transform: translate3d(0, var(--at-btn-fx-travel, 105%), 0);
    }

    to {
        transform: translate3d(0, 0, 0);
    }
}

/* mimas — un bandeau en biais traverse le bouton de la gauche vers la droite.

   L'original fait SORTIR un aplat noir par la droite, en comptant là encore sur
   `mix-blend-mode: difference` pour le texte. Même parti pris que pour hyperion :
   on garde notre convention (le calque ARRIVE au survol) et la couleur du texte
   bascule quand le balayage a déjà couvert.

   Le décalage du parallélogramme vaut hauteur × tan(30°), soit 0,29 fois la
   hauteur. Un débord fixe en `em` suffisait tant que la hauteur d'un bouton
   restait de l'ordre de 2,5 em — mais une FORME imposée la porte à la largeur,
   et le coin bas du calque revenait alors mordre dans le bouton au repos.
   Un débord d'une demi-largeur borne le problème dans tous les cas : tant que
   le bouton est au moins aussi large que haut, 0,29 × hauteur ≤ 0,5 × largeur. */

.at-btn-hover-mimas {
    /* Courbe ADOUCIE par rapport à l'original. La sienne,
       `cubic-bezier(0.3, 1, 0.8, 1)`, atteint sa valeur finale dès ~30 % du
       temps : la traversée se joue en un éclair et les deux tiers restants se
       passent hors champ, où le calque a déjà tout couvert. Allonger la durée
       n'aurait allongé que cette queue invisible — c'est la courbe qu'il faut
       tempérer pour que le bandeau se voie passer. */
    --at-btn-fx-duration: 0.55s;
    --at-btn-fx-ease: cubic-bezier(0.35, 0.85, 0.3, 1);
    --at-btn-fx-skew: 30deg;
    --at-btn-fx-bleed: -50%;

    /* Bascule de couleur calée sur la TRAVERSÉE, pas sur la durée nominale.
       La courbe étant rapide au départ, l'arête du bandeau atteint le libellé
       vers 15 % du temps alors que la transition, elle, court encore : un délai
       calculé en fraction de la durée totale arrivait trop tard.

       L'enjeu est propre aux types PLEINS : l'aplat qui arrive a la couleur du
       texte au repos (l'inversion, cf. les jetons `fx` du socle). Tant que le
       texte n'a pas basculé, il est de la couleur de ce qui le recouvre — donc
       invisible. Le fondu est volontairement étalé sur ~un tiers de la durée
       pour encadrer le passage de l'arête plutôt que de tomber pile dessus. */
    --at-btn-fx-text-delay: calc(var(--at-btn-fx-duration) * 0.15);
    --at-btn-fx-text-duration: calc(var(--at-btn-fx-duration) * 0.3);

    transition: background-color var(--at-btn-duration),
        color var(--at-btn-fx-text-duration) var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration), transform var(--at-btn-duration);
}

/* Le biais n'est PAS constant : son angle interpole de 30° à 0° pendant que le
   bandeau glisse, exactement comme dans l'original (`skew(30deg)` au repos,
   absent du survol). C'est ce redressement progressif — et non une rotation —
   qui donne son caractère à l'effet.

   L'angle doit valoir son maximum à l'état de DÉPART, pas d'arrivée : le seul
   moment où l'arête se voit est la traversée. Une fois le bouton couvert, la
   pente est hors champ et ne se lit plus, dans un sens comme dans l'autre.

   Les deux états déclarent la même liste de fonctions : l'interpolation se fait
   fonction par fonction, sans passer par la décomposition de matrices. */
.at-btn-hover-mimas::before {
    transform: translate3d(-100%, 0, 0) skewX(var(--at-btn-fx-skew));
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-mimas:hover::before {
    transform: translate3d(0, 0, 0) skewX(0deg);
}

/* atlas — le contenu s'efface et une bande de libellés défile à sa place.

   Contrairement aux trois précédents, cet effet ne peint RIEN : pas d'aplat, pas
   d'inversion de couleur. Il annule donc les deux comportements du socle, en
   passant par ses jetons plutôt qu'en luttant contre ses sélecteurs —
   `--at-btn-fx-text` ramené à la couleur normale, et le calque supprimé net.

   L'animation n'est déclarée QUE dans l'état survolé : au repos il n'existe
   aucune animation, donc aucun objet à faire vivre ni de calque de composition
   à promouvoir, sur chaque bouton de la page. Une boucle continue redémarrant
   à zéro, la reprise ne se voit pas. */

.at-btn-hover-atlas {
    --at-btn-fx-text: var(--at-btn-text);
    /* Aucun aplat n'est posé : la bordure de `soft` reste celle du repos. */
    --at-btn-fx-border: var(--at-btn-border-color);
    --at-btn-mq-duration: 1.1s;
    --at-btn-mq-gap: 0.6em;
    --at-btn-mq-fade: 0.25s;
}

.at-btn-hover-atlas::before {
    content: none;
}

/* Le contenu réel s'efface ; la bande, elle, apparaît. */
.at-btn-hover-atlas>:not(.at-btn__marquee) {
    transition: opacity var(--at-btn-mq-fade);
}

.at-btn-hover-atlas:hover>:not(.at-btn__marquee) {
    opacity: 0;
}

.at-btn__marquee {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    overflow: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--at-btn-mq-fade);
}

.at-btn-hover-atlas:hover>.at-btn__marquee {
    opacity: 1;
}

.at-btn__marquee-track {
    display: flex;
    width: max-content;
}

/* Un motif de la bande = le contenu complet du bouton, mis en page comme lui
   (mêmes classes d'icône, même gouttière). */
.at-btn__marquee-item {
    display: inline-flex;
    align-items: center;
    gap: var(--at-btn-gap);
    padding-inline-end: var(--at-btn-mq-gap);
    white-space: nowrap;
}

.at-btn-hover-atlas:hover>.at-btn__marquee>.at-btn__marquee-track {
    animation: at-btn-marquee var(--at-btn-mq-duration) linear infinite;
}

/* Un tour = exactement UNE copie, d'où le décalage rapporté au nombre de
   copies. La boucle est donc continue quel que soit ce nombre — un décalage
   fixe de -50 % ne le serait qu'avec un compte pair. */
@keyframes at-btn-marquee {
    to {
        transform: translate3d(calc(-100% / var(--at-btn-mq-copies, 10)), 0, 0);
    }
}

/* pandora — la face du bouton est décalée au-dessus d'une plaque fixe, et
   s'en écarte au survol.

   L'original superpose deux boîtes : un fond sombre, et un `<span>` décalé qui
   porte tout le contenu. Ici la plaque est une OMBRE DURE, ce qui évite d'avoir
   à envelopper le contenu dans un élément de plus : un seul nœud, et l'effet
   marche pour tous les types. Une ombre externe n'étant jamais peinte sous la
   boîte, `outline` et `ghost` ne la laissent pas transparaître malgré leur fond
   transparent.

   Décalages en `em` et non en pixels : ils suivent ainsi l'échelle des tailles,
   là où le 4px/8px d'origine paraîtrait timide sur un bouton `xl`. */

.at-btn-hover-pandora {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-lift: 0.25em;
    --at-btn-fx-lift-hover: calc(var(--at-btn-fx-lift) * 2);
    --at-btn-fx-plate: color-mix(in oklab, var(--at-btn-current) 70%, black);

    /* Aucun aplat n'est posé : ni inversion de couleur, ni bordure qui suit. */
    --at-btn-fx-text: var(--at-btn-text);
    --at-btn-fx-border: var(--at-btn-border-color);

    transform: translate3d(calc(var(--at-btn-fx-lift) * -1), calc(var(--at-btn-fx-lift) * -1), 0);
    box-shadow: var(--at-btn-fx-lift) var(--at-btn-fx-lift) 0 0 var(--at-btn-fx-plate);
    transition: background-color var(--at-btn-duration), color var(--at-btn-duration),
        border-color var(--at-btn-duration),
        transform var(--at-btn-fx-duration) var(--at-btn-fx-ease),
        box-shadow var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-pandora::before {
    content: none;
}

.at-btn-hover-pandora:hover {
    transform: translate3d(calc(var(--at-btn-fx-lift-hover) * -1), calc(var(--at-btn-fx-lift-hover) * -1), 0);
    box-shadow: var(--at-btn-fx-lift-hover) var(--at-btn-fx-lift-hover) 0 0 var(--at-btn-fx-plate);
}

/* anthe — le bouton s'étire en pointe vers la droite, et son contenu recule.

   L'original découpe la PLAQUE (`::before`), le bouton lui-même n'ayant aucun
   fond. Ici la découpe est portée par le BOUTON : c'est la seule façon qu'elle
   emporte aussi la bordure, donc que la pointe existe pour tous les types et
   pas seulement pour les fonds pleins.

   La profondeur de la pointe est en `em` et non en pourcentage (75 % chez
   Codrops) : l'angle du chevron reste ainsi constant, là où un pourcentage le
   rendrait très aigu sur un bouton long et très ouvert sur un bouton court. */

.at-btn-hover-anthe {
    --at-btn-fx-duration: 0.4s;
    --at-btn-fx-ease: cubic-bezier(0.2, 1, 0.8, 1);
    --at-btn-fx-point: 1.25em;
    --at-btn-fx-shift: 0.5em;

    /* Les fonds pleins gardent leurs couleurs : c'est la forme qui joue. */
    --at-btn-fx-text: var(--at-btn-text);
    --at-btn-fx-border: var(--at-btn-border-color);

    clip-path: polygon(0 0, 100% 0, 100% 50%, 100% 100%, 0 100%);
    transition: background-color var(--at-btn-fx-duration) var(--at-btn-fx-ease),
        color var(--at-btn-duration), border-color var(--at-btn-fx-duration) var(--at-btn-fx-ease),
        clip-path var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-anthe::before {
    content: none;
}

/* Le point médian reste à 100 % : c'est lui qui forme la pointe pendant que les
   deux coins droits reculent. */
.at-btn-hover-anthe:hover {
    clip-path: polygon(0 0,
            calc(100% - var(--at-btn-fx-point)) 0,
            100% 50%,
            calc(100% - var(--at-btn-fx-point)) 100%,
            0 100%);
}

.at-btn-hover-anthe>*:not(.at-btn__layer) {
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* Le contenu recule pour ne pas se retrouver sous la pointe. */
.at-btn-hover-anthe:hover>*:not(.at-btn__layer) {
    transform: translate3d(calc(var(--at-btn-fx-shift) * -1), 0, 0);
}

/* Types transparents : une découpe en biais COUPE la bordure, dont les deux
   arêtes diagonales resteraient sans tracé — le contour paraîtrait rompu. On
   remplit donc la pointe et on efface la bordure, ce qui revient à l'inversion
   que ces types font déjà au survol, mais en forme de fanion. `ghost`, sans
   fond ni bordure, n'aurait de toute façon rien à découper. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-anthe:hover {
    background-color: var(--at-btn-current);
    border-color: transparent;
    --at-btn-fx-text: var(--at-light);
}

/* pallene — le bouton se remplit depuis ses bords vers le centre.

   Une ombre INTERNE dont l'étalement grandit : elle épouse d'elle-même le
   `border-radius` du bouton, donc l'effet marche avec tous les arrondis, de
   l'angle droit à la pilule, sans jamais toucher à la forme. C'est ce qui
   permet d'écarter le morph de rayon de l'original (`border-radius: 50%`) sans
   rien perdre du remplissage.

   Une ombre interne étant découpée sur la boîte de padding, la bordure garde sa
   propre couleur : `outline` conserve son cercle pendant que l'intérieur se
   remplit, `normal` garde le sien une fois inversé. Rien à traiter par type. */

.at-btn-hover-pallene {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-ease: cubic-bezier(0.7, 0, 0.2, 1);

    /* Il suffit de couvrir la DEMI-HAUTEUR : l'étalement gagne autant depuis
       chaque bord, et dès que les bords haut et bas se rejoignent il n'y a plus
       de trou, quelle que soit la largeur. Le viser au plus juste garde le
       remplissage visible jusqu'au bout de la transition — trop généreux, il
       serait terminé à mi-course. */
    --at-btn-fx-spread: 1.6em;

    /* Le centre est rempli EN DERNIER : la couleur du texte bascule donc tard,
       contrairement aux balayages latéraux où elle doit partir tôt. */
    --at-btn-fx-text-delay: calc(var(--at-btn-fx-duration) * 0.6);

    box-shadow: inset 0 0 0 0 var(--at-btn-fx-color);
    transition: background-color var(--at-btn-duration),
        color var(--at-btn-duration) var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration),
        box-shadow var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-pallene::before {
    content: none;
}

.at-btn-hover-pallene:hover {
    box-shadow: inset 0 0 0 var(--at-btn-fx-spread) var(--at-btn-fx-color);
}

/* telesto — une lame en pointe traverse le bouton, un aplat plein prend le
   relais à l'arrivée, et le contenu sort par la droite pour revenir par la
   gauche.

   Deux calques, ce qui est possible parce que `::after` était encore libre :
   `::before` porte la lame pointue, `::after` l'aplat carré qui la remplace
   d'un coup une fois la traversée finie. Ce relais est le cœur de l'effet — il
   fait disparaître la pointe pile au moment où elle atteindrait le bord, et
   laisse le bouton normalement rempli.

   La lame déborde de la profondeur de sa pointe, de sorte que celle-ci commence
   exactement au bord droit du bouton : la pointe n'est donc visible que PENDANT
   la traversée, jamais à l'arrivée. C'est le rôle du lien entre `--…-bleed` et
   `--…-point`, là où l'original fige un 135 % / 75 % qui suppose une largeur. */

.at-btn-hover-telesto {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-point: 1.25em;
    --at-btn-fx-bleed: calc(var(--at-btn-fx-point) * -1);

    /* Départ, temps hors champ, retour — les trois temps de l'original. */
    --at-btn-fx-out: 0.1s;
    --at-btn-fx-hold: 0.2s;
    --at-btn-fx-in: 0.3s;

    /* Le contenu est hors champ entre la sortie et le retour : c'est là que la
       couleur bascule, donc sans jamais être vue de la mauvaise teinte. */
    --at-btn-fx-text-delay: calc((var(--at-btn-fx-out) + var(--at-btn-fx-hold)) / 2);

    transition: background-color var(--at-btn-duration),
        color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration);
}

/* La lame : hors champ à gauche, elle traverse au survol. Sa transition n'est
   déclarée QUE dans l'état survolé — au retour elle reprend sa place d'un coup,
   pendant que l'aplat, lui, sort en glissant. C'est cette asymétrie qui donne
   sa nervosité à l'effet. */
.at-btn-hover-telesto::before {
    clip-path: polygon(0 0,
            calc(100% - var(--at-btn-fx-point)) 0,
            100% 50%,
            calc(100% - var(--at-btn-fx-point)) 100%,
            0 100%);
    transform: translate3d(-100%, 0, 0);
}

.at-btn-hover-telesto:hover::before {
    transform: translate3d(0, 0, 0);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* L'aplat de relais, sans pointe : il attend hors champ à droite et se pose
   d'un coup (transition quasi nulle, retardée de la durée de la traversée). */
.at-btn-hover-telesto::after {
    content: '';
    position: absolute;
    /* Même débord que le calque du socle, pour la même raison. */
    inset: -2px var(--at-btn-fx-bleed) -2px;
    z-index: 0;
    background-color: var(--at-btn-fx-color);
    pointer-events: none;
    transform: translate3d(100%, 0, 0);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-telesto:hover::after {
    transform: translate3d(0, 0, 0);
    transition: transform 0.01s var(--at-btn-fx-duration);
}

.at-btn-hover-telesto:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-telesto:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-telesto:hover .at-btn__icon>* {
    animation: at-btn-slide-out var(--at-btn-fx-out) forwards var(--at-btn-fx-delay, 0s),
        at-btn-slide-in var(--at-btn-fx-in) forwards calc(var(--at-btn-fx-hold) + var(--at-btn-fx-delay, 0s));
}

@keyframes at-btn-slide-out {
    to {
        transform: translate3d(var(--at-btn-fx-travel, 105%), 0, 0);
    }
}

@keyframes at-btn-slide-in {
    from {
        transform: translate3d(calc(var(--at-btn-fx-travel, 105%) * -1), 0, 0);
    }

    to {
        transform: translate3d(0, 0, 0);
    }
}

/* calypso — un cercle gonfle depuis le bas et recouvre le bouton, un aplat
   prend le relais, et le contenu s'étire en sortant puis en revenant.

   Le calque du socle devient ici un DISQUE : plus large que le bouton et
   débordant très largement vers le haut, de sorte que son arête basse — la
   seule qu'on voie — reste une courbe franche pendant toute la montée. Sa
   hauteur vient d'un `padding-bottom` en pourcentage, qui se rapporte à la
   LARGEUR : c'est ce qui garantit un cercle et non une ellipse, quelle que soit
   la longueur du libellé.

   Comme pour telesto, un second calque carré se pose d'un coup à l'arrivée : le
   disque, aussi grand soit-il, laisserait sinon ses coins bas à découvert.

   L'étirement du contenu (`scaleY(2)` + fondu) est ce qui distingue cet effet
   d'un simple aller-retour : le libellé s'allonge en filant, comme aspiré. */

.at-btn-hover-calypso {
    --at-btn-fx-duration: 0.4s;
    --at-btn-fx-ease: cubic-bezier(0.1, 0, 0.3, 1);

    --at-btn-fx-out: 0.3s;
    --at-btn-fx-hold: 0.3s;
    --at-btn-fx-in: 0.3s;
    --at-btn-fx-text-delay: calc((var(--at-btn-fx-out) + var(--at-btn-fx-hold)) / 2);

    transition: background-color var(--at-btn-duration),
        color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration);
}

/* Le disque est dimensionné dans les DEUX axes en part du bouton, et non par un
   `padding-bottom` en pourcentage comme l'original : celui-ci se rapporte à la
   largeur, si bien qu'un bouton au libellé court se retrouvait avec un disque
   trop bas pour le couvrir pendant sa montée. Ici il déborde toujours des
   quatre côtés, quelle que soit la longueur du libellé.

   L'origine de l'échelle est posée au BAS du disque : réduit à zéro il se
   ramasse donc sous le bouton, et grandit vers le haut. Cela remplace le
   couple translation + échelle de l'original par une seule propriété animée,
   et le point de départ reste juste — l'original devait le rattraper à la
   main. */
.at-btn-hover-calypso::before {
    width: 140%;
    height: 260%;
    left: -20%;
    top: -110%;
    border-radius: 50%;
    transform-origin: 50% 100%;
    transform: scale(0);
}

.at-btn-hover-calypso:hover::before {
    transform: scale(1);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-calypso::after {
    content: '';
    position: absolute;
    inset: -2px var(--at-btn-fx-bleed, -2px) -2px;
    z-index: 0;
    background-color: var(--at-btn-fx-color);
    pointer-events: none;
    transform: translate3d(0, -100%, 0);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-calypso:hover::after {
    transform: translate3d(0, 0, 0);
    transition: transform 0.05s linear var(--at-btn-fx-duration);
}

.at-btn-hover-calypso:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-calypso:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-calypso:hover .at-btn__icon>* {
    animation: at-btn-stretch-out var(--at-btn-fx-out) forwards var(--at-btn-fx-delay, 0s),
        at-btn-stretch-in var(--at-btn-fx-in) forwards calc(var(--at-btn-fx-hold) + var(--at-btn-fx-delay, 0s));
}

@keyframes at-btn-stretch-out {
    to {
        transform: translate3d(0, calc(var(--at-btn-fx-travel, 105%) * -1), 0) scale3d(1, 2, 1);
        opacity: 0;
    }
}

@keyframes at-btn-stretch-in {
    from {
        transform: translate3d(0, var(--at-btn-fx-travel, 105%), 0) scale3d(1, 2, 1);
        opacity: 0;
    }

    to {
        transform: translate3d(0, 0, 0);
        opacity: 1;
    }
}

/* skoll — le fond se dérobe par le bas et découvre la couleur d'arrivée, le
   contenu roulant comme dans hyperion.

   Seul effet dont le calque PART au lieu d'arriver, et c'est ce qui fait sa
   valeur : la lecture n'est pas « quelque chose recouvre le bouton » mais « la
   couleur tombe et laisse voir ce qu'il y a dessous ».

   L'inversion se règle sans toucher au socle :
     - le calque prend la couleur de REPOS et couvre le bouton dès le départ ;
     - le fond du bouton bascule sur la couleur d'arrivée à l'instant du survol,
       à l'abri sous le calque, qui n'a plus qu'à s'écarter pour la révéler.

   Le calque étant exactement de la couleur du fond au repos, il est invisible
   tant qu'il ne bouge pas : aucun liseré sur l'arrondi, et rien à corriger. */

.at-btn-hover-skoll {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-roll: 0.2s;
    --at-btn-fx-text-delay: var(--at-btn-fx-roll);

    /* RÈGLE DU JEU : le basculement du fond doit toujours se produire PENDANT
       que le calque couvre, sinon on le voit. Ici le calque couvre au départ et
       s'écarte : l'aller est donc immédiat, le retour attend que le calque soit
       revenu.

       D'où une transition déclarée dans les DEUX états — c'est le seul moyen
       d'avoir des retards différents à l'aller et au retour, puisque chaque
       sens lit la liste de l'état vers lequel il va. La voie de l'animation,
       employée ailleurs, ne convient pas ici : elle disparaît avec `:hover` et
       la valeur qu'elle tenait retombe SANS transition — le fond reprenait sa
       couleur d'un coup, et la remontée du calque ne se voyait plus. */
    --at-btn-fx-settle-delay: 0s;

    /* Au RETOUR, les TROIS couleurs basculent au même instant : fond, texte et
       bordure. Les laisser à des rythmes différents — la bordure suivait encore
       un fondu de 0,25 s sans retard — donnait un clignotement à la sortie du
       survol, chacune changeant à son heure.

       Cet instant est pris aux trois quarts du retour, pas à sa fin : le calque
       recouvre le libellé bien avant d'avoir fini sa course, et viser la toute
       fin expose au décalage d'une image, que la courbe très lente en fin de
       trajet rend justement visible. */
    --at-btn-fx-revert-delay: calc(var(--at-btn-fx-duration) * 0.75);

    /* La bordure ne peut pas être traitée comme le reste : posée hors de la
       boîte de padding, `overflow: hidden` empêche tout calque interne de la
       recouvrir — elle est donc à découvert du début à la fin. Or elle est
       invisible aux deux EXTRÉMITÉS (pâle sur pâle au repos, colorée sur
       coloré à l'arrivée) : seul le trajet la montre. Un fondu calé sur le
       mouvement du calque est donc préférable à une bascule sèche, qui se
       remarquait sur `soft`. */
    transition: background-color 0s var(--at-btn-fx-revert-delay),
        color 0s var(--at-btn-fx-revert-delay),
        border-color var(--at-btn-fx-back, var(--at-btn-fx-duration)) var(--at-btn-fx-ease);
}

/* À l'ALLER : le fond bascule selon le sens de l'effet (à l'abri sous le
   calque), texte et bordure à l'instant où le contenu est hors champ. */
.at-btn-hover-skoll:hover {
    background-color: var(--at-btn-fx-color);
    /* Bordure en fondu sur toute la durée du mouvement (cf. la note de l'état
       de repos) : elle est la seule que le calque ne puisse pas couvrir. */
    transition: background-color 0s var(--at-btn-fx-settle-delay),
        color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* Types transparents : rien à dérober, un bouton sans fond n'a pas de couleur à
   laisser tomber. L'effet s'inverse donc — c'est l'aplat qui TOMBE DEDANS par
   le haut. Le mouvement va dans le même sens, la lecture reste la même, et les
   deux retards se retournent avec lui : le calque couvre désormais à la fin. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-skoll {
    --at-btn-fx-settle-delay: var(--at-btn-fx-duration);
    --at-btn-fx-revert-delay: 0s;
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-skoll::before {
    background-color: var(--at-btn-fx-color);
    transform: translate3d(0, -100%, 0);
}

/* Là, en revanche, le calque ARRIVE : son arête de tête doit couvrir jusqu'aux
   coins, sinon le fond de repos y percerait pendant tout le trajet. On lui rend
   donc son débord et sa forme rectangulaire. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-skoll::before {
    inset: -2px var(--at-btn-fx-bleed, -2px) -2px;
    border-radius: 0;
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-skoll:hover::before {
    transform: translate3d(0, 0, 0);
}

/* Le calque emprunte la FORME du bouton au lieu de s'y faire découper. La
   différence n'existe que pour cet effet, parce qu'il est le seul dont on voie
   l'arête s'éloigner : découpé, un rectangle garde un bord droit qui descend au
   milieu du disque ; taillé à la forme, c'est bien un cercle qui tombe.

   D'où aussi le retour à `inset: 0` — le débord de 2 px, utile ailleurs contre
   le liseré d'antialiasing, fausserait ici la forme. Il ne manque à rien :
   calque et fond ont la même couleur au repos, il n'y a pas de liseré possible. */
.at-btn-hover-skoll::before {
    inset: 0;
    border-radius: inherit;
    /* Couleur de repos : le calque se confond avec le bouton avant de tomber. */
    background-color: var(--at-btn-bg);
    transform: translate3d(0, 0, 0);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-skoll:hover::before {
    transform: translate3d(0, 100%, 0);
}

.at-btn-hover-skoll:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-skoll:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-skoll:hover .at-btn__icon>* {
    animation: at-btn-roll-out var(--at-btn-fx-roll) forwards var(--at-btn-fx-delay, 0s),
        at-btn-roll-in var(--at-btn-fx-roll) forwards calc(var(--at-btn-fx-roll) + var(--at-btn-fx-delay, 0s));
}

/* greip — le calque se replie en diagonale et libère le bouton, le contenu
   roulant comme dans hyperion.

   Même famille que skoll : le calque PART au lieu d'arriver, avec la même
   mécanique de couleurs — il porte la teinte de repos et couvre dès le départ,
   le fond basculant sur la couleur d'arrivée à l'abri sous lui. Ce qui change
   est la façon de s'en aller : son coin bas-droit est tiré vers le coin
   haut-gauche, si bien que le rectangle se replie en triangle puis s'efface.

   Le repli étant plus lent que le retour (0,3 s contre 0,2 s, comme dans
   l'original), la durée de retour a son propre jeton — et le retard du fond
   s'aligne dessus, sinon il basculerait avant que le calque n'ait fini de
   recouvrir. */

.at-btn-hover-greip {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-back: 0.2s;
    --at-btn-fx-roll: 0.2s;
    --at-btn-fx-text-delay: var(--at-btn-fx-roll);

    --at-btn-fx-settle-delay: 0s;
    /* Comme pour skoll : les trois couleurs au même instant, aux trois quarts
       du retour. Ici la référence est la durée de RETOUR, plus courte. */
    --at-btn-fx-revert-delay: calc(var(--at-btn-fx-back) * 0.75);

    /* La bordure ne peut pas être traitée comme le reste : posée hors de la
       boîte de padding, `overflow: hidden` empêche tout calque interne de la
       recouvrir — elle est donc à découvert du début à la fin. Or elle est
       invisible aux deux EXTRÉMITÉS (pâle sur pâle au repos, colorée sur
       coloré à l'arrivée) : seul le trajet la montre. Un fondu calé sur le
       mouvement du calque est donc préférable à une bascule sèche, qui se
       remarquait sur `soft`. */
    transition: background-color 0s var(--at-btn-fx-revert-delay),
        color 0s var(--at-btn-fx-revert-delay),
        border-color var(--at-btn-fx-back, var(--at-btn-fx-duration)) var(--at-btn-fx-ease);
}

.at-btn-hover-greip:hover {
    background-color: var(--at-btn-fx-color);
    /* Bordure en fondu sur toute la durée du mouvement (cf. la note de l'état
       de repos) : elle est la seule que le calque ne puisse pas couvrir. */
    transition: background-color 0s var(--at-btn-fx-settle-delay),
        color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-greip::before {
    inset: 0;
    background-color: var(--at-btn-bg);
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
    transition: clip-path var(--at-btn-fx-back) var(--at-btn-fx-ease);
}

/* Le troisième sommet — le coin bas-droit — rejoint le premier : c'est ce seul
   déplacement qui replie la forme. */
.at-btn-hover-greip:hover::before {
    clip-path: polygon(0 0, 100% 0, 0 0, 0 100%);
    transition-duration: var(--at-btn-fx-duration);
}

/* Types transparents : rien à replier, un bouton sans fond n'a pas de couleur à
   retirer. Le calque se DÉPLIE alors pour arriver, et les deux retards se
   retournent, comme pour skoll. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-greip {
    --at-btn-fx-settle-delay: var(--at-btn-fx-duration);
    --at-btn-fx-revert-delay: 0s;
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-greip::before {
    background-color: var(--at-btn-fx-color);
    clip-path: polygon(0 0, 100% 0, 0 0, 0 100%);
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-greip:hover::before {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

.at-btn-hover-greip:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-greip:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-greip:hover .at-btn__icon>* {
    animation: at-btn-roll-out var(--at-btn-fx-roll) forwards var(--at-btn-fx-delay, 0s),
        at-btn-roll-in var(--at-btn-fx-roll) forwards calc(var(--at-btn-fx-roll) + var(--at-btn-fx-delay, 0s));
}

/* dione — l'aplat se contracte pendant qu'un cadre fin grandit jusqu'au bord.

   Deux calques qui se croisent, ce qui n'existait pas encore : l'un rétrécit,
   l'autre s'étend, et ils échangent leurs rôles à mi-chemin.

   Pour qu'une contraction se VOIE, il faut que quelque chose apparaisse
   derrière : le fond du bouton passe donc en transparent et c'est le calque qui
   le peint. Au repos rien ne change à l'œil — le calque couvre exactement — et
   au survol l'espace libéré laisse voir la page, comme dans l'original.

   Le cadre paraît en fondu plutôt que de compter sur une coïncidence de
   couleurs (l'original le confond avec l'aplat au repos, ce qui suppose que les
   deux soient de la même teinte). Il est ainsi invisible au repos quel que soit
   le type de bouton. */

.at-btn-hover-dione {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-ease: cubic-bezier(0.2, 1, 0.7, 1);
    --at-btn-fx-shrink-x: 0.9;
    --at-btn-fx-shrink-y: 0.8;

    /* Le contenu reste posé sur l'aplat, qui garde sa couleur : rien à inverser. */
    --at-btn-fx-text: var(--at-btn-text);
    --at-btn-fx-border: var(--at-btn-border-color);

    /* La bordure est effacée : le cadre est le seul trait extérieur de l'effet,
       et un second cerclage juste à côté brouillait sa lecture. Sa couleur est
       neutralisée plutôt que son épaisseur, pour ne pas modifier la géométrie
       du bouton — le calque et le cadre s'appuient dessus. */
    --at-btn-border-color: transparent;

    background-color: transparent;
}

/* Les tailles ne se contractent pas au même taux en largeur et en hauteur : sur
   un bouton allongé, un taux unique déplacerait beaucoup les bords verticaux et
   à peine les horizontaux. Une forme imposée n'a pas ce problème — et un taux
   inégal y transformerait le cercle en ellipse, ce qui se voit immédiatement. */
.at-btn:where(.at-btn-shape-circle, .at-btn-shape-square).at-btn-hover-dione {
    --at-btn-fx-shrink-x: 0.85;
    --at-btn-fx-shrink-y: 0.85;
    --at-btn-fx-frame-x: 0.7;
    --at-btn-fx-frame-y: 0.7;
}

.at-btn-hover-dione::before {
    inset: 0;
    border-radius: inherit;
    background-color: var(--at-btn-bg);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-dione:hover::before {
    transform: scale3d(var(--at-btn-fx-shrink-x), var(--at-btn-fx-shrink-y), 1);
}

/* Types transparents : pas d'aplat à contracter. Il ARRIVE alors, déjà à sa
   taille réduite — le croisement avec le cadre demeure, et le contenu se pose
   dessus, d'où l'inversion de sa couleur que le socle prévoit. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-dione {
    --at-btn-fx-text: var(--at-light);
}

/* `outline` est le seul type dont la bordure EST l'apparence de repos : la lui
   retirer le ferait disparaître. Il la garde donc, et se passe en échange du
   cadre, qui ferait doublon juste à côté. */
.at-btn.at-btn-outline.at-btn-hover-dione {
    --at-btn-border-color: var(--at-btn-current);
}

.at-btn.at-btn-outline.at-btn-hover-dione::after {
    content: none;
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-dione::before {
    background-color: var(--at-btn-fx-color);
    transform: scale3d(0, 0, 1);
}

.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-dione:hover::before {
    transform: scale3d(var(--at-btn-fx-shrink-x), var(--at-btn-fx-shrink-y), 1);
}

/* Le cadre : petit et effacé au repos, il gagne le bord du bouton. */
.at-btn-hover-dione::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border: 1px solid var(--at-btn-current);
    border-radius: inherit;
    opacity: 0;
    transform: scale3d(var(--at-btn-fx-frame-x, 0.85), var(--at-btn-fx-frame-y, 0.65), 1);
    pointer-events: none;
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease),
        opacity var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-dione:hover::after {
    opacity: 1;
    transform: scale3d(1, 1, 1);
}

/* narvi — une pointe de bulle pousse sous le bouton, qui se soulève, pendant
   que le contenu roule.

   L'original dessine la bulle dans les 70 % HAUTS du bouton et réserve le reste
   à la pointe. Transposé tel quel, un bouton narvi serait visiblement plus
   court que ses voisins à taille égale et ne s'alignerait plus avec eux. Ici la
   bulle reste le bouton lui-même — avec son fond, son arrondi et sa forme, quels
   qu'ils soient — et la pointe pousse EN DEHORS. Le bouton garde donc sa
   géométrie, et l'effet marche avec l'arrondi comme avec la forme ronde.

   Cela suppose de laisser dépasser (`overflow: visible`), ce qui prive les
   effets de leur masque habituel : le contenu est donc masqué par son ENVELOPPE,
   serrée autour de lui — d'où la distance de sortie ramenée à 105 %. */

.at-btn-hover-narvi {
    --at-btn-fx-duration: 0.3s;
    --at-btn-fx-roll: 0.15s;
    --at-btn-fx-tail: 0.7em;
    --at-btn-fx-tail-w: 1.5em;
    --at-btn-fx-lift: 0.15em;
    --at-btn-fx-travel: 105%;

    /* Rien n'est recouvert : ni inversion de couleur, ni bordure qui suit. */
    --at-btn-fx-text: var(--at-btn-text);
    --at-btn-fx-border: var(--at-btn-border-color);

    overflow: visible;
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

/* Pas de calque : c'est le bouton qui fait la bulle. */
.at-btn-hover-narvi::before {
    content: none;
}

/* L'enveloppe redevient le masque, faute d'un bouton qui découpe. */
.at-btn-hover-narvi>.at-btn__group {
    overflow: hidden;
}

/* La pointe, sous le bord bas. Elle pousse depuis son attache, donc l'origine
   de l'échelle est en haut. */
.at-btn-hover-narvi::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 62%;
    width: var(--at-btn-fx-tail-w);
    height: var(--at-btn-fx-tail);
    background-color: var(--at-btn-bg);
    clip-path: polygon(0 0, 100% 0, 15% 100%);
    transform: scaleY(0);
    transform-origin: 50% 0;
    pointer-events: none;
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-narvi:hover::after {
    transform: scaleY(1);
}

/* Sur une forme ronde, le bord du bouton ne descend jusqu'en bas de sa boîte
   qu'au CENTRE : une pointe décalée sur le côté se retrouverait détachée, dans
   le vide. Elle se recentre donc, seul endroit où elle reste soudée au cercle. */
.at-btn:where(.at-btn-shape-circle, .at-btn-shape-square).at-btn-hover-narvi::after {
    left: 50%;
    margin-left: calc(var(--at-btn-fx-tail-w) / -2);
}

/* Types transparents : leur fond ne peut pas peindre la pointe. Elle prend la
   couleur du bouton, comme le ferait son tracé. */
.at-btn:where(.at-btn-outline, .at-btn-ghost).at-btn-hover-narvi::after {
    background-color: var(--at-btn-current);
}

/* La bulle se soulève en même temps que la pointe s'étire. */
.at-btn-hover-narvi:hover {
    transform: translate3d(0, calc(var(--at-btn-fx-lift) * -1), 0);
}

.at-btn-hover-narvi:hover>.at-btn__group,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-narvi:hover .at-btn__text-inner,
.at-btn:where([class*="at-btn-stagger-"]).at-btn-hover-narvi:hover .at-btn__icon>* {
    animation: at-btn-roll-out var(--at-btn-fx-roll) forwards var(--at-btn-fx-delay, 0s),
        at-btn-roll-in var(--at-btn-fx-roll) forwards calc(var(--at-btn-fx-roll) + var(--at-btn-fx-delay, 0s));
}

/* bestia — un disque enfle depuis le centre et remplit le bouton.

   L'original y ajoute un fond qui grossit de 20 %, porté par un élément à part.
   Écarté ici, pour deux raisons qui se rejoignent : `overflow: hidden` découpe
   ce gonflement, on ne le voit donc jamais — et ce calque supplémentaire, posé
   entre le fond du bouton et sa découpe, rouvrait le liseré d'antialiasing le
   long de l'arrondi. Il ne restait que ses inconvénients.

   Reste le disque, qui est l'effet lui-même, et le calque du socle suffit à le
   porter : aucun markup n'est nécessaire.

   Dimensionné dans les DEUX axes en part du bouton — et non par un
   `padding-bottom` en pourcentage comme l'original, qui rapporte la hauteur à
   la largeur et découvre les coins sur un bouton court. À 142 % il couvre la
   diagonale de tout rectangle au moins aussi large que haut. */

.at-btn-hover-bestia {
    --at-btn-fx-duration: 0.4s;
    --at-btn-fx-ease: cubic-bezier(0.1, 0, 0.3, 1);

    /* Le disque part du CENTRE : il couvre donc le libellé très tôt, et la
       couleur du texte doit basculer bien avant la fin — l'inverse d'un
       remplissage par les bords. */
    --at-btn-fx-text-delay: calc(var(--at-btn-fx-duration) * 0.3);

    transition: background-color var(--at-btn-duration),
        color 0s var(--at-btn-fx-text-delay),
        border-color var(--at-btn-duration);
}

.at-btn-hover-bestia::before {
    top: 50%;
    left: 50%;
    width: 142%;
    aspect-ratio: 1;
    border-radius: 50%;
    transform: translate3d(-50%, -50%, 0) scale(0);
    transition: transform var(--at-btn-fx-duration) var(--at-btn-fx-ease);
}

.at-btn-hover-bestia:hover::before {
    transform: translate3d(-50%, -50%, 0) scale(1);
}

@media (prefers-reduced-motion: reduce) {

    .at-btn,
    .at-btn::before,
    .at-btn::after {
        transition: none;
    }

    /* Ne vise que le survol PAR DÉFAUT (le `scale(1.02)`) : un effet dont la
       géométrie de repos passe par `transform` — pandora — serait sinon remis
       à plat au survol, donc déplacé, ce que ce réglage cherche à éviter. */
    .at-btn:hover:where(:not([class*="at-btn-hover-"]), .at-btn-hover-normal) {
        transform: none;
    }

    /* Le relief reste, seul le mouvement disparaît. */
    .at-btn-hover-pandora {
        --at-btn-fx-lift-hover: var(--at-btn-fx-lift);
    }

    /* Les keyframes ne sont PAS couvertes par `transition: none` : un effet à
       animation doit être éteint explicitement. */
    .at-btn[class*="at-btn-hover-"]:hover>.at-btn__group,
    .at-btn[class*="at-btn-hover-"]:hover .at-btn__text-inner,
    .at-btn[class*="at-btn-hover-"]:hover .at-btn__icon>* {
        animation: none;
    }

    /* Une bande figée n'aurait aucun sens : on garde simplement le bouton
       normal, libellé compris. */
    .at-btn__marquee {
        display: none;
    }

    .at-btn-hover-atlas:hover>:not(.at-btn__marquee) {
        opacity: 1;
    }
}