.card-grid {
    /* flex plutôt que CSS grid : une grille classique laisse les cases vides de la
       dernière ligne alignées à gauche (les pistes de colonnes existent même sans
       contenu) — avec flex-wrap, chaque ligne (y compris la dernière, incomplète) est
       centrée indépendamment, sans configuration supplémentaire */
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1.75rem;
    padding: 2.5rem 0 1rem;
}

.card-reveal {
    animation: card-reveal 0.5s ease-out backwards;
    animation-delay: calc(var(--i) * 0.15s);
}

@keyframes card-reveal {
    from {
        opacity: 0;
        transform: translateY(30px) scale(0.8);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.card {
    --card-color: #ffffff;

    width: 170px;
    cursor: pointer;
    transition: transform 0.15s ease-out, filter 0.15s ease-out;
    /* "transform-style: preserve-3d" retiré le 2026-09-21 : aucun enfant de .card n'utilise
       translateZ() ni n'a besoin d'un espace 3D partagé (juste l'inclinaison de la carte
       elle-même) — mais SA PRÉSENCE rendait le découpage (overflow/contain/clip-path) de tous
       les descendants non fiable dès que le transform 3D du survol (card_tilt_controller.js)
       est actif, causant le débordement des particules d'effet hors de la carte. Confirmé après
       avoir éliminé un par un : filter: drop-shadow, taille de particule, et mask-image
       (chacun désactivé/testé individuellement en devtools avec l'utilisateur, aucun n'était la
       cause) — il ne restait que ce transform-style, un comportement connu de preserve-3d avec
       le clipping, pas une supposition. */
    /* "will-change: transform" n'est plus posé ici en statique (2026-09-21, audit perf) :
       ça promouvait TOUTES les cartes de la grille dans leur propre couche GPU en permanence,
       même celles jamais survolées — coût mémoire GPU proportionnel au nombre de cartes
       affichées, page collection non paginée. card_tilt_controller.js le pose/retire
       maintenant seulement pendant le survol réel (mouseenter/mouseleave). */
    /* colonne flex + .card__sacrifice-form en margin-top:auto (plus bas) : sur une même
       ligne de .card-grid, une carte avec un badge de variante (une ligne de texte en plus)
       ne doit pas décaler SON bloc "Sacrifier" vers le bas par rapport aux autres cartes de
       la ligne — .card-grid (flex, align-items: stretch par défaut) étire déjà chaque carte
       à la hauteur de la plus grande de sa ligne, il ne restait qu'à répartir cet espace en
       bas plutôt qu'en le laissant vide sous un contenu resté aligné en haut. */
    display: flex;
    flex-direction: column;
}

.card--large {
    width: 320px;
}

.card--missing {
    cursor: default;
    opacity: 0.55;
}

/* pas ".card__image--hidden" (le placeholder générique "?", déjà volontairement neutre) :
   ce filtre assombrirait encore le texte "?" par-dessus, le rendant moins lisible. */
.card--missing .card__image:not(.card__image--hidden) {
    filter: grayscale(1) brightness(0.55) !important;
}

.card__image-wrap {
    position: relative;
}

.card__new-badge {
    /* position absolue (ancrée sur .card__image-wrap, déjà en position: relative) : sinon
       ce badge s'insère dans le flux normal et décale l'image vers le bas, désalignant les
       cartes "nouvelles" par rapport aux autres dans la même grille */
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1;
    width: fit-content;
    white-space: nowrap;
    padding: 0.25rem 0.8rem;
    border-radius: 999px;
    background: linear-gradient(90deg, #ffd23f, #ff8c00);
    color: #10131a;
    font-size: 0.7rem;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    box-shadow: 0 0 12px rgba(255, 180, 0, 0.7);
    /* synchronisé avec l'apparition différée de la carte (.card-reveal) : sinon, sur une
       grille de plusieurs cartes, ce badge apparaît dès le chargement de la page, avant
       même que la carte (encore masquée par son propre délai) ne soit visible */
    animation: new-badge-pop 0.4s ease-out backwards;
    animation-delay: calc(var(--i, 0) * 0.15s);
}

@keyframes new-badge-pop {
    from {
        opacity: 0;
        transform: translateX(-50%) translateY(6px) scale(0.7);
    }
    to {
        opacity: 1;
        transform: translateX(-50%) translateY(0) scale(1);
    }
}

/* petite gerbe d'étincelles à la couleur de la rareté (--card-color, héritée de .card),
   jouée une seule fois à l'apparition de la carte — pas un effet continu comme les
   variantes (rainbow/stars), juste un "pop" pour célébrer la découverte */
.card__new-spark-burst {
    position: absolute;
    inset: 0;
    overflow: visible;
    pointer-events: none;
    z-index: 2;
}

.card__spark {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 4px;
    height: 26px;
    margin: -13px -2px;
    border-radius: 999px;
    background: var(--card-color);
    box-shadow: 0 0 8px var(--card-color), 0 0 16px var(--card-color);
    transform: rotate(var(--spark-angle)) translateY(0) scaleY(0.2);
    opacity: 0;
    animation: new-spark-burst 1s ease-out backwards;
    /* attend que la carte ait fini son propre effet d'apparition (délai .card-reveal
       + sa durée de 0.5s) avant de se déclencher, plus le décalage propre à chaque étincelle */
    animation-delay: calc(var(--i, 0) * 0.15s + 0.5s + var(--spark-delay));
}

@keyframes new-spark-burst {
    0% {
        transform: rotate(var(--spark-angle)) translateY(0) scaleY(0.2);
        opacity: 1;
    }
    60% {
        transform: rotate(var(--spark-angle)) translateY(-90px) scaleY(1);
        opacity: 1;
    }
    100% {
        transform: rotate(var(--spark-angle)) translateY(-140px) scaleY(0.3);
        opacity: 0;
    }
}

.card__quantity-badge {
    position: absolute;
    top: -8px;
    right: -8px;
    z-index: 1;
    padding: 0.2rem 0.55rem;
    border-radius: 999px;
    background: #d6006e;
    color: #fff;
    font-size: 0.75rem;
    font-weight: 700;
}

/* --card-invert (effet "Négatif") et --card-glow (Card::collectionGlowEnabled /
   Channel::pullGlowEnabled) pilotent ce filter via des variables plutôt que des classes
   séparées : un "filter" ne s'additionne pas entre deux règles CSS distinctes (la dernière
   qui s'applique remplace tout le reste), donc deux réglages indépendants (négatif + halo
   coupé sur la même carte, par exemple) ne pourraient pas se combiner avec des classes
   modificatrices classiques sans dupliquer toutes les combinaisons possibles.
   --card-glow multiplie le rayon des drop-shadow plutôt que de les retirer : à 0, le flou
   devient 0px (donc invisible), sans avoir à gérer un filter totalement différent. */
.card__image {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 4;
    object-fit: contain;
    border-radius: 10px;
    filter:
        invert(var(--card-invert, 0))
        drop-shadow(0 0 calc(8px * var(--card-glow, 1)) var(--card-color))
        drop-shadow(0 0 calc(18px * var(--card-glow, 1)) var(--card-color));
}

/* Placeholder générique pour une carte manquante quand Channel::hideMissingCardArt est actif
   (voir card/_card.html.twig) : jamais l'illustration réelle, juste un "?" — évite de spoiler
   le dessin d'une carte pas encore obtenue en parcourant le set en entier. Déclaré APRÈS
   .card__image (même sélectivité, donc c'est l'ordre dans le fichier qui décide) : sinon
   "display: block" ci-dessus écrasait le "display: flex" nécessaire pour centrer le "?"
   (repéré par l'utilisateur sur une vraie capture — le "?" restait collé en haut à gauche). */
.card__image--hidden {
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.06);
    border: 1px dashed var(--color-border);
}

.card__hidden-mark {
    font-size: 3rem;
    font-weight: 800;
    color: var(--color-text-dim);
}

/* effet holo : dégradé arc-en-ciel, masqué par la transparence du PNG,
   déplacé par card_tilt_controller.js selon la position de la souris. Paramétrable par
   variante via --holo-hue-rotate/--holo-saturation (Variant.effectHueRotateDeg/
   effectSaturationPercent) : décale/module le dégradé fixe plutôt que de le remplacer par
   des couleurs éditables une à une, pour garder un rendu harmonieux. Défauts (0deg, 1)
   = rendu inchangé. */
.card__holo {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(
        115deg,
        transparent 0%,
        #ff008c 12%,
        #ffee00 26%,
        #00ff88 40%,
        #00d4ff 54%,
        #a200ff 68%,
        transparent 82%
    );
    background-size: 250% 250%;
    background-position: var(--holo-x, 50%) var(--holo-y, 50%);
    filter: hue-rotate(var(--holo-hue-rotate, 0deg)) saturate(var(--holo-saturation, 1));
    mix-blend-mode: color-dodge;
    opacity: var(--holo-opacity, 0.7);
    -webkit-mask-image: var(--card-image);
    mask-image: var(--card-image);
    /* voir le commentaire sur .card__sparkle-mask : sans ça, le dégradé ne s'affiche que
       sur les zones claires d'une carte sans vraie transparence (fond sombre = masqué). */
    -webkit-mask-mode: alpha;
    mask-mode: alpha;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    animation: holo-idle 5s ease-in-out infinite;
}

/* pendant l'interaction souris, le JS pilote --holo-x/--holo-y : on coupe l'animation au repos */
.card--holo-active .card__holo {
    animation: none;
}

@keyframes holo-idle {
    0%, 100% {
        background-position: 20% 30%;
    }
    50% {
        background-position: 80% 70%;
    }
}

/* calque étoiles/scintillement : masque fixe (silhouette) + calque intérieur qui tourne
   librement à l'intérieur, sans déformer le contour de la carte. "mask-mode: alpha" est
   essentiel : par défaut un masque-image utilise la LUMINANCE (clair/sombre), pas la
   transparence — sur une carte sans vrai canal alpha (fond uni sombre, cas fréquent), les
   étoiles ne s'affichaient alors que sur les zones claires du dessin (cheveux, cadre doré...)
   et disparaissaient sur les zones sombres, au lieu de couvrir toute la carte (repéré en prod
   le 2026-09-19 — pris pour un bug de répartition à tort, avant d'identifier la vraie cause). */
.card__sparkle-mask {
    position: absolute;
    inset: 0;
    overflow: hidden;
    contain: paint;
    /* overflow/contain/clip-path ici sont une sécurité, pas LA correction : le vrai débordement
       de particules constaté le 2026-09-21 n'était pas un bug de CSS/clipping (longtemps
       soupçonné à tort, tout un historique de tentatives dans FEATURES.md) mais un problème de
       GÉOMÉTRIE — AppExtension::angleSparklePositions() ne tenait pas compte de la taille de la
       particule en calculant sa position, laissant une grosse particule proche d'un bord
       dépasser la carte par construction. Corrigé là-bas ; ces propriétés de découpage restent
       en filet de sécurité pour tout futur cas similaire, sans plus rien y débordant dessus. */
    clip-path: inset(1px);
    pointer-events: none;
    -webkit-mask-image: var(--card-image);
    mask-image: var(--card-image);
    -webkit-mask-mode: alpha;
    mask-mode: alpha;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

.card__star {
    position: absolute;
    top: var(--star-top);
    left: var(--star-left);
    width: var(--star-size);
    /* "aspect-ratio: 1" plutôt que "height: var(--star-size)" (2026-09-21) : la carte n'est PAS
       carrée (ratio 3:4) — width% et height% se calculent chacun contre un axe différent du
       conteneur (largeur vs hauteur, de tailles différentes en px), donc les deux mêmes %
       donnaient un RECTANGLE, pas un carré (confirmé par capture devtools : boîte 114×130px au
       lieu d'un carré). Repéré grâce à "Scintillement selon l'angle", où cette même différence
       largeur/hauteur rendait une particule tournée bien plus large que prévu et la faisait
       déborder de la carte — les étoiles ont le même souci géométrique, juste masqué jusqu'ici
       par leur propre masque (silhouette de la carte) qui rattrapait le débordement. */
    aspect-ratio: 1;
    background-image: var(--star-image);
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    opacity: var(--star-opacity, 1);
    /* drop-shadow(s) retirés le 2026-09-21 (il y en avait 2, déjà réduits de 3 pour la perf
       le 2026-09-19) : quel que soit le nombre de couches, un filter: drop-shadow() sur cet
       élément ignore le clipping de ses ancêtres (overflow: hidden, contain: paint ET
       clip-path testés, aucun ne suffit à le contenir — confirmé par plusieurs captures
       devtools avec l'utilisateur), le halo débordait alors visuellement de la carte. Le halo
       reste porté UNIQUEMENT par .card__star::before ci-dessous (un vrai fond, pas un filtre
       — correctement coupé par le clip-path du parent). Bénéfice de perf conservé au passage :
       plus aucun drop-shadow ici. */
    /* Réglable par variante (Variant::$effectImageBrightness/effectImageContrast/
       effectImageSaturation, %, 100 = neutre) — codé en dur à 1.8/1.3/2 jusqu'ici, pensé pour
       un dessin fin/pâle en silhouette qui a besoin d'être boosté pour ressortir. Sur une
       image DÉJÀ en couleurs vives, ce boost fixe sursaturait et délavait tout vers le blanc
       (retour utilisateur, capture d'une fleur de sakura "toute blanche"). --star-boost-* posé
       par le Twig avec les anciennes valeurs par défaut (1.8/1.3/2), remplaçables par
       n'importe quelle valeur admin — 1/1/1 donne l'image dans ses couleurs naturelles. */
    filter:
        brightness(var(--star-boost-brightness, 1.8))
        contrast(var(--star-boost-contrast, 1.3))
        saturate(var(--star-boost-saturation, 2));
    animation: star-drift var(--star-duration) ease-in-out infinite;
    animation-delay: var(--star-delay);
}

/* halo lumineux derrière l'étoile : un dégradé radial se voit bien plus qu'un simple contour
   fin (le dessin d'origine est un trait très fin, peu visible une fois réduit à sa taille
   normale) — et un dégradé se peint directement, sans passe de flou comme un drop-shadow
   supplémentaire l'exigerait, donc moins cher malgré le halo plus marqué. S'applique aussi
   à .card__star--tinted (même sélecteur de base, la classe s'ajoute, ne le remplace pas). */
.card__star::before {
    content: '';
    position: absolute;
    /* halo agrandi (-70% → -100%) et boosté (filter brightness) le 2026-09-21 : jugé "limite"
       par l'utilisateur — plus de surface ET plus de punch, sans changer le réglage
       d'intensité de l'admin (--star-glow-intensity reste respecté tel quel). */
    inset: -100%;
    border-radius: 50%;
    background: radial-gradient(circle, var(--star-glow-color, #fff) 0%, transparent 70%);
    opacity: var(--star-glow-intensity, 0.85);
    mix-blend-mode: screen;
    filter: brightness(1.4);
    pointer-events: none;
}

/* quand une "couleur du dessin" est configurée sur la variante : l'image sert de masque
   (sa forme, peu importe sa couleur d'origine) et --star-image-color la remplit — sinon
   l'image s'affiche telle quelle via background-image ci-dessus */
.card__star--tinted {
    background-image: none;
    background-color: var(--star-image-color);
    -webkit-mask-image: var(--star-image);
    mask-image: var(--star-image);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

/* "couleur holographique" (Variant.effectHolographicColor) : même dégradé/technique que
   l'effet "rainbow" principal (.card__holo). Ce qui fait qu'un effet se LIT comme "vraiment
   holo" plutôt que "peinture arc-en-ciel plaquée", ce n'est pas la teinte elle-même mais le
   fait qu'elle RÉAGISSE au mouvement — exactement comme .card__holo, dont le dégradé suit
   --holo-x/--holo-y (déjà calculées par card_tilt_controller.js, aucun JS à ajouter ici, ces
   variables sont posées sur .card et héritées par ses enfants). Retenté avec
   mix-blend-mode: color-dodge puis screen avant ça (2026-09-20/21) : tous deux dépendent trop
   de la luminosité/teinte du fond sous-jacent pour un résultat fiable sur un petit trait fin
   (couleurs qui disparaissent ou ne "s'allument" pas selon l'endroit) — repassé en mode normal
   (couleurs toujours fidèles, peu importe le fond).
   Un simple décalage de teinte au survol restait perçu comme "de la peinture qui tourne", pas
   comme du holo (retour utilisateur 2026-09-20) : il manquait le REFLET, la bande claire qui
   balaie la surface — ingrédient distinct de la couleur elle-même sur une vraie carte foil.
   Ajouté ici en 2e calque de fond (bande diagonale blanche, transparente de part et d'autre)
   au-dessus du dégradé arc-en-ciel, combiné via "background-blend-mode: overlay" — ce blend-là
   mélange les calques de fond ENTRE EUX, contrairement à "mix-blend-mode" qui mélange l'élément
   avec ce qu'il y a DERRIÈRE (la photo de carte, cause des deux échecs précédents) : résultat
   fiable, peu importe la carte. Les deux calques partagent la même position/taille de fond,
   donc le reflet balaie EXACTEMENT avec le changement de teinte au mouvement de la souris.
   Prend le pas sur .card__star--tinted si les deux classes sont présentes (celle-ci est
   déclarée après dans le fichier, donc gagne à spécificité égale). */
/* Approche revue en profondeur le 2026-09-21 après plusieurs échecs avec un dégradé arc-en-ciel
   qu'on glisse via background-position (mix-blend-mode dépendant du fond, clustering près des
   bords, clamp() qui casse tout, etc.) : la vraie source du problème était le PRINCIPE, pas les
   réglages. Un dégradé qui doit boucler pour une dérive infinie fluide a FORCÉMENT la même
   couleur au début et à la fin (ici rose) — glisser sa position fait régulièrement retomber
   plusieurs étoiles sur cette "couture" commune, qui paraissent alors identiques.
   Remplacé par : une couleur de base UNIE + un filtre hue-rotate() qui la fait tourner sur toute
   la roue chromatique. hue-rotate() n'a pas de "couture" : c'est une rotation continue, modulo
   360°, jamais de retour à une valeur strictement égale à une autre étoile par coïncidence de
   bord. --holo-hue (degrés, posé par card_tilt_controller.js) apporte la réaction à la souris,
   --star-hue-offset (degrés, posé par le Twig à partir de la position de CETTE étoile) apporte
   la variation par étoile — les deux s'additionnent proprement dans calc() car ce sont deux
   angles (même unité), contrairement à l'ancienne tentative qui mélangeait des % de manières
   qui finissaient par déborder. Le halo balayant (fine bande blanche) reste un calque de fond
   séparé, inchangé : lui n'a jamais eu ce problème de couture colorée. */
.card__star--holo-tinted {
    /* reflet blanc SUPPRIMÉ (2026-09-21, règle explicite de l'utilisateur : "jamais de blanc,
       c'est moche") — il causait déjà des soucis de "trop blanc" plusieurs fois ce soir, sous
       différentes formes (reflet, halo en color-dodge...). Plus aucune source de blanc dans cet
       effet : juste la couleur (tournée par hue-rotate) et le boost brightness/contrast/saturate.
       #ff008c avait un canal (R=255) déjà à l'extrême — même piège que le jaune pur repéré sur
       "reverse holo" (brightness/saturate n'ont plus de marge à pousser sur un canal déjà au
       max). Remplacé par un rose vif SANS canal à 0 ou 255, pour que saturate/contrast
       ci-dessous aient une vraie marge de manœuvre. */
    background-color: #e0308c;
    background-size: 250% 250%;
    background-position: var(--holo-x, 50%) var(--holo-y, 50%);
    -webkit-mask-image: var(--star-image);
    mask-image: var(--star-image);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    animation: star-drift var(--star-duration) ease-in-out infinite, holo-idle 4s ease-in-out infinite;
    animation-delay: var(--star-delay), 0s;
    /* .card__star applique par défaut 2 drop-shadow BLANCS (--star-glow-color) — un filtre
       s'applique à l'élément ET son ::before composités ensemble, donc ce blanc dominait aussi
       le halo arc-en-ciel de ::before ci-dessous, le rendant quasi invisible (même famille de
       bug que le halo "trop blanc" déjà réglé une fois ce soir, repéré une 2e fois le
       2026-09-20 sur le halo cette fois, pas le remplissage). Retiré ici : seuls
       brightness/contrast/saturate (utiles pour n'importe quelle couleur) sont gardés, plus le
       hue-rotate() qui porte maintenant toute la variation de couleur (voir commentaire
       ci-dessus). Brightness réduite (1.8 → 1.3) et saturate montée (2 → 2.5) : une couleur
       UNIE poussée trop claire perd vite en vivacité (contrairement à l'ancien dégradé
       arc-en-ciel, qui gardait du contraste entre ses teintes même très éclairci) — moins de
       clair, plus de saturation compense ("bcp moins vibrant", 2026-09-21). Ce filter s'applique
       aussi au ::before (halo) par composition (voir son commentaire) — des valeurs trop hautes
       (essayé jusqu'à contrast(3)) écrasaient certains pixels fins/bords anti-aliasés du halo
       vers le NOIR ou le BLANC selon leur luminance d'origine ("les pointes sont noires",
       "c'est n'importe quoi", retour utilisateur) — repassé à des valeurs raisonnables. */
    filter: hue-rotate(calc(var(--holo-hue, 0deg) + var(--star-hue-offset, 0deg))) brightness(1.2) contrast(1.2) saturate(1.8);
}

/* dès que la souris bouge sur la carte (card_tilt_controller.js ajoute cette classe), le
   dégradé suit --holo-x/y en direct — on coupe juste la dérive automatique (holo-idle),
   même principe que .card--holo-active .card__holo plus haut dans ce fichier. */
.card--holo-active .card__star--holo-tinted {
    animation: star-drift var(--star-duration) ease-in-out infinite;
    animation-delay: var(--star-delay);
}

/* le halo (.card__star::before) reste dans --star-glow-color (blanc par défaut) sinon —
   avec un trait de dessin fin, ce grand halo uni domine visuellement le peu de couleur
   visible à travers le masque, et l'ensemble paraît surtout blanc plutôt qu'holographique.
   Dégradé conique plutôt que linéaire : un halo est un cercle, un balayage de teinte tout
   autour rend mieux qu'un dégradé en diagonale plaqué dessus.
   Le "mix-blend-mode: screen" hérité de .card__star::before est retiré ici explicitement :
   contre un fond photo à dominante chaude, "screen" fait ressortir fortement les couleurs
   FROIDES (cyan/bleu) du dégradé et quasi disparaître les couleurs chaudes (rose/jaune),
   donnant l'impression que seul le bleu s'affiche — repéré en test réel le 2026-09-20. En
   "normal", les vraies couleurs du dégradé s'affichent telles quelles, peu importe le fond.
   Le fondu doux (dégradé radial → transparent, que "screen" donnait gratuitement avant)
   est recréé via un masque radial dédié, pour ne pas perdre le halo qui s'estompe. */
/* ".card__star--holo-glow" seulement (2026-09-25, Variant::$effectHolographicGlow) : ce halo
   est un calque à part du dessin recoloré lui-même — avec beaucoup de particules proches, il
   peut se fondre en une grande tache colorée qui domine visuellement le dessin (retour
   utilisateur direct). Désactivable séparément, sans toucher au dessin recoloré (le
   hue-rotate() reste sur .card__star--holo-tinted, jamais sur ce ::before). */
.card__star--holo-tinted.card__star--holo-glow::before {
    /* resserré (hérite -70% sinon) : même dose de couleur concentrée sur moins de surface =
       plus dense/visible qu'étalée sur un grand halo dilué. */
    inset: -40%;
    /* dégradé conique statique (plus de background-position/size ici — la réaction à la souris
       et la variation par étoile passent maintenant par hue-rotate() dans le filter ci-dessous,
       même principe que .card__star--holo-tinted, voir son commentaire détaillé). */
    background-image: conic-gradient(
        from 0deg,
        #ff008c,
        #ffee00,
        #00ff88,
        #00d4ff,
        #a200ff,
        #ff008c
    );
    mix-blend-mode: normal;
    /* vérifié par capture devtools (2026-09-20) : la cascade CSS était déjà correcte (rien
       d'écrasé par erreur) — le vrai souci, c'est qu'un halo étalé (inset -70% hérité) + flou
       + opacité 0.85 se noie dans une photo chargée (feuillage, détails). Masque resserré (le
       plein reste net jusqu'à 40% du rayon au lieu de dégrader dès 0%) + opacité forcée à 1 +
       flou réduit + saturation/luminosité boostées : plus dense, perce mieux à travers la photo. */
    -webkit-mask-image: radial-gradient(circle, white 0%, white 40%, transparent 75%);
    mask-image: radial-gradient(circle, white 0%, white 40%, transparent 75%);
    opacity: 1;
    /* "mix-blend-mode: color-dodge" tenté ici (2026-09-21) — retiré : dépend trop de la photo
       en dessous (color-dodge explose vers le BLANC sur les zones claires — ciel, feuillage
       éclairé), donnant "plein de zones blanches" sur une photo chargée (retour utilisateur).
       Même famille de piège déjà rencontrée avec "screen" sur ce même halo le 2026-09-20.
       Repassé en "normal" (fidèle peu importe la photo). Filter réduit à "blur" seul ici
       (2026-09-21) : un filter sur .card__star--holo-tinted (l'élément parent) s'applique à TOUT
       ce qu'il contient, ::before compris (composite les deux, PUIS applique son filter à
       l'ensemble) — hue-rotate/brightness/saturate déclarés ICI EN PLUS de ceux du parent se
       CUMULENT (rotation doublée, luminosité/saturation sursaturée au point de virer au blanc —
       "bah?", retour utilisateur face à des halos blancs malgré aucun blanc dans ce code). Tout
       le boost de couleur/luminosité vient désormais UNIQUEMENT du filter du parent — ce
       ::before ne garde que "blur" (le parent n'a pas de blur, pas de risque de cumul dessus). */
    mix-blend-mode: normal;
    filter: blur(2px);
}

/* effet "reverse holo" : deux calques imbriqués, comme .card__sparkle-mask + son contenu pour
   les étoiles, mais ici chacun porte SON PROPRE masque plutôt qu'un seul combiné (mask-composite
   a une syntaxe différente entre -webkit et standard, fragile — deux calques simples et
   indépendants, dont l'effet cumulé est une intersection, est plus robuste).
   Calque extérieur : masqué par la PHOTO elle-même (var(--card-image)), pour ne jamais dépasser
   la VRAIE zone visible de l'image (pas la boîte 3:4 de .card__image-wrap, qui peut avoir des
   bandes vides si la photo n'a pas exactement ce ratio — "object-fit: contain" laisse alors de
   l'espace où il n'y a aucun pixel de photo, où une particule semblait "sortir de l'image" sans
   pour autant sortir de la CARTE — repéré par l'utilisateur le 2026-09-21). Même technique que
   .card__sparkle-mask pour les étoiles, qui a toujours eu ce garde-fou. */
.card__angle-sparkle-photo-mask {
    position: absolute;
    inset: 0;
    overflow: hidden;
    contain: paint;
    clip-path: inset(1px);
    pointer-events: none;
    -webkit-mask-image: var(--card-image);
    mask-image: var(--card-image);
    -webkit-mask-mode: alpha;
    mask-mode: alpha;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

/* Calque intérieur : étincelles à positions FIXES (voir AppExtension::angleSparklePositions),
   éparpillées partout + rotation aléatoire chacune (rendu organique, pas un motif géométrique),
   masquées par un dégradé radial qui suit --holo-x/--holo-y (déjà calculées par
   card_tilt_controller.js pour l'effet holographique — réutilisées ici tel quel) : seules
   les étincelles proches de "l'angle courant" de la souris deviennent visibles. Le rayon est
   volontairement large : --holo-x/y valent 50%/50% par défaut (pas de survol), donc un spot
   trop étroit ne montre presque aucune particule tant qu'on ne survole pas la carte — priorité
   confirmée par l'utilisateur à "visible par défaut" plutôt qu'à "le mouvement se sent plus". */
.card__angle-sparkle-layer {
    position: absolute;
    inset: 0;
    overflow: hidden;
    contain: paint;
    clip-path: inset(1px);
    pointer-events: none;
    -webkit-mask-image: radial-gradient(circle at var(--holo-x, 50%) var(--holo-y, 50%), black 0%, black 22%, transparent 58%);
    mask-image: radial-gradient(circle at var(--holo-x, 50%) var(--holo-y, 50%), black 0%, black 22%, transparent 58%);
}

/* au moment où la carte apparaît (isNew), on élargit brièvement le masque pour montrer
   TOUTES les étincelles une fois — histoire que le joueur comprenne qu'il y a un effet à
   découvrir en bougeant la souris, avant de revenir au comportement normal (spot étroit) */
.card__angle-sparkle-layer--hint {
    animation: angle-sparkle-hint 1.6s ease-in-out 1;
}

@keyframes angle-sparkle-hint {
    0% {
        -webkit-mask-size: 100% 100%;
        mask-size: 100% 100%;
    }
    35%, 55% {
        -webkit-mask-size: 300% 300%;
        mask-size: 300% 300%;
    }
    100% {
        -webkit-mask-size: 100% 100%;
        mask-size: 100% 100%;
    }
}

/* .card__angle-sparkle est juste une boîte POSITIONNÉE (translate seulement, jamais tournée) —
   le dessin/masque/rotation vivent dans .card__angle-sparkle__shape (enfant dédié) ci-dessous.
   Un transform incluant rotate() sur l'élément positionné lui-même faisait échouer le découpage
   du parent (overflow/contain/clip-path, tous insuffisants dans ce cas) ; séparer la rotation
   dans un enfant dédié règle ça, comme pour .card__star (jamais tourné, jamais ce bug). */
.card__angle-sparkle {
    position: absolute;
    top: var(--sparkle-top);
    left: var(--sparkle-left);
    width: var(--sparkle-size);
    /* voir le commentaire détaillé sur .card__star : "aspect-ratio: 1" pour un VRAI carré,
       peu importe le ratio (3:4) de la carte — width%/height% à la même valeur donnaient un
       rectangle, pas un carré. */
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
    opacity: var(--sparkle-opacity, 1);
}

/* filtre repris tel quel du CSS committé (avant toute la soirée de réglages) : glow en
   drop-shadow (couleur pilotée par l'admin, --sparkle-glow-color) + brightness/saturate. Revenu
   ici sciemment le 2026-09-21 après une soirée d'ajustements devenue incohérente ("les pointes
   sont plus jaunes", "c'est moins clair que ce matin") — plutôt que deviner un énième réglage,
   retour à la dernière version qui marchait, avec SEULEMENT la correction overflow ajoutée
   (voir .card__angle-sparkle-photo-mask, .card__angle-sparkle-layer, et la séparation
   .card__angle-sparkle / .card__angle-sparkle__shape ci-dessus). */
.card__angle-sparkle__shape {
    position: absolute;
    /* l'image de cet effet (contrairement à celle des étoiles) a ses pointes qui touchent
       littéralement les 4 bords de son propre fichier, sans marge interne : même bien contenue
       dans sa boîte, une particule touche visuellement ses voisines dès qu'elles sont proches.
       Cette marge recrée l'espace de respiration que l'image des étoiles a nativement. */
    inset: 22%;
    background-image: var(--sparkle-image);
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    transform: rotate(var(--sparkle-rotation, 0deg));
    /* 4 drop-shadow → 2 (2026-09-21, test perf demandé) : drop-shadow est l'une des fonctions
       de filter les plus chères (passe de flou + recomposition), répétée par particule — 4
       couches empilées sur potentiellement 20-25 particules/carte a un vrai coût. Gardé le
       rayon le plus proche (4px, définit le contour net) et le plus large (32px, porte le
       glow visible de loin) ; à confirmer visuellement si la différence avec les 2 couches
       intermédiaires (9px/18px) se voit ou non. */
    /* Réglable par variante, même principe que .card__star ci-dessus (voir son commentaire
       détaillé) — brightness/saturate étaient codés en dur à 2.2/2.2, le contraste n'a jamais
       été ajusté ici (fallback 1 = neutre, contrast(1) ne change donc rien pour une variante
       déjà existante). */
    filter:
        drop-shadow(0 0 4px var(--sparkle-glow-color, #fff))
        drop-shadow(0 0 32px var(--sparkle-glow-color, #fff))
        brightness(var(--sparkle-boost-brightness, 2.2))
        contrast(var(--sparkle-boost-contrast, 1))
        saturate(var(--sparkle-boost-saturation, 2.2));
}

/* même technique de recoloration que .card__star--tinted, appliquée au calque interne
   (reprise telle quelle du CSS committé). */
.card__angle-sparkle--tinted .card__angle-sparkle__shape {
    background-image: none;
    background-color: var(--sparkle-image-color);
    -webkit-mask-image: var(--sparkle-image);
    mask-image: var(--sparkle-image);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

/* "couleur holographique" — non demandée à l'origine pour cet effet, ajoutée le 2026-09-21,
   gardée pour plus tard (l'utilisateur veut d'abord que le mode couleur unie ci-dessus refonctionne
   correctement) : même principe que .card__star--holo-tinted (voir son commentaire détaillé) —
   couleur de base UNIE sans canal à l'extrême + hue-rotate() (réagit à la souris via --holo-hue,
   varie par particule via --sparkle-hue-offset). Pas de star-drift ici (particules fixes par
   design), juste le scintillement de teinte (holo-idle). */
.card__angle-sparkle--holo-tinted .card__angle-sparkle__shape {
    background-image: none;
    background-color: #e0308c;
    -webkit-mask-image: var(--sparkle-image);
    mask-image: var(--sparkle-image);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    animation: holo-idle 4s ease-in-out infinite;
    filter: hue-rotate(calc(var(--holo-hue, 0deg) + var(--sparkle-hue-offset, 0deg))) brightness(1.2) contrast(1.2) saturate(1.8);
}

/* Contrairement aux étoiles (.card__star::before), ce trait n'a jamais eu de halo en mode
   uni/teinté (voir plus haut). Mais en mode holo, l'absence de halo se voit : sans lueur
   colorée, le trait fin garde un rendu "contour néon plat" plutôt que "brille de partout".
   Ajouté ici, seulement en mode holo, le même halo conique arc-en-ciel que
   .card__star--holo-tinted::before — sur .card__angle-sparkle (l'élément SANS filter, voir
   plus haut), donc pas de risque de cumul de filter avec un parent comme sur les étoiles :
   brightness/saturate peuvent rester directement sur ce ::before. */
/* ".card__angle-sparkle--holo-glow" seulement (2026-09-25, Variant::$effectHolographicGlow) —
   même principe que .card__star--holo-tinted.card__star--holo-glow::before ci-dessus : ce halo
   est un calque à part du dessin recoloré, désactivable séparément (surtout utile ici, cet
   effet est pensé pour un nombre élevé de particules — leurs halos se chevauchent vite). */
.card__angle-sparkle--holo-tinted.card__angle-sparkle--holo-glow::before {
    content: '';
    position: absolute;
    /* resserré : un halo étalé + flou se noie dans une photo chargée. */
    inset: -35%;
    border-radius: 50%;
    background: conic-gradient(
        from 0deg,
        #ff008c,
        #ffee00,
        #00ff88,
        #00d4ff,
        #a200ff,
        #ff008c
    );
    -webkit-mask-image: radial-gradient(circle, white 0%, white 40%, transparent 75%);
    mask-image: radial-gradient(circle, white 0%, white 40%, transparent 75%);
    opacity: 1;
    /* normal plutôt que screen/color-dodge : les deux dépendent trop de la luminosité de la
       photo en dessous (explosent vers le blanc sur les zones claires). */
    mix-blend-mode: normal;
    filter: blur(2px) brightness(1.6) saturate(2.5);
    pointer-events: none;
}

@keyframes star-drift {
    /* translate(-50%, -50%) recentre la particule sur --star-top/--star-left (comme
       .card__angle-sparkle) — doit être répété à CHAQUE étape, pas juste posé une fois sur
       .card__star : l'animation (toujours active, infinie) écrase entièrement le "transform"
       de la règle de base à chaque frame, un centrage posé là seulement disparaîtrait dès le
       premier tick. Bug pré-existant repéré le 2026-09-21 (jamais eu ce translate, contrairement
       à "Scintillement selon l'angle" qui l'a depuis le début) — chaque étoile était décalée
       vers le bas-droite de la moitié de sa propre taille. */
    0%, 100% {
        transform: translate(-50%, -50%) translate(0, 0) rotate(0deg);
    }
    50% {
        transform: translate(-50%, -50%) translate(var(--star-dx), var(--star-dy)) rotate(180deg);
    }
}

.card__info {
    margin-top: 0.6rem;
    text-align: center;
    color: #f2f2f2;
}

.card__name {
    margin: 0;
    font-weight: 600;
    font-size: 0.95rem;
}

.card__rarity-label {
    display: block;
    margin-top: 0.2rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--card-color);
}

.card__variant-badge {
    display: inline-block;
    margin-top: 0.3rem;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: #10131a;
    background: var(--variant-color, #ffffff);
}

.card--rare {
    --card-color: #4aa8ff;
}

.card--epique {
    --card-color: #b04aff;
}

.card--legendaire {
    --card-color: #ffd23f;
}

.card--legendaire .card__image {
    animation: legendary-glow 2s ease-in-out infinite;
}

@keyframes legendary-glow {
    /* mêmes variables --card-invert/--card-glow que .card__image (voir son commentaire) :
       une animation CSS remplace ENTIÈREMENT le filter à chaque étape (même piège que
       @keyframes star-drift, voir plus haut) — sans les répéter ici, une carte légendaire au
       halo désactivé le retrouverait quand même pendant le pouls de l'animation. */
    0%, 100% {
        filter:
            invert(var(--card-invert, 0))
            drop-shadow(0 0 calc(8px * var(--card-glow, 1)) var(--card-color))
            drop-shadow(0 0 calc(18px * var(--card-glow, 1)) var(--card-color));
    }
    50% {
        filter:
            invert(var(--card-invert, 0))
            drop-shadow(0 0 calc(14px * var(--card-glow, 1)) var(--card-color))
            drop-shadow(0 0 calc(30px * var(--card-glow, 1)) var(--card-color));
    }
}

.card-dialog {
    border: none;
    border-radius: 16px;
    padding: 2rem;
    background: transparent;
    /* le <dialog> a "overflow: auto" par défaut dans les navigateurs : l'inclinaison 3D de
       la carte (card_tilt_controller.js, rotateX/rotateY + scale) dépasse transitoirement sa
       boîte à certains angles, ce qui faisait apparaître une scrollbar parasite. */
    overflow: hidden;
}

.card-dialog::backdrop {
    background: rgba(0, 0, 0, 0.65);
    backdrop-filter: blur(4px);
}

.card-dialog__close {
    display: block;
    margin: 0 0 1rem auto;
    background: none;
    border: none;
    color: #f2f2f2;
    font-size: 1.5rem;
    cursor: pointer;
    line-height: 1;
}

.card__sacrifice-form {
    /* auto = collé au bas de .card (voir son commentaire) plutôt qu'un espacement fixe */
    margin-top: auto;
    padding-top: 0.5rem;
    text-align: center;
    font-size: 0.7rem;
    color: #f2f2f2;
}

.card__sacrifice-amount {
    width: 2.5rem;
    text-align: center;
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.3);
    background: rgba(0, 0, 0, 0.3);
    color: #fff;
}

.card__sacrifice-btn {
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    border: none;
    background: #d6006e;
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    cursor: pointer;
}

.card__sacrifice-btn:hover {
    background: #a90057;
}
