/* Les tokens du design system « Yam's Orchard », REPRIS TELS QUELS depuis
   `module/frontend/.jvm/src/main/resources/index.html` du dépôt Scala.
   C'est du CSS, pas du Scala : le porter, c'est le copier — aucune traduction,
   donc aucune dérive possible entre les deux fronts tant qu'ils coexistent.

   Les composants ne lisent JAMAIS une couleur en dur, seulement `var(--color-*)`.
   C'est ce qui fait tenir le mode sombre en une ligne : `data-theme="dark"` sur
   <html> rebranche les mêmes noms sur d'autres valeurs, et rien d'autre ne bouge.
*/
:root {
  /* Primary (Mandarin) */
  --color-primary: #8c4a00; --color-primary-dim: #7b4000; --color-primary-container: #fd8b00;
  --color-on-primary: #fff0e7; --color-on-primary-container: #442100;
  --color-primary-fixed: #fd8b00; --color-primary-fixed-dim: #ea8000;
  --color-on-primary-fixed: #180800; --color-on-primary-fixed-variant: #512800;
  /* Secondary (Lemon) */
  --color-secondary: #615e00; --color-secondary-dim: #555200; --color-secondary-container: #f0e800;
  --color-on-secondary: #fff858; --color-on-secondary-container: #575400;
  --color-secondary-fixed: #f0e800; --color-secondary-fixed-dim: #e1da00;
  --color-on-secondary-fixed: #434100; --color-on-secondary-fixed-variant: #615e00;
  /* Tertiary (Fresh Mint) */
  --color-tertiary: #006944; --color-tertiary-dim: #005c3b; --color-tertiary-container: #00ffab;
  --color-on-tertiary: #c9ffde; --color-on-tertiary-container: #005c3b;
  --color-tertiary-fixed: #00ffab; --color-tertiary-fixed-dim: #00efa0;
  --color-on-tertiary-fixed: #00472d; --color-on-tertiary-fixed-variant: #006742;
  /* Error */
  --color-error: #b02500; --color-error-dim: #b92902; --color-error-container: #f95630;
  --color-on-error: #ffefec; --color-on-error-container: #520c00;
  /* Surfaces */
  --color-background: #f6f6f6; --color-on-background: #2d2f2f;
  --color-surface: #f6f6f6; --color-surface-dim: #d3d5d5; --color-surface-bright: #f6f6f6;
  --color-surface-container-lowest: #ffffff; --color-surface-container-low: #f0f1f1;
  --color-surface-container: #e7e8e8; --color-surface-container-high: #e1e3e3;
  --color-surface-container-highest: #dbdddd; --color-surface-variant: #dbdddd;
  --color-surface-tint: #8c4a00; --color-on-surface: #2d2f2f; --color-on-surface-variant: #5a5c5c;
  --color-outline: #767777; --color-outline-variant: #acadad;
  /* Inverse */
  --color-inverse-surface: #0c0f0f; --color-inverse-on-surface: #9c9d9d; --color-inverse-primary: #fd8b00;
  /* Ghost border (fallback accessibilité, jamais opaque) */
  --color-ghost-border: rgba(118,119,119,0.15); --color-ghost-border-primary: rgba(140,74,0,0.2);

  /* Espacements (base 4px) */
  --spacing-1: 0.25rem; --spacing-2: 0.5rem; --spacing-3: 0.75rem; --spacing-4: 1rem;
  --spacing-5: 1.5rem; --spacing-6: 2rem; --spacing-8: 3rem; --spacing-12: 4rem; --spacing-16: 5.5rem;
  /* Radii — rien sous sm, xl/full favorisés */
  --radius-sm: 0.5rem; --radius-default: 1rem; --radius-lg: 2rem; --radius-xl: 3rem; --radius-full: 9999px;
  /* Ombres « bubbly » ambiantes (teintées, jamais grises) */
  --shadow-tint: 45, 47, 47;
  --shadow-card: 0 10px 40px -10px rgba(var(--shadow-tint), 0.08);
  --shadow-float: 0 8px 30px rgba(var(--shadow-tint), 0.06);
  --shadow-pop: 0 15px 30px rgba(0,0,0,0.15);
  /* Gradient d'« inflation » tonale (CTA, hero) — tons fixed, invariants light/dark */
  --gradient-primary-inflate: linear-gradient(135deg, var(--color-primary-fixed) 0%, var(--color-primary-fixed-dim) 100%);

  /* Typographie — Plus Jakarta (display/headline/label) + Be Vietnam Pro (body) */
  --font-headline: 'Plus Jakarta Sans', sans-serif;
  --font-body: 'Be Vietnam Pro', sans-serif;
  --font-label: 'Plus Jakarta Sans', sans-serif;
  --text-display-lg: 800 3.5rem/1.0 var(--font-headline);
  --text-display-md: 800 2.75rem/1.05 var(--font-headline);
  --text-headline-lg: 800 2rem/1.1 var(--font-headline);
  --text-headline-md: 800 1.5rem/1.2 var(--font-headline);
  --text-headline-sm: 700 1.25rem/1.25 var(--font-headline);
  --tracking-display: -0.02em;
  --text-body-lg: 500 1.125rem/1.5 var(--font-body);
  --text-body-md: 400 1rem/1.5 var(--font-body);
  --text-body-sm: 400 0.875rem/1.4 var(--font-body);
  --text-label-lg: 700 1rem/1.2 var(--font-label);
  --text-label-md: 800 0.875rem/1.2 var(--font-label);
  --text-label-sm: 800 0.6875rem/1.2 var(--font-label);
  --tracking-label: 0.08em;
}

/* ===== Dark theme (M3 dark role mapping) — activer via document.documentElement.dataset.theme = 'dark' ===== */
[data-theme="dark"] {
  --color-primary: #ffb77a; --color-primary-dim: #ffcda0; --color-primary-container: #6b3600;
  --color-on-primary: #4a2800; --color-on-primary-container: #ffdcc2;
  --color-secondary: #d3cb00; --color-secondary-dim: #e8e04a; --color-secondary-container: #494700;
  --color-on-secondary: #333100; --color-on-secondary-container: #f5ee6e;
  --color-tertiary: #5ce6ab; --color-tertiary-dim: #83ffc7; --color-tertiary-container: #00512f;
  --color-on-tertiary: #00391f; --color-on-tertiary-container: #83ffc7;
  --color-error: #ffb4a0; --color-error-dim: #ff9c80; --color-error-container: #862000;
  --color-on-error: #5f1600; --color-on-error-container: #ffdad2;
  --color-background: #1a1c1b; --color-on-background: #e2e3e1;
  --color-surface: #1a1c1b; --color-surface-dim: #1a1c1b; --color-surface-bright: #40423f;
  --color-surface-container-lowest: #141615; --color-surface-container-low: #222423;
  --color-surface-container: #262827; --color-surface-container-high: #313332;
  --color-surface-container-highest: #3c3e3c; --color-surface-variant: #45483f;
  --color-surface-tint: #ffb77a; --color-on-surface: #e2e3e1; --color-on-surface-variant: #c5c6c4;
  --color-outline: #8f918e; --color-outline-variant: #45483f;
  --color-inverse-surface: #e2e3e1; --color-inverse-on-surface: #2d2f2f; --color-inverse-primary: #8c4a00;
  --color-ghost-border: rgba(197,198,196,0.16); --color-ghost-border-primary: rgba(255,183,122,0.22);
  /* Ombres = glow chaud (une ombre grise disparaîtrait sur near-black) */
  --shadow-tint: 0, 0, 0; --shadow-glow-tint: 255, 183, 122;
  --shadow-card: 0 10px 40px -8px rgba(var(--shadow-glow-tint), 0.14), 0 2px 10px rgba(0,0,0,0.4);
  --shadow-float: 0 8px 30px rgba(var(--shadow-glow-tint), 0.1), 0 2px 8px rgba(0,0,0,0.35);
  --shadow-pop: 0 15px 30px rgba(0,0,0,0.5);
}

/* ===== Atelier — le CHROME (fond de page + panneau latéral) suit le thème =====
   Le STAGE d'aperçu, lui, reste une vitrine sombre en toute circonstance (ses
   couleurs sont en dur dans `Scene`) : les VFX des dés en `mix-blend:screen` ne
   ressortent que sur fond sombre. Ces variables n'habillent donc QUE le pourtour
   et le panneau ; le texte du panneau lit les tokens `--color-on-surface*`, qui
   basculent déjà. */
:root {
  --atelier-fond: radial-gradient(ellipse at 38% 30%, #ffffff 0%, #e9efe9 62%);
  --atelier-panneau: rgba(255,255,255,0.72);
  --atelier-tuile: rgba(0,0,0,0.05);
  --atelier-bordure: rgba(0,0,0,0.1);
}
[data-theme="dark"] {
  --atelier-fond: radial-gradient(ellipse at 38% 30%, #113124 0%, #07100c 62%);
  --atelier-panneau: rgba(9,20,14,0.72);
  --atelier-tuile: rgba(255,255,255,0.05);
  --atelier-bordure: rgba(255,255,255,0.1);
}

/* Une icône occupe UN carré de 1em, police chargée ou non. Tant que la police
   des symboles n'est pas arrivée, la ligature s'affiche en toutes lettres
   (« storefront ») : sans boîte fixe, l'en-tête s'élargissait d'autant et
   sortait de l'écran d'un téléphone, puis sautait au chargement. */
.material-symbols-outlined {
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
  display: inline-block; width: 1em; overflow: hidden; white-space: nowrap;
}

/* CTA « hyper-tactile » : arête basse teintée + enfoncement au clic (les
   styles inline ne peuvent pas exprimer :active). Teinte fixed → invariante
   light/dark, comme le gradient du bouton. */
.yo-cta {
  box-shadow: 0 8px 0 0 var(--color-on-primary-fixed-variant), var(--shadow-pop);
  transition: transform .08s ease, box-shadow .08s ease;
}
.yo-cta:active {
  transform: translateY(6px);
  box-shadow: 0 2px 0 0 var(--color-on-primary-fixed-variant);
}

/* Dé 3D (Die3D) — cube CSS ; anims que les styles inline ne peuvent exprimer.
   Port du composant Die3D du design system : le DS anime en rAF (X/Y
   ≈ +19°/frame jitterés + lente vrille Z, puis settle JS vers la pose la
   plus proche). En CSS pur : roulis = keyframes linéaires *inégales* (le
   jitter) joué UNE fois et fini sur des multiples de 360° (≡ base), plus
   court que la phase Tumbling du pilote pour se poser sur la valeur AVANT la
   coupure (cf. `ds::de::MS_ROULIS_VISUEL`) ; pose = petit débord amorti
   (`animation-composition: add`) qui revient à zéro, d'amplitude sous-faciale.
   Durée surchargeable via `--yo-settle` : la table garde .5s, mais l'aperçu
   de l'atelier l'étire à la durée de l'éclat pour que le dé s'amortisse pendant
   tout le flash au lieu de se figer une fois posé. Le débord restant sous-facial,
   allonger ne change pas la face lue. */
.yo-die3d { position: relative; transform-style: preserve-3d; transition: transform .5s cubic-bezier(0.19,1,0.22,1); }
/* Additif, mais joué UNE fois (`forwards`) et sur .64s — plus court que la phase
   Tumbling du pilote (MS_ROULIS = .78s, cf. `ds::de::MS_ROULIS_VISUEL`). Il
   atteint donc son image finale (720°≡base, décalage nul) AVANT que le JS ne
   coupe la phase, et s'y fige : le dé se pose sur sa base (sa valeur) et l'y
   tient jusqu'à la pose. En `infinite` calé pile sur la coupure, le JS et le CSS
   n'étant pas synchronisés, la coupure tombait en pleine culbute, le décalage
   additif restant sautait, et l'avant-dernière image montrait une autre face. */
.yo-die3d.rolling { animation: yoDie3dTumble .64s linear forwards; animation-composition: add; }
/* La pose n'AJOUTE plus un tour complet (qui, coupé avant la fin, montrait une
   face au hasard) : juste un petit débord amorti qui revient à zéro, d'amplitude
   sous-faciale (≤12°), donc le dé lit toujours sa valeur, même si la pose est
   écourtée. */
.yo-die3d.settling { animation: yoDie3dSettle var(--yo-settle,.5s) cubic-bezier(0.19,1,0.22,1) forwards; animation-composition: add; }
@keyframes yoDie3dTumble {
  0%   { transform: rotateX(0deg)   rotateY(0deg)   rotateZ(0deg); }
  27%  { transform: rotateX(214deg) rotateY(158deg) rotateZ(10deg); }
  52%  { transform: rotateX(376deg) rotateY(402deg) rotateZ(-8deg); }
  78%  { transform: rotateX(562deg) rotateY(587deg) rotateZ(6deg); }
  100% { transform: rotateX(720deg) rotateY(720deg) rotateZ(0deg); }
}
@keyframes yoDie3dSettle {
  0%   { transform: rotateX(0deg)   rotateY(0deg); }
  45%  { transform: rotateX(-12deg) rotateY(-9deg); }
  100% { transform: rotateX(0deg)   rotateY(0deg); }
}
/* La PIERRE (forme Rounded) est plate et ronde : au lancer elle se RETOURNE sur
   un seul axe comme une pièce jetée, au lieu de culbuter comme un cube. Même
   recette que le cube : roulis additif joué une fois sur .64s (fin à 720°≡base
   AVANT la coupure de phase), pose = petit débord amorti qui revient à zéro. */
.yo-die3d.rolling-piece { animation: yoDie3dCoinTumble .64s linear forwards; animation-composition: add; }
.yo-die3d.settling-piece { animation: yoDie3dCoinSettle var(--yo-settle,.5s) cubic-bezier(0.19,1,0.22,1) forwards; animation-composition: add; }
@keyframes yoDie3dCoinTumble { from { transform: rotateX(0deg); } to { transform: rotateX(720deg); } }
@keyframes yoDie3dCoinSettle { 0% { transform: rotateX(0deg); } 45% { transform: rotateX(-16deg); } 100% { transform: rotateX(0deg); } }
/* La GEMME (forme Gem) est une taille brillante : au lancer elle TOURNE sur son
   axe vertical comme un diamant qui scintille, au lieu de culbuter. Additif sur
   la transform de repos (rotateX(-18deg) posée inline), même recette que le cube :
   spin joué une fois sur .64s (fin à 720°≡base avant la coupure), pose amortie. */
.yo-die3d.rolling-gem { animation: yoDie3dGemSpin .64s linear forwards; animation-composition: add; }
.yo-die3d.settling-gem { animation: yoDie3dGemSettle var(--yo-settle,.5s) cubic-bezier(0.19,1,0.22,1) forwards; animation-composition: add; }
@keyframes yoDie3dGemSpin { from { transform: rotateY(0deg); } to { transform: rotateY(720deg); } }
@keyframes yoDie3dGemSettle { 0% { transform: rotateY(0deg); } 45% { transform: rotateY(-16deg); } 100% { transform: rotateY(0deg); } }
/* Le CRISTAL (forme Crystal) est un prisme dressé : au lancer il culbute en
   TOURNOYANT — spin vertical (rotateY) ET bascule (rotateX) combinés, distinct
   du pur spin de la gemme. Additif sur la pose de repos, joué une fois sur .64s
   (fin à des multiples de 360° ≡ base avant la coupure), pose amortie. */
.yo-die3d.rolling-crystal { animation: yoDie3dCrystalTumble .64s linear forwards; animation-composition: add; }
.yo-die3d.settling-crystal { animation: yoDie3dCrystalSettle var(--yo-settle,.5s) cubic-bezier(0.19,1,0.22,1) forwards; animation-composition: add; }
@keyframes yoDie3dCrystalTumble { from { transform: rotateY(0deg) rotateX(0deg); } to { transform: rotateY(720deg) rotateX(360deg); } }
@keyframes yoDie3dCrystalSettle { 0% { transform: rotateY(0deg) rotateX(0deg); } 45% { transform: rotateY(-14deg) rotateX(-10deg); } 100% { transform: rotateY(0deg) rotateX(0deg); } }
/* Le CRÂNE (forme Skull) est une médaille plate : au lancer il GRELOTTE — une
   secousse rapide en rotateZ, comme un crâne qui ricane — puis se pose droit en
   oscillation amortie. Pas de composition additive : la secousse EST la transform
   (la pose de repos du crâne est nulle). */
.yo-die3d.rolling-skull { animation: yoDie3dSkullRattle .14s ease-in-out infinite; }
.yo-die3d.settling-skull { animation: yoDie3dSkullSettle var(--yo-settle,.5s) ease-out forwards; }
@keyframes yoDie3dSkullRattle { 0%,100% { transform: rotate(-13deg); } 50% { transform: rotate(13deg); } }
@keyframes yoDie3dSkullSettle { 0% { transform: rotate(-11deg); } 30% { transform: rotate(7deg); } 55% { transform: rotate(-4deg); } 78% { transform: rotate(2deg); } 100% { transform: rotate(0deg); } }
/* Rebond du dé entier pendant le roulis + flash d'impact à la pose (DS). */
@keyframes yoDie3dBounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-18%) rotate(4deg); } }
@keyframes yoDie3dImpact { 0% { transform: scale(0.5); opacity: 1; } 100% { transform: scale(1.6); opacity: 0; } }
/* Animations de REPOS d'un thème de dés (`DiceAnimation`) : elles jouent sur le
   conteneur, jamais sur le cube — celui-ci porte déjà la transform de sa face
   de repos, et l'animer ferait tourner un dé qui n'affiche plus le bon chiffre. */
@keyframes yoDieIdleBounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-9%); } }
@keyframes yoDieIdleSpin   { to { transform: rotate(360deg); } }
@keyframes yoDieIdleWobble { 0%,100% { transform: rotate(-5deg); } 50% { transform: rotate(5deg); } }

@keyframes yoDie3dAura { 0%,100% { box-shadow: 0 0 20px var(--color-primary-fixed-glow, rgba(253,139,0,0.55)); } 50% { box-shadow: 0 0 38px var(--color-primary-fixed); } }

/* Accessibilité : qui demande moins de mouvement n'a AUCUNE animation de dé —
   ni roulis, ni pose, ni flash, ni idle de thème, ni aura de dé retenu. La
   face reste lisible : le cube porte déjà sa transform de repos inline, donc
   couper l'animation le fige sur le bon chiffre. `!important` bat les
   `animation:` inline posés par le composant. */
@media (prefers-reduced-motion: reduce) {
  .yo-de, .yo-die3d, .yo-die3d.rolling, .yo-die3d.settling,
  .yo-die3d.rolling-piece, .yo-die3d.settling-piece,
  .yo-die3d.rolling-gem, .yo-die3d.settling-gem,
  .yo-die3d.rolling-crystal, .yo-die3d.settling-crystal,
  .yo-die3d.rolling-skull, .yo-die3d.settling-skull, .yo-de-face, .yo-de-flash {
    animation: none !important;
  }
}

/* Flottement des dés (accueil hero + vitrine auth) & pulses — portés depuis les
   maquettes Claude Design (Page d'accueil / Auth). */
@keyframes yoFloat  { 0%,100% { transform: translateY(0) rotate(-8deg); } 50% { transform: translateY(-14px) rotate(-2deg); } }
@keyframes yoFloat2 { 0%,100% { transform: translateY(0) rotate(10deg); } 50% { transform: translateY(-18px) rotate(4deg); } }
@keyframes yoBlink  { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
/* Le « +X » d'un gain de score : il apparaît, s'envole et s'efface. */
/* Le « +X » du score : il JAILLIT d'en bas, gros, puis rétrécit en s'élevant et
   s'efface — sur 2 s pour qu'on le voie vraiment (la police de départ est déjà
   grosse côté rail ; ici c'est le `scale` qui l'enfle puis le dégonfle). */
/* L'annonce d'une marque au centre de la table, et la case qu'elle remplit
   qui clignote dans la feuille. */
/* Les dés vierges qui ondulent avant le premier lancer, à mon tour : une
   invitation à lancer. Coupée si le système demande moins d'animations. */
@keyframes yoVague { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-10px); } }
@media (prefers-reduced-motion: reduce) { .yo-de-attente { animation:none !important; } }
@keyframes yoAnnonce { 0% { opacity:0; transform:translate(-50%,-50%) scale(.85); } 100% { opacity:1; transform:translate(-50%,-50%) scale(1); } }
@keyframes yoCaseMarquee { 0%,100% { box-shadow:none; } 15%,45%,75% { box-shadow:0 0 0 3px var(--color-tertiary-fixed-dim, #7bdcb0); } 30%,60% { box-shadow:0 0 0 1px var(--color-tertiary-fixed-dim, #7bdcb0); } }
@keyframes yoScoreGain {
  0%   { opacity: 0; transform: translateY(20px) scale(1.35); }
  14%  { opacity: 1; transform: translateY(8px) scale(1.3); }
  35%  { opacity: 1; transform: translateY(-2px) scale(1); }
  100% { opacity: 0; transform: translateY(-30px) scale(.72); }
}
/* Le GIGOTEMENT d'un dé qu'on peut lancer (profil) : un petit sursaut par
   `top` — jamais par `transform`, cf. `yoFlotteA`. Il s'arrête sous la souris :
   le dé qu'on vise ne se dérobe pas. `position:relative` requis. */
.yo-gigote { position: relative; animation: yoGigote 3.6s ease-in-out infinite; }
.yo-gigote:hover { animation-play-state: paused; }
@keyframes yoGigote { 0%,70%,100% { top: 0; } 76% { top: -6px; } 82% { top: 0; } 88% { top: -3px; } 94% { top: 0; } }
@media (prefers-reduced-motion: reduce) { .yo-gigote { animation: none; } }

/* Le flottement des CUBES 3D (vitrine de connexion) : par `top`, pas par
   `transform`. Un ancêtre à transform ANIMÉE fait masquer, sous certains
   moteurs (Firefox accéléré), les faces `backface-visibility:hidden` du cube :
   le dé devenait invisible. Élément `position:relative` requis. */
@keyframes yoFlotteA { 0%,100% { top: 0; } 50% { top: -14px; } }
@keyframes yoFlotteB { 0%,100% { top: -8px; } 50% { top: -26px; } }
@keyframes yoFlotteC { 0%,100% { top: -3px; } 50% { top: -19px; } }
@keyframes yoGlowPulse { 0%,100% { opacity: .5; } 50% { opacity: .85; } }

/* Bulle de réaction de table : elle ARRIVE, elle ne clignote pas. Une entrée
   courte suffit — la disparition est décidée côté Rust (une minuterie retire la
   bulle), pas par une fin d'animation qu'un onglet en arrière-plan ne jouerait
   jamais. */
@keyframes yoReactionPop { from { transform: translateY(6px) scale(.85); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }

/* Le glissé des dés du rejeu à la passation est joué à l'impérative par la Web
   Animations API (`front/src/rejeu.rs`), pas par un `@keyframes` : Leptos
   réconcilie le nœud en place et une animation CSS ne repartirait pas. */

/* Cartes qui se soulèvent au survol (accueil, boutique). */
.yo-lift { transition: transform .18s ease, box-shadow .18s ease; }
.yo-lift:hover { transform: scale(1.02); box-shadow: 0 22px 48px -12px rgba(140,74,0,.24) !important; }

/* Atelier de dés — aperçu de lancer + burst VFX (façon MOBA). */
@keyframes yoSpin { to { transform: rotate(360deg); } }
@keyframes yoStageGlow { 0%,100% { opacity:.55; } 50% { opacity:.92; } }
@keyframes yoFlash { 0% { opacity:0; } 12% { opacity:1; } 100% { opacity:0; } }
/* ── Éclats d'impact (VFX) — Braise, Lumière, Foudre, Ras de marée. ──────────
   Chaque effet a DEUX couches (derrière/devant les dés) bâties en chaînes HTML
   déterministes côté Rust. Les éléments sont centrés sur leur point par un
   `margin` négatif : ces keyframes animent donc scale/rotate/opacity autour de
   ce centre (pas de `translate(-50%,-50%)` qui doublerait le décalage), plus des
   translations de MOUVEMENT explicites — particules vers `--dx`/`--dy`, feu et
   vague qui montent, crête qui balaie. */

/* Communs à plusieurs effets : impact, ondes, particules, scintillement. */
@keyframes yoDeton { 0% { transform:scale(0); opacity:1; } 9% { transform:scale(1.15); opacity:1; } 45% { transform:scale(.92); } 100% { transform:scale(1.5); opacity:0; } }
@keyframes yoBoom { 0% { opacity:0; } 2% { opacity:.9; } 12% { opacity:0; } 100% { opacity:0; } }
@keyframes yoVflash { 0% { opacity:0; } 3% { opacity:1; } 22% { opacity:0; } 100% { opacity:0; } }
@keyframes yoShock { 0% { transform:scale(.05); opacity:0; } 8% { opacity:1; } 100% { transform:scale(2.8); opacity:0; } }
@keyframes yoShockG { 0% { transform:scale(.05); opacity:0; } 6% { opacity:1; } 100% { transform:scale(3.4); opacity:0; } }
@keyframes yoGlint { 0% { transform:scale(0) rotate(0); opacity:0; } 12% { transform:scale(1.1); opacity:1; } 100% { transform:scale(.2) rotate(35deg); opacity:0; } }
@keyframes yoJet { 0% { transform:translate(0,0) scale(1); opacity:0; } 8% { opacity:1; } 100% { transform:translate(var(--dx),var(--dy)) scale(.15); opacity:0; } }
@keyframes yoBreathe { 0%,100% { opacity:.45; transform:scale(.97); } 50% { opacity:.95; transform:scale(1.05); } }
@keyframes yoHeat { 0% { transform:scaleX(.35); opacity:0; } 30% { opacity:1; } 100% { transform:scaleX(1.2); opacity:0; } }
@keyframes yoRays { to { transform:rotate(360deg); } }

/* Foudre — les branches CLIGNOTENT (électricité). */
@keyframes yoFlick { 0% { opacity:0; } 4% { opacity:1; } 10% { opacity:.15; } 16% { opacity:1; } 26% { opacity:.4; } 36% { opacity:1; } 52% { opacity:.6; } 68% { opacity:.95; } 84% { opacity:.5; } 100% { opacity:0; } }

/* Braise — colonnes de feu qui montent, ondulent, fument. */
@keyframes yoFrise { 0% { transform:scaleY(.05) scaleX(.5); opacity:0; } 12% { opacity:1; } 50% { transform:scaleY(1) scaleX(1); } 72% { opacity:1; } 100% { transform:scaleY(1.08) scaleX(.85); opacity:0; } }
@keyframes yoFwob { 0%,100% { transform:skewX(-4deg) scaleX(1); } 50% { transform:skewX(5deg) scaleX(1.06); } }
@keyframes yoFswirl { from { background-position:0 0, 0 0; } to { background-position:0 -140px, 0 0; } }
@keyframes yoFumee { 0% { transform:translateY(0) scale(.4) rotate(0); opacity:0; } 25% { opacity:.6; } 100% { transform:translateY(-130px) scale(2) rotate(30deg); opacity:0; } }

/* Ras de marée — la vague MONTE puis REDESCEND ; la crête d'écume balaie la
   rangée ; `yoRideMaree` fait suivre les dés à la même courbe (ils surfent). */
@keyframes yoDeferl { 0% { transform:translateY(120px) scaleY(.1); opacity:0; } 18% { opacity:1; } 44% { transform:translateY(0) scaleY(1); } 66% { transform:translateY(0) scaleY(1); opacity:1; } 100% { transform:translateY(130px) scaleY(.2); opacity:0; } }
@keyframes yoCrete { 0% { transform:translateX(-300px); opacity:0; } 22% { opacity:1; } 45% { transform:translateX(0); } 80% { opacity:1; transform:translateX(300px); } 100% { opacity:0; transform:translateX(300px); } }
@keyframes yoRideMaree { 0% { transform:translateY(0); } 44% { transform:translateY(-26px); } 66% { transform:translateY(-26px); } 100% { transform:translateY(0); } }

/* Pluie d'emojis & confettis — chaque pièce TOMBE du haut en tournant, et
   S'ÉTEINT en douceur sur son dernier tiers (au lieu de disparaître net). */
@keyframes yoPluie { 0% { transform:translateY(0) rotate(0); opacity:0; } 8% { opacity:1; } 62% { opacity:1; } 100% { transform:translateY(560px) rotate(var(--rot)); opacity:0; } }
/* La lueur de fond de la fête : monte, tient, puis s'efface (pas d'`infinite`
   qui serait coupée net à la fin de l'éclat). */
@keyframes yoLueur { 0% { opacity:0; transform:scale(.85); } 14% { opacity:.9; } 55% { opacity:.8; } 100% { opacity:0; transform:scale(1.1); } }
@keyframes yoPop { 0% { transform:translate(-50%,-50%) scale(.3) rotate(-6deg); opacity:0; } 40% { transform:translate(-50%,-50%) scale(1.16) rotate(2deg); opacity:1; } 68% { transform:translate(-50%,-50%) scale(.97) rotate(0); } 100% { transform:translate(-50%,-50%) scale(1.04); opacity:0; } }
/* Les effets « shiny » haut de gamme (Shiny, Holo). */
@keyframes yoShinyStar { 0% { transform:scale(0) rotate(0); opacity:0; } 30% { transform:scale(1.25) rotate(45deg); opacity:1; } 60% { transform:scale(.9) rotate(70deg); opacity:1; } 100% { transform:scale(0) rotate(110deg); opacity:0; } }
@keyframes yoHoloBalaye { 0% { transform:translateX(-470px) skewX(-16deg); opacity:0; } 12% { opacity:1; } 88% { opacity:1; } 100% { transform:translateX(470px) skewX(-16deg); opacity:0; } }
/* Et les dés eux-mêmes qui prennent la lumière, le temps de l'éclat. */
/* Shiny : les dés FLASHENT blanc et or, en scintillements (pas d'arc-en-ciel). */
@keyframes yoShinyDes { 0%,100% { filter:none; } 6% { filter:brightness(1.9) drop-shadow(0 0 22px #fff); } 18% { filter:brightness(1.2) drop-shadow(0 0 14px #ffd24a); } 34% { filter:brightness(1.7) drop-shadow(0 0 20px #fff6c8); } 50% { filter:brightness(1.15) drop-shadow(0 0 12px #ffc83a); } 68% { filter:brightness(1.6) drop-shadow(0 0 18px #fff); } 84% { filter:brightness(1.1) drop-shadow(0 0 10px #ffd24a); } }
@keyframes yoHoloDes { 0%,100% { filter:none; } 15% { filter:saturate(1.8) brightness(1.3) drop-shadow(0 0 14px #5ce1ff); } 45% { filter:saturate(1.8) brightness(1.3) drop-shadow(0 0 14px #ff5cf0); } 75% { filter:saturate(1.8) brightness(1.3) drop-shadow(0 0 14px #ffd24a); } }
/* Le thème des bots (Circuit) : la LED qui respire — jamais sous 85 % pour
   garder la valeur lisible — et l'éclat Glitch. */
@keyframes yoLedPulse { 0%,100% { opacity:1; box-shadow:0 0 7px 1px rgba(56,242,255,.8); } 50% { opacity:.85; box-shadow:0 0 3px 0 rgba(56,242,255,.45); } }
@keyframes yoGlitchDes { 0%,100% { filter:none; transform:none; } 8% { filter:drop-shadow(-4px 0 #ff2bd6) drop-shadow(4px 0 #38f2ff); transform:translateX(3px); } 14% { filter:none; transform:translateX(-2px); } 22% { filter:drop-shadow(3px 0 #ff2bd6) drop-shadow(-3px 0 #38f2ff); transform:translate(-4px,1px) skewX(-4deg); } 30% { filter:none; transform:none; } 46% { filter:drop-shadow(-6px 0 #ff2bd6) drop-shadow(6px 0 #38f2ff); transform:translateX(5px); } 52% { filter:none; transform:none; } 70% { filter:drop-shadow(2px 0 #ff2bd6) drop-shadow(-2px 0 #38f2ff); transform:translateX(-2px) skewX(3deg); } 76% { filter:none; transform:none; } }
@keyframes yoGlitchScan { 0%,100% { opacity:0; } 6% { opacity:.9; } 18% { opacity:.3; } 30% { opacity:.8; } 52% { opacity:.2; } 64% { opacity:.7; } 84% { opacity:.15; } }
@keyframes yoGlitchBloc { 0%,100% { opacity:0; transform:none; } 10% { opacity:1; } 35% { transform:translateX(var(--gx)); } 55% { opacity:0; } 65% { opacity:1; transform:translateX(0); } 85% { opacity:0; } }
@keyframes yoGlitchTear { 0%,100% { opacity:0; transform:none; } 20% { opacity:1; transform:translateX(var(--tx)); } 60% { opacity:1; transform:translateX(calc(var(--tx) * -.6)); } }
@keyframes yoGlitchBalai { 0% { transform:translateY(-120px); opacity:0; } 10% { opacity:1; } 90% { opacity:1; } 100% { transform:translateY(120px); opacity:0; } }
@keyframes yoShake { 0%,100% { transform:translate(0,0); } 15% { transform:translate(-7px,4px); } 35% { transform:translate(6px,-5px); } 55% { transform:translate(-5px,-3px); } 75% { transform:translate(4px,3px); } }
@keyframes yoShakeBig { 0%,100% { transform:translate(0,0) rotate(0); } 12% { transform:translate(-13px,7px) rotate(-.6deg); } 30% { transform:translate(12px,-8px) rotate(.6deg); } 50% { transform:translate(-10px,-5px) rotate(-.4deg); } 70% { transform:translate(8px,6px) rotate(.4deg); } 88% { transform:translate(-4px,3px) rotate(0); } }
/* L'impact d'un VFX à la table : une lueur colorée qui gonfle et s'éteint quand
   les dés se posent — la version contenue de l'éclat de l'atelier. */
@keyframes yoImpactBloom { 0% { transform:translate(-50%,-50%) scale(.4); opacity:0; } 25% { opacity:.9; } 100% { transform:translate(-50%,-50%) scale(1.7); opacity:0; } }

/* Matchmaking 2A — recherche, salon, reveal versus. */
@keyframes yoPulseRing { 0% { transform:scale(.55); opacity:.55; } 100% { transform:scale(1.5); opacity:0; } }
@keyframes yoPulseChip { 0%,100% { transform:translate(-50%,0) scale(1); } 50% { transform:translate(-50%,0) scale(1.06); } }
@keyframes yoInL { 0% { transform:translateX(-60px); opacity:0; } 100% { transform:translateX(0); opacity:1; } }
@keyframes yoInR { 0% { transform:translateX(60px); opacity:0; } 100% { transform:translateX(0); opacity:1; } }
@keyframes yoVsPop { 0% { transform:translate(-50%,-50%) scale(0) rotate(-18deg); } 68% { transform:translate(-50%,-50%) scale(1.16) rotate(5deg); } 100% { transform:translate(-50%,-50%) scale(1) rotate(0); } }
@keyframes yoUp { 0% { transform:translateY(20px); opacity:0; } 100% { transform:translateY(0); opacity:1; } }
