/* ── style.css — refondu sur tokens.css ───────────────────────────────────── */
@import url('tokens.css?v=2');

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  /* iOS Safari régression : `overflow-x: clip` parfois ignoré quand un descendant
     a une transform/translateX qui dépasse. Doubler avec hidden (les deux ne
     créent pas de scroll-y grâce à overflow-y: hidden explicite). position:
     relative pour bien établir le contexte de containment. !important pour
     bloquer toute règle plus tardive qui voudrait restaurer le scroll. */
  position: relative;
  max-width: 100%;
  width: 100%;
  overflow-x: hidden !important;
  overflow-y: hidden;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
}

body::before {
  content: "";
  position: fixed; inset: 0;
  pointer-events: none;
  background-image: radial-gradient(circle, rgba(10,10,10,0.06) 1px, transparent 1px);
  background-size: 22px 22px;
  z-index: 0;
}

.app {
  position: relative; z-index: 1;
  max-width: 1180px;
  margin: 0 auto;
  padding: 20px 28px 16px;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ── Header ──────────────────────────────────────────────────────────────── */
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  gap: var(--space-3);
}

.header-actions { display: flex; gap: var(--space-2); align-items: center; }

.btn-collection, .btn-nav {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  padding: 9px 16px;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  flex-shrink: 0;
  white-space: nowrap;
  cursor: pointer;
}
.btn-collection:focus-visible,
.btn-nav:focus-visible {
  transform: translate(-2px, -2px);
  box-shadow: 5px 5px 0 0 var(--ink);
}
html.dlp-can-hover .btn-collection:hover,
html.dlp-can-hover .btn-nav:hover {
  transform: translate(-2px, -2px);
  box-shadow: 5px 5px 0 0 var(--ink);
}
.btn-nav .ico { display: inline-flex; }
/* Bouton icône seule (ex : paramètres rouage) : carré, sans texte */
.btn-nav.btn-icon-only {
  padding: 9px 11px;
  font-family: inherit;
}

.site-title {
  font-family: var(--font-display);
  font-size: clamp(18px, 3.8vw, 44px);
  line-height: 1;
  letter-spacing: -0.02em;
  white-space: nowrap;
  flex: 1 1 0;
  /* min-width: 0 indispensable sinon flex items refusent de shrink en dessous
     de leur largeur de contenu (par défaut min-width: auto). Pas d'overflow
     hidden (tronquait l'emoji avec son drop-shadow vertical). */
  min-width: 0;
  display: flex;
  align-items: center;
}
/* ── Réduire les animations (accessibilité, opt-in via paramètres) ──────
   Anti-pattern historique : tuer toutes les animations/transitions à 1ms
   rendait l'expérience SACCADÉE (perte du smooth essentiel — modal flip,
   reveal, swipe carte). Nouveau modèle : on désactive UNIQUEMENT les
   effets de mouvement-écran (motion sickness) + les loops infinis bavards.
   Les transitions hover, les anims contenues (cartes, modales, toasts,
   combo, reveal) restent intactes. Règles dans la section Juice (bas du
   fichier) pour shake/flash/punch/wobble, et ci-dessous pour les loops. */
html.reduce-motion .hint-postit-wrap,
html.reduce-motion .settings-donate,
html.reduce-motion .rare-explain-card,
html.reduce-motion .rare-explain-card::before,
html.reduce-motion .pop-sector.armed-pulse {
  animation: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   MENU PARAMÈTRES — refonte en blocs néobrut colorés
   ────────────────────────────────────────────────────────────────────────── */

/* Modal principal ─────────────────────────────────────────────────────── */
.settings-backdrop {
  position: fixed;
  inset: 0;
  z-index: 600;
  background: rgba(10,10,10,0.7);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
}
.settings-backdrop.show { display: flex; }
.settings-modal {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  max-width: 540px;
  width: 100%;
  max-height: 92vh;
  overflow: hidden;        /* le scroll est délégué à .settings-scroll */
  display: flex;
  flex-direction: column;
}
.settings-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-thick);
  background: var(--yellow);
  flex-shrink: 0;          /* header ne se compresse pas */
}
/* Wrapper interne qui scrolle — évite le bug "header sticky qui détache
   au sur-défilement" dans un container flex. */
.settings-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;  /* coupe la chaîne de scroll vers la page */
}
.settings-title {
  font-family: var(--font-display);
  font-size: clamp(20px, 3vw, 28px);
  text-transform: uppercase;
  letter-spacing: -0.01em;
  margin: 0;
}
.settings-close,
.feedback-close,
.feedback-back {
  width: 36px;
  height: 36px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  font-family: var(--font-display);
  font-size: 16px;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .settings-close:hover,
html.dlp-can-hover .feedback-close:hover,
html.dlp-can-hover .feedback-back:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 0 var(--ink); }

.settings-body {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Bloc — carte néobrut colorée ────────────────────────────────────────── */
.settings-block {
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  background: var(--paper);
  display: flex;
  flex-direction: column;
}
.settings-block-head {
  padding: 8px 14px;
  border-bottom: var(--border-thick);
  background: var(--ink);
  color: var(--paper);
}
.settings-block-title {
  font-family: var(--font-display);
  font-size: clamp(13px, 1.4vw, 15px);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0;
  line-height: 1.1;
}
.settings-block-body {
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Variantes de fond par bloc — pastel doux qui évoque la fonction */
.settings-block--player   { background: #d8f3dc; }   /* vert pastel — identité */
.settings-block--prefs    { background: #ffe9c2; }   /* sable — préférences */
.settings-block--data     { background: #d6e0ff; }   /* bleu pastel — utilitaire */
.settings-block--feedback { background: #ead4ff; }   /* violet pastel — créatif */
.settings-block--donate   { padding: 0; border: 0; box-shadow: none; background: transparent; }

/* Champs — label + input/toggle ──────────────────────────────────────── */
.settings-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.settings-field--toggle {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.settings-label {
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1;
}
.settings-input {
  font-family: var(--font-body);
  font-size: 15px;
  padding: 8px 10px;
  border: var(--border-thick);
  background: var(--paper);
  outline: none;
  width: 100%;
  box-sizing: border-box;
}
.settings-input:focus {
  background: var(--white);
  box-shadow: var(--sh-sm);
}

/* Toggle néobrut (checkbox stylée) ───────────────────────────────────── */
.settings-toggle {
  appearance: none;
  -webkit-appearance: none;
  box-sizing: border-box;
  width: 52px;
  height: 28px;
  background: var(--paper);
  border: var(--border-thick);
  position: relative;
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--dur-fast);
  margin: 0;
}
.settings-toggle::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 4px;
  width: 14px;
  height: 14px;
  background: var(--ink);
  transform: translateY(-50%);
  transition: left var(--dur-fast);
}
.settings-toggle:checked { background: var(--green); }
.settings-toggle:checked::after { left: calc(100% - 18px); }

/* Slider full-card néobrut (Musique, Sons) ─────────────────────────────
   Pattern : <input type="range"> en overlay absolu sur toute la card, thumb
   et track invisibles. La card elle-même affiche une fill bar via ::before
   gradient (largeur = --fill-pct, valeur 0-100 mise à jour par JS au input).
   Le user peut tap/drag n'importe où sur la card pour ajuster le volume —
   c'est la card entière qui sert de slider, plus de widget réduit à droite.
   Pas de data-tip ici (volume self-explanatory). */
.settings-pref-card--slider {
  position: relative;
  overflow: hidden;
  cursor: ew-resize;
  grid-template-columns: auto 1fr auto;  /* emoji | label | value */
}
.settings-pref-card--slider::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--fill-pct, 60%);
  background: linear-gradient(90deg, rgba(45, 165, 103, 0.22), rgba(45, 165, 103, 0.32));
  border-right: 2px solid var(--ink);
  pointer-events: none;
  z-index: 0;
  transition: width 80ms linear;
}
.settings-pref-card--slider > * {
  position: relative;
  z-index: 2;  /* emoji + label + value au-dessus du fill */
}
.settings-pref-slider-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  background: transparent;
  appearance: none;
  -webkit-appearance: none;
  cursor: ew-resize;
  z-index: 3;  /* au-dessus de tout pour capturer touch/click */
  opacity: 0.01;  /* visible pour AT/keyboard mais invisible visuellement */
  /* Mobile : indique au navigateur que le touch est pour un drag horizontal.
     Sans ça, le browser interprète le touch comme un scroll vertical et le
     drag du slider ne fonctionne pas (avant : seul le tap-to-jump marchait). */
  touch-action: pan-x;
}
.settings-pref-slider-overlay::-webkit-slider-thumb {
  -webkit-appearance: none;
  /* Hit area large pour le touch drag mobile (28×28 = recommandation
     Apple HIG 44px minimum / Material 24px). Visuel invisible via
     background transparent — l'overlay a opacity 0.01 globalement. */
  width: 28px;
  height: 28px;
  background: transparent;
  border: 0;
  cursor: ew-resize;
}
.settings-pref-slider-overlay::-moz-range-thumb {
  width: 28px;
  height: 28px;
  background: transparent;
  border: 0;
  cursor: ew-resize;
}
.settings-pref-slider-overlay::-webkit-slider-runnable-track {
  background: transparent;
  border: 0;
}
.settings-pref-slider-overlay::-moz-range-track {
  background: transparent;
  border: 0;
}
.settings-pref-slider-value {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  font-weight: 600;
  min-width: 42px;
  text-align: right;
}

/* Préférences en grid 2-col compactée ────────────────────────────────── */
.settings-prefs-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding: 14px;
}
.settings-pref-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--paper);
  border: var(--border-thick);
  cursor: pointer;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .settings-pref-card:hover { transform: translate(-1px, -1px); box-shadow: var(--sh-sm); }
.settings-pref-emoji { font-size: 20px; line-height: 1; }
.settings-pref-label {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
}
/* Variante "wide" : prend toute la largeur de la grille parente (2 col).
   Layout 1-row identique aux cards small : emoji + label + control. Pas
   de description inline — la desc longue est affichée en tooltip via
   data-tip (cf. .settings-pref-card[data-tip] plus bas + pills-anim.js).
   Permet le layout : Son + Tirage (small) row 1, Animations (--wide) row 2. */
.settings-pref-card--wide {
  grid-column: 1 / -1;
}

/* ─── Tooltip data-tip pour les cards prefs ───────────────────────────────
   Description longue affichée :
     - au HOVER sur desktop (@media hover: hover)
     - au TAP sur la card en mobile (classe .show-tip posée par JS, hors zone
       toggle/picker — handler dans pills-anim.js / settings.js)
   Position : sous la card, s'étend sur toute sa largeur. Background ink
   inversé (paper texte) pour contraste fort. */
.settings-pref-card[data-tip] {
  position: relative;
  cursor: help;
}
/* IMPORTANT : lift z-index sur la card elle-même au hover/show-tip pour
   créer un nouveau stacking context. Sans ça, le ::after (z-index 100)
   reste empilé dans le contexte parent où la card Animations (row 2)
   peint après Son/Tirage (row 1) → masque le tooltip. Avec z-index sur
   la card, son ::after passe collectivement au-dessus des cards
   siblings. */
html.dlp-can-hover .settings-pref-card[data-tip]:hover { z-index: 50; }
.settings-pref-card[data-tip].show-tip { z-index: 50; }
.settings-pref-card[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--ink);
  color: var(--paper);
  padding: 8px 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.01em;
  line-height: 1.45;
  border: 2px solid var(--ink);
  box-shadow: var(--sh-sm);
  white-space: normal;
  text-align: left;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 180ms ease, transform 180ms var(--ease-out), visibility 180ms;
  pointer-events: none;
}
html.dlp-can-hover .settings-pref-card[data-tip]:hover::after {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.settings-pref-card[data-tip].show-tip::after {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
@media (max-width: 420px) {
  .settings-prefs-grid { grid-template-columns: 1fr; }
}

/* Picker 3 niveaux pour le réglage Animations (settings).
   Segmented control compact : un seul pill néobrut avec 3 segments
   internes séparés par border-left. Empreinte visuelle alignée sur le
   .settings-toggle voisin (~28px de haut) pour cohérence avec les
   autres prefs (Son, Tirage). Pas de hover lift individuel — c'est UN
   bloc, pas 3 boutons séparés. */
.settings-anim-picker {
  display: inline-flex;
  align-items: stretch;
  background: var(--paper);
  border: var(--border-thick);
  height: 28px;             /* match .settings-toggle height pour cohérence visuelle */
  flex-shrink: 0;
  overflow: hidden;
  box-sizing: border-box;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
/* Card Animations : picker en row 2 sur toute la largeur (tap-friendly mobile).
   Layout 2 lignes : row 1 = emoji + label, row 2 = picker full-width. Garde
   le data-tip infobox au tap sur la card (hors picker). */
.settings-pref-card--animations {
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  row-gap: 8px;
  align-items: center;
}
.settings-pref-card--animations .settings-anim-picker {
  grid-column: 1 / -1;
  width: 100%;
  height: 36px;  /* un peu plus haut pour tap-friendly mobile */
}
.settings-pref-card--animations .settings-anim-picker button {
  flex: 1 1 0;           /* 3-way equal split */
  justify-content: center;
  padding: 0 4px;
  font-size: 11px;
}
.settings-anim-picker::-webkit-scrollbar { display: none; }
.settings-anim-picker button {
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: transparent;
  border: none;
  padding: 0 10px;
  cursor: pointer;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  transition: background-color 180ms ease, color 180ms ease;
  /* Empêche les boutons de se shrinker / wraper : si le picker doit
     dépasser, c'est le container qui scrolle. Chaque bouton garde sa
     largeur intrinsèque (= label + padding). */
  flex-shrink: 0;
  white-space: nowrap;
}
.settings-anim-picker button + button {
  border-left: var(--border-thick);
}
html.dlp-can-hover .settings-anim-picker button:hover:not(.active) { background: rgba(10, 10, 10, 0.06); }
.settings-anim-picker button.active {
  background: var(--ink);
  color: var(--paper);
}

/* Hints + Actions ────────────────────────────────────────────────────── */
.settings-hint {
  font-family: var(--font-mono);
  font-size: 11px;
  opacity: 0.75;
  line-height: 1.45;
  margin: 0;
}
.settings-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Trio Classements (toggle full row 1 + 2 buttons row 2). Layout TOUJOURS
   stacké : la modal a max-width 540px → un layout 3-col (toggle + 2 btns
   sur 1 ligne) cause un overflow du bouton "QUITTER LES CLASSEMENTS"
   même sur desktop. Stack toujours = toggle pleine largeur + buttons
   côté à côté avec assez d'espace pour leur multi-line text. */
.settings-lb-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  align-items: stretch;
}
.settings-lb-toggle {
  grid-column: 1 / -1;
  /* Hérite de .settings-pref-card : le label peut prendre deux lignes ;
     le toggle reste à droite sans réduire le label. */
  grid-template-columns: auto 1fr auto;
  align-items: center;
}
.settings-lb-toggle .settings-pref-label {
  white-space: normal;
  line-height: 1.2;
}

/* Trio Sauvegarde (Export/Import/Reset sur 1 ligne) ──────────────────── */
.settings-save-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
@media (max-width: 480px) {
  .settings-save-row { grid-template-columns: 1fr 1fr; }
  .settings-save-row > #settings-reset { grid-column: 1 / -1; }
}

/* Boutons ─────────────────────────────────────────────────────────────── */
.settings-btn {
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 9px 14px;
  background: var(--paper);
  color: var(--ink);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  cursor: pointer;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  /* iOS Safari : sans touch-action explicite, le tap peut être avalé par
     le delay 300ms ou le double-tap zoom détection. manipulation = supprime
     ces 2 comportements parasites tout en gardant le pinch-zoom global. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* <label for="settings-import-file"> stylé comme un bouton + input SR-only */
.settings-import-file-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
label.settings-btn { display: inline-flex; }
html.dlp-can-hover .settings-btn:hover:not(:disabled) {
  transform: translate(-2px, -2px);
  box-shadow: 5px 5px 0 0 var(--ink);
}
.settings-btn:active:not(:disabled) {
  transform: translate(1px, 1px);
  box-shadow: 2px 2px 0 0 var(--ink);
}
.settings-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.settings-btn--block { width: 100%; text-align: center; }
.settings-btn--danger { background: var(--red); color: var(--paper); }
.settings-btn--primary { background: var(--ink); color: var(--paper); }

/* Bouton multiligne : 2 spans empilés, hauteur identique aux cartes adjacentes */
.settings-btn--multiline {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 8px 10px;
  line-height: 1.15;
  min-height: 56px;
  text-align: center;
}
.settings-btn--multiline > span:first-child { font-size: 12px; }
.settings-btn--multiline > span:last-child  {
  font-size: 10px;
  letter-spacing: 0.06em;
  opacity: 0.8;
}

/* CTA Feedback (gros bouton dans le bloc violet) ─────────────────────── */
.settings-feedback-cta {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  color: var(--ink);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .settings-feedback-cta:hover {
  transform: translate(-2px, -2px);
  box-shadow: 5px 5px 0 0 var(--ink);
}
html.dlp-can-hover .settings-feedback-cta:hover .settings-feedback-cta-arrow { transform: translateX(4px); }
.settings-feedback-cta-emoji {
  font-size: 28px; line-height: 1;
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
}
.settings-feedback-cta-text { display: flex; flex-direction: column; gap: 2px; }
.settings-feedback-cta-title {
  font-family: var(--font-display);
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.15;
}
.settings-feedback-cta-sub {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  opacity: 0.65;
}
.settings-feedback-cta-arrow {
  font-family: var(--font-display);
  font-size: 22px;
  transition: transform var(--dur-fast);
}
/* Footer À propos — minimal, gris doux ──────────────────────────────── */
/* Footer locké en bas de la modale Paramètres : flex-shrink 0 pour qu'il
   reste toujours visible quand le contenu central scroll. Sort donc de
   .settings-scroll dans le markup. Bord noir net pour le détacher du
   scroll area. */
.settings-footer {
  flex-shrink: 0;
  background: var(--paper-2);
  border-top: var(--border-thick);
  padding: 10px var(--space-4) 12px;
}
.settings-about {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.6;
  margin: 0;
  text-align: center;
  opacity: 0.75;
}
.settings-about a { color: var(--ink); text-decoration: underline; }
/* Badge "beta" inline juste après le numéro de version. Jaune saturé +
   border noire fine + tilt léger pour le rendre visible sans crier — on
   est en beta long terme, autant l'assumer visuellement. */
.settings-beta-tag {
  display: inline-block;
  padding: 1px 6px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: var(--yellow);
  color: var(--ink);
  border: 1.5px solid var(--ink);
  transform: rotate(-2deg) translateY(-1px);
  vertical-align: middle;
}

/* ══════════════════════════════════════════════════════════════════════════
   SOUS-MODAL FEEDBACK
   ────────────────────────────────────────────────────────────────────────── */
.feedback-backdrop {
  position: fixed;
  inset: 0;
  z-index: 650;             /* au-dessus du modal Paramètres (600) */
  background: rgba(10,10,10,0.78);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
}
.feedback-backdrop.show { display: flex; }
.feedback-modal {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  max-width: 520px;
  width: 100%;
  max-height: 92vh;
  overflow: hidden;        /* scroll délégué au wrapper .settings-scroll interne */
  display: flex;
  flex-direction: column;
}
.feedback-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-thick);
  background: #ead4ff;       /* violet pastel cohérent avec le bloc d'origine */
  flex-shrink: 0;
}
.feedback-title {
  font-family: var(--font-display);
  font-size: clamp(18px, 2.6vw, 24px);
  text-transform: uppercase;
  letter-spacing: -0.01em;
  margin: 0;
  text-align: center;
}
.feedback-body {
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Chips type ─────────────────────────────────────────────────────────── */
.feedback-types { border: 0; padding: 0; margin: 0; }
.feedback-types legend {
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 8px;
  padding: 0;
}
.feedback-types-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.feedback-type-chip {
  position: relative;
  cursor: pointer;
}
.feedback-type-chip input { position: absolute; opacity: 0; pointer-events: none; }
.feedback-type-content {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 10px 8px;
  border: var(--border-thick);
  background: var(--paper);
  font-family: var(--font-display);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast), background var(--dur-fast);
}
.feedback-type-chip input:checked + .feedback-type-content {
  background: var(--ink);
  color: var(--paper);
  box-shadow: var(--sh-sm);
  transform: translate(-1px, -1px);
}
.feedback-type-chip input:focus-visible + .feedback-type-content {
  outline: 3px solid var(--violet);
  outline-offset: 2px;
}

/* Textarea ───────────────────────────────────────────────────────────── */
.feedback-field { display: flex; flex-direction: column; gap: 6px; }
.feedback-textarea {
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  padding: 10px 12px;
  border: var(--border-thick);
  background: var(--paper);
  outline: none;
  resize: vertical;
  min-height: 120px;
  width: 100%;
  box-sizing: border-box;
}
.feedback-textarea:focus { background: var(--white); box-shadow: var(--sh-sm); }
.feedback-counter {
  font-family: var(--font-mono);
  font-size: 11px;
  opacity: 0.7;
  margin: 0;
  text-align: right;
}

/* Contexte (details) ─────────────────────────────────────────────────── */
.feedback-context {
  border: 2px dashed rgba(10,10,10,0.3);
  padding: 8px 12px;
  background: var(--paper-2);
}
.feedback-context summary {
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  list-style: none;
}
.feedback-context summary::before {
  content: '+ ';
  display: inline-block;
  width: 14px;
  font-family: var(--font-mono);
}
.feedback-context[open] summary::before { content: '− '; }
.feedback-context-preview {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-all;
  margin: 8px 0 0 0;
  opacity: 0.85;
}

/* Actions feedback ───────────────────────────────────────────────────── */
.feedback-actions { display: flex; flex-direction: column; gap: 8px; }
.feedback-status {
  font-family: var(--font-mono);
  font-size: 12px;
  margin: 0;
  min-height: 1em;
  text-align: center;
}
.feedback-status--ok  { color: var(--green-2); }
.feedback-status--err { color: var(--red); }

/* ══════════════════════════════════════════════════════════════════════════
   BADGES JOUEUR — chip dans la status bar + modal de sélection
   Réutilise les gradients tier-or/argent/bronze de .dcard .badge (stats.html).
   ────────────────────────────────────────────────────────────────────────── */

/* Chip "badge actif" dans la status bar des classements ──────────────── */
.player-badge {
  display: inline-flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 10px 0 8px;
  border: 2px solid var(--ink);
  font-family: var(--font-display);
  font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase;
  cursor: pointer;
  opacity: 1;                                    /* pas de fade comme le reste de la bar */
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  box-shadow: 2px 2px 0 0 var(--ink);
  line-height: 1;
}
html.dlp-can-hover .player-badge:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 0 var(--ink); }
.player-badge-icon {
  width: 14px; height: 14px;
  color: currentColor;                           /* hérite de la couleur du chip (tier-fg) */
}
.player-badge-text { white-space: nowrap; }

/* Tiers — gradients identiques à .dcard .badge.tier-* (stats.html) */
.player-badge.tier-or {
  background: linear-gradient(135deg,#8a6508 0%,#e8b015 35%,#fff2a8 50%,#d49a0c 65%,#704f06 100%);
  color: #3a2a00;
  text-shadow: 0 1px 0 rgba(255,240,180,.5);
}
.player-badge.tier-argent {
  background: linear-gradient(135deg,#6e6e6e 0%,#c9c9c9 35%,#ffffff 50%,#b8b8b8 65%,#5e5e5e 100%);
  color: var(--ink);
}
.player-badge.tier-bronze {
  background: linear-gradient(135deg,#7a3f12 0%,#c87a35 35%,#f3c98a 50%,#a85e22 65%,#6a3410 100%);
  color: #fff4e3;
  text-shadow: 0 1px 0 rgba(0,0,0,.35);
}

/* CTA "+ Choisir un badge" si aucun badge actif ──────────────────────── */
.player-badge-empty {
  display: inline-flex; align-items: center;
  height: 26px; padding: 0 10px;
  background: transparent;
  border: 2px dashed rgba(10,10,10,0.45);
  font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.04em;
  cursor: pointer;
  opacity: 0.85;
  transition: background var(--dur-fast), border-color var(--dur-fast);
  line-height: 1;
}
html.dlp-can-hover .player-badge-empty:hover {
  background: var(--paper-2);
  border-color: var(--ink);
}

/* Texte muet dans la status bar (pour "non connecté·e", etc.) */
.lb-status-mute { opacity: 0.7; }

/* Modal Mes badges ───────────────────────────────────────────────────── */
.badge-picker-backdrop {
  position: fixed; inset: 0;
  z-index: 620;                                  /* > settings (600), < feedback (650) */
  background: rgba(10,10,10,0.78);
  display: none; align-items: center; justify-content: center;
  padding: var(--space-3);
}
.badge-picker-backdrop.show { display: flex; }
.badge-picker-modal {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  max-width: 540px;
  width: 100%;
  max-height: 92vh;
  overflow: hidden;                              /* scroll délégué à .settings-scroll */
  display: flex; flex-direction: column;
}
.badge-picker-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-thick);
  background: var(--yellow);
  flex-shrink: 0;
}
.badge-picker-title {
  font-family: var(--font-display);
  font-size: clamp(18px, 2.6vw, 24px);
  text-transform: uppercase;
  letter-spacing: -0.01em;
  margin: 0;
}
.badge-picker-close {
  width: 36px; height: 36px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  font-family: var(--font-display);
  font-size: 16px;
  cursor: pointer;
  display: grid; place-items: center;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .badge-picker-close:hover { transform: translate(-2px, -2px); box-shadow: 5px 5px 0 0 var(--ink); }

.badge-picker-body {
  padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-4);
}

/* Notif "tu as perdu ton badge" ──────────────────────────────────────── */
.badge-picker-notif {
  background: var(--orange);
  border: var(--border-thick);
  padding: 10px 14px;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.4;
  box-shadow: var(--sh-sm);
}

.badge-picker-section { display: flex; flex-direction: column; gap: 10px; }
.badge-picker-sub {
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0;
  border-bottom: 3px solid var(--ink);
  padding-bottom: 6px;
}
.badge-picker-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}
@media (min-width: 480px) {
  .badge-picker-grid { grid-template-columns: 1fr 1fr; }
}

/* Carte badge dans le picker (disponible OU verrouillée) ─────────────── */
.badge-pick-card {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: var(--border-thick);
  background: var(--paper);
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  color: var(--ink);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  box-shadow: var(--sh-sm);
}
html.dlp-can-hover .badge-pick-card:hover:not(.is-locked) {
  transform: translate(-2px, -2px);
  box-shadow: 5px 5px 0 0 var(--ink);
}
.badge-pick-card.tier-or {
  background: linear-gradient(135deg,#8a6508 0%,#e8b015 35%,#fff2a8 50%,#d49a0c 65%,#704f06 100%);
  color: #3a2a00;
}
.badge-pick-card.tier-argent {
  background: linear-gradient(135deg,#6e6e6e 0%,#c9c9c9 35%,#ffffff 50%,#b8b8b8 65%,#5e5e5e 100%);
  color: var(--ink);
}
.badge-pick-card.tier-bronze {
  background: linear-gradient(135deg,#7a3f12 0%,#c87a35 35%,#f3c98a 50%,#a85e22 65%,#6a3410 100%);
  color: #fff4e3;
}
.badge-pick-card.is-current {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
.badge-pick-card.is-locked {
  background: var(--paper-2);
  cursor: not-allowed;
  opacity: 0.55;
  filter: grayscale(0.8);
  box-shadow: 2px 2px 0 0 rgba(10,10,10,0.4);
}
.badge-pick-icon {
  width: 22px; height: 22px;
  color: currentColor;                            /* suit la couleur de texte du tier */
}
.badge-pick-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.badge-pick-title {
  font-family: var(--font-display);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.15;
}
.badge-pick-sub {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  opacity: 0.78;
  text-transform: none;
}
.badge-pick-check {
  width: 20px; height: 20px;
  color: currentColor;
}

.badge-picker-empty {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
  padding: 24px 12px;
  background: var(--paper-2);
  border: 2px dashed rgba(10,10,10,0.3);
  opacity: 0.85;
}

.badge-picker-actions {
  border-top: 2px dashed rgba(10,10,10,0.25);
  padding-top: 12px;
}

/* Bouton de donation néobrut — pleine largeur, accent jaune chaleureux.
   Animation "breathing" loop subtile pour attirer l'œil sans agressivité.
   transform-origin décalé pour un sway légèrement asymétrique (plus naturel).
   Désactivée au hover/active pour ne pas conflicter avec les transforms ciblés.
   Respecte automatiquement html.reduce-motion (règle globale). */
@keyframes donate-breathe {
  0%, 100% { transform: rotate(-0.7deg) translateY(0)    scale(1);     }
  50%      { transform: rotate( 0.9deg) translateY(-2px) scale(1.022); }
}
.settings-donate {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 16px;
  background: var(--yellow, #ffe066);
  border: var(--border-thick);
  box-shadow: 5px 5px 0 rgba(10,10,10,0.4);
  text-decoration: none; color: var(--ink);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  animation: donate-breathe 3.4s ease-in-out infinite;
  transform-origin: 28% 60%;  /* pivote autour de l'emoji ☕ */
  will-change: transform;
}
.settings-donate:active,
.settings-donate:focus-visible {
  animation: none;
}
html.dlp-can-hover .settings-donate:hover {
  animation: none;
  transform: translate(-2px, -2px);
  box-shadow: 7px 7px 0 rgba(10,10,10,0.5);
}
.settings-donate:active {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 rgba(10,10,10,0.4);
}
.settings-donate-emoji {
  font-size: 32px; line-height: 1;
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  flex-shrink: 0;
}
.settings-donate-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.settings-donate-title {
  font-family: var(--font-display); font-size: 17px;
  letter-spacing: 0.01em; text-transform: uppercase;
  line-height: 1.1;
}
.settings-donate-sub {
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.06em; opacity: 0.75;
}

/* Emoji "Classical Building" 🏛️ utilisé avant les titres de page —
   stroke fin pour détacher du fond + grosse ombre portée en bas-droite
   (façon tampon néobrut). Légèrement superposé au début du titre. */
.site-emoji {
  display: inline-block;
  position: relative;
  z-index: 2;               /* passe au-dessus du fond coloré du .accent */
  font-size: 1.15em;
  line-height: 1;
  vertical-align: middle;
  margin-right: -0.3em;     /* mord franchement le titre qui suit */
  transform: rotate(-6deg) translateY(-0.05em);
  filter:
    /* Stroke ~2 px (cardinales + diagonales pour combler) */
    drop-shadow( 2px  0   0 var(--ink))
    drop-shadow(-2px  0   0 var(--ink))
    drop-shadow( 0    2px 0 var(--ink))
    drop-shadow( 0   -2px 0 var(--ink))
    drop-shadow( 1.5px  1.5px 0 var(--ink))
    drop-shadow(-1.5px  1.5px 0 var(--ink))
    drop-shadow( 1.5px -1.5px 0 var(--ink))
    drop-shadow(-1.5px -1.5px 0 var(--ink))
    /* Grosse ombre portée d'un côté (en bas-droite) — effet néobrut */
    drop-shadow(5px 5px 0 var(--ink));
  flex-shrink: 0;
}

.site-title .accent {
  /* fond + texte définis dynamiquement par applyRandomTitleColor() ;
     var(--blue) en fallback avant que le JS s'exécute */
  background: var(--blue);
  color: var(--paper);
  padding: 1px 8px;
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  display: inline-block;
  transform: rotate(-1deg);
  margin-left: 6px;
  text-transform: uppercase;
  transition: background-color var(--dur-base), color var(--dur-base);
  user-select: none;
}
html.dlp-can-hover .site-title .accent:hover {
  transform: rotate(-1deg) translate(-1px, -1px);
  box-shadow: 5px 5px 0 0 var(--ink);
}

/* ── Icon utility ────────────────────────────────────────────────────────── */
.ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ico-svg { display: block; }

/* ── Main grid ───────────────────────────────────────────────────────────── */
.main {
  display: grid;
  grid-template-columns: clamp(200px, calc((100dvh - 142px) * 0.75 + 42px), 48%) 1fr;
  grid-template-rows: 1fr;
  gap: var(--space-4);
  flex: 1;
  min-height: 0;
  overflow: visible;
}

/* ── Card stack ──────────────────────────────────────────────────────────── */
.card-stack {
  position: relative;
  height: 100%;
  min-height: 0;
}

.deputy-card-shadow {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--white);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  /* overflow: hidden pour clipper les tampons agrandis qui peuvent dépasser
     du cadre du shadow (sinon on voit un « fantôme » de tampon par-dessus
     le titre quand la card principale swipe-out et révèle le shadow). */
  overflow: hidden;
  /* Container query context pour les unités cqi des tampons (.fx-stamp-*). */
  container-type: inline-size;
}
.shadow-photo-wrap {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 0;
  overflow: visible;
  border: var(--border-thick);
  background: var(--paper-2);
  container-type: inline-size;
}
.shadow-photo {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
}

.deputy-card {
  background: var(--white);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  padding: var(--space-4);
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  height: 100%;
  overflow: hidden;
  transition: transform 0.55s cubic-bezier(0.45, 0.05, 0.55, 0.95);
  will-change: transform;
  /* Container query context pour les unités cqi des tampons (.fx-stamp-*).
     Permet aussi aux tampons d'occuper toute la zone carte (incluant le
     padding autour de .photo-wrap), spécialement utile sur mobile. */
  container-type: inline-size;
}
.deputy-card.swiping-out {
  /* Trajectoire de chute : X linéaire dans le temps, Y quadratique (gravité).
     Linear timing — la courbe vient des positions des keyframes elles-mêmes. */
  animation: swipeOutCurve 0.55s linear forwards;
}
@keyframes swipeOutCurve {
  /* X(p) = p × X_final  (linéaire — vitesse horizontale constante)
     Y(p) = p² × Y_final (quadratique — accélération vers le bas)
     rot(p) = p × rot_final  ·  scale(p) = 1 + p × (scale_final − 1) */
  0% {
    transform: translate(0, 0) rotate(0) scale(1);
  }
  25% {
    transform:
      translate(calc(var(--swipe-x, -120vw) * 0.25), calc(var(--swipe-y, 80vh) * 0.0625))
      rotate(calc(var(--swipe-rot, -25deg) * 0.25))
      scale(calc(1 + (var(--swipe-scale, 0.75) - 1) * 0.25));
  }
  50% {
    transform:
      translate(calc(var(--swipe-x, -120vw) * 0.50), calc(var(--swipe-y, 80vh) * 0.25))
      rotate(calc(var(--swipe-rot, -25deg) * 0.50))
      scale(calc(1 + (var(--swipe-scale, 0.75) - 1) * 0.50));
  }
  75% {
    transform:
      translate(calc(var(--swipe-x, -120vw) * 0.75), calc(var(--swipe-y, 80vh) * 0.5625))
      rotate(calc(var(--swipe-rot, -25deg) * 0.75))
      scale(calc(1 + (var(--swipe-scale, 0.75) - 1) * 0.75));
  }
  100% {
    transform:
      translate(var(--swipe-x, -120vw), var(--swipe-y, 80vh))
      rotate(var(--swipe-rot, -25deg))
      scale(var(--swipe-scale, 0.75));
  }
}

/* ── Shiny card — apparaît ~1/15 fois, double les points ──────────────── */
/* Glow doré pulsant autour de la carte (box-shadow, ne couvre aucun bouton).
   `--card-sh-base` reprend l'ombre néobrut de la carte (sh-lg desktop, sh-md
   mobile via override) → shiny/holo gardent la MÊME ombre portée que la
   carte non-rare, juste avec un glow par-dessus. Sans cette variable, le
   keyframe imposait sh-lg en mobile et la shiny dépassait l'hémicycle.
   Délai -0.9s aligné sur les autres animations shiny → le glow est déjà au
   peak (50%) au moment où la carte apparaît, plus de « stagger » de 900ms
   où le glow monte depuis zéro. */
.deputy-card { --card-sh-base: var(--sh-lg); }
.deputy-card.shiny {
  animation: shinyGlow 1.8s ease-in-out -0.9s infinite;
}
/* Quand une carte shiny part en swipe-out, on combine les deux animations
   (sinon shiny gagne par cascade et le swipe-out n'a pas lieu). */
.deputy-card.shiny.swiping-out {
  animation:
    shinyGlow 1.8s ease-in-out -0.9s infinite,
    swipeOutCurve 0.55s linear forwards;
}
@keyframes shinyGlow {
  0%, 100% { box-shadow: var(--card-sh-base, var(--sh-lg)), 0 0 0 rgba(245, 197, 24, 0); }
  50%      { box-shadow: var(--card-sh-base, var(--sh-lg)), 0 0 28px rgba(245, 197, 24, 0.75); }
}

/* Couche dédiée aux effets shiny — clip ses propres pseudos sans toucher le post-it.
   `contain: layout paint` isole les recalculs au sous-arbre → l'apparition
   d'une nouvelle carte shiny/holo n'invalide pas le layout du parent et
   réduit le « stagger » visible sur le shimmer à la 1ère frame.
   `transform: translateZ(0)` force la promotion GPU dès la base (pas
   d'attente du compositor au moment du display:block toggle). */
.shiny-fx {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  display: none;
  contain: layout paint;
  transform: translateZ(0);
}
.deputy-card.shiny .shiny-fx,
.deputy-card.holo  .shiny-fx,
.deputy-card-shadow.shiny .shiny-fx,
.deputy-card-shadow.holo  .shiny-fx {
  display: block;
}
/* Glow doré INTERNE (carte courante + shadow card pour pre-style).
   ⚠ Diag profond 2026-05-17 : avant cette refonte, .shiny-fx::before avait
   une `border: 4px solid …` qui dupliquait la bordure animée du .photo-wrap.
   Les 2 bordures avaient les mêmes durées (1.8s shiny, 3s holo) mais des
   delays légèrement différents (-0.85 vs -0.9, -0.55 vs -0.6) → micro-
   désynchronisation visible à chaque nouvelle carte. La border de
   .shiny-fx::before est désormais retirée : seule reste celle du
   .photo-wrap (qui anime sa border-color via shinyPhotoBorder /
   holoPhotoBorder). Le ::before garde son rôle de GLOW INTERNE inset
   (rayon 8-22px) qui se voit toujours derrière la bordure unique. */
.deputy-card.shiny .shiny-fx::before,
.deputy-card-shadow.shiny .shiny-fx::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  animation: shinyBorderPulse 1.8s ease-in-out -0.9s infinite;
}
@keyframes shinyBorderPulse {
  0%, 100% { box-shadow: inset 0 0 8px rgba(255, 215, 0, 0.4); }
  50%      { box-shadow: inset 0 0 22px rgba(255, 230, 102, 0.8); }
}
/* Glow HOLO iridescent INTERNE : cycle de couleurs sur 6 paliers (rose →
   cyan → jaune → vert → orange → magenta) via la box-shadow inset
   uniquement. La border est portée par .photo-wrap (= bordure unique). */
.deputy-card.holo  .shiny-fx::before,
.deputy-card-shadow.holo  .shiny-fx::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  animation: holoBorderHue 3s linear -0.6s infinite;
}
@keyframes holoBorderHue {
  0%   { box-shadow: inset 0 0 14px rgba(255, 0, 255, 0.55); }
  20%  { box-shadow: inset 0 0 14px rgba(0, 255, 255, 0.55); }
  40%  { box-shadow: inset 0 0 14px rgba(255, 255, 0, 0.55); }
  60%  { box-shadow: inset 0 0 14px rgba(0, 255, 100, 0.55); }
  80%  { box-shadow: inset 0 0 14px rgba(255, 100, 50, 0.55); }
  100% { box-shadow: inset 0 0 14px rgba(255, 0, 255, 0.55); }
}
/* Bordure photo dorée pour les rôles rectangulaires (depute, etc.) en mode
   shiny/holo. Les rôles avec clip-path mettent leur .photo en border:none,
   donc ces règles n'ont aucun effet sur eux (l'animation SVG du polygon
   prend le relais pour suivre la forme). Pas de box-shadow inset ici (sinon
   on tinte la photo elle-même). */
@keyframes shinyPhotoBorder {
  0%, 100% { border-color: #f5c518; }
  50%      { border-color: #fff09a; }
}
@keyframes holoPhotoBorder {
  0%   { border-color: #ff00ff; }
  20%  { border-color: #00ffff; }
  40%  { border-color: #ffff00; }
  60%  { border-color: #00ff66; }
  80%  { border-color: #ff6633; }
  100% { border-color: #ff00ff; }
}
/* Animations en pause par défaut (perf grille + sync avec .shiny-fx::before
   qui est aussi paused). Reprises au hover/focus/shiny-active : les deux
   strokes redémarrent à 0% en même temps → cycles iridescents synchronisés.
   Délais identiques à .shiny-fx::before pour que la frame figée soit cohérente
   entre les deux strokes (cadre + photo même couleur). */
.dcard.shiny .photo,
.deputy-card.shiny .photo-wrap,
.deputy-card-shadow.shiny .shadow-photo-wrap {
  animation: shinyPhotoBorder 1.8s ease-in-out -0.9s infinite;
  animation-play-state: paused;
}
.dcard.holo .photo,
.deputy-card.holo .photo-wrap,
.deputy-card-shadow.holo .shadow-photo-wrap {
  animation: holoPhotoBorder 3s linear -0.6s infinite;
  animation-play-state: paused;
}

/* ── Fond photo · axe D rareté · SHINY ──────────────────────────────────
   15 sparkles dorées (path SVG user) distribution exponentielle 1:2:4:8 sur
   tile 240×240. Path défini une seule fois dans le fichier externe
   `img/textures/sparkles/shiny-sparkles.svg` (avec <defs><path id='sp'/></defs>)
   et réutilisé via <use href='#sp'> — beaucoup plus léger que dupliquer le
   path inline. Animation CSS interne au SVG : opacity + scale, durées par
   niveau (6s lvl0 → 3s lvl3) → "bigger = appears less frequently". */
.deputy-card.shiny .photo-wrap,
.deputy-card-shadow.shiny .shadow-photo-wrap {
  --bg-rarity:
    url("img/textures/sparkles/shiny-sparkles.svg"),
    radial-gradient(circle at center, rgba(255,210,63,0.20) 0%, transparent 60%);
  --bg-rarity-size: 240px 240px, 100% 100%;
}

/* ── Fond photo · axe D rareté · HOLO : overflow hidden + ::before ──────
   Définition complète plus bas (après la règle `background: none` l.1914+,
   pour gagner la cascade). Ici juste l'overflow:hidden requis pour clip
   le pseudo qui dépasse via inset négatif. */
.deputy-card.holo .photo-wrap,
.deputy-card-shadow.holo .shadow-photo-wrap {
  overflow: hidden;
}
.dcard.shiny:focus-within .photo,
.dcard.shiny.shiny-active .photo,
.dcard.holo:focus-within .photo,
.dcard.holo.shiny-active .photo,
.deputy-card.shiny .photo-wrap,
.deputy-card.holo  .photo-wrap,
.deputy-card-shadow.shiny .shadow-photo-wrap,
.deputy-card-shadow.holo  .shadow-photo-wrap {
  animation-play-state: running;
}
html.dlp-can-hover .dcard.shiny:hover .photo,
html.dlp-can-hover .dcard.holo:hover  .photo {
  animation-play-state: running;
}
/* Shimmer diagonal — élément 200% de large, translation de -50% à 0%
   (= shift d'une demi-largeur = une période). Le gradient contient DEUX
   bands identiques, espacées d'exactement une période. Quand la première
   sort à droite, la deuxième est centrée → continuité visuelle.
   Plus : un léger fade-in/out aux extrémités du cycle pour adoucir le
   point de loop (la moindre désynchro est masquée par l'opacité qui dip). */
.deputy-card.shiny .shiny-fx::after,
.deputy-card-shadow.shiny .shiny-fx::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  background: linear-gradient(115deg,
    transparent 17%,
    rgba(255, 232, 120, 0.5) 25%,
    transparent 33%,
    transparent 67%,
    rgba(255, 232, 120, 0.5) 75%,
    transparent 83%);
  transform: translate3d(-50%, 0, 0);
  /* Délai -0.7s (≈25% du cycle) aligné sur la version Collection (.dcard).
     Sans ça, le shimmer démarrait à opacity 0 → 500ms invisibles à l'apparition
     de la carte = effet de « stagger » du gradient. Avec -0.7s on rentre déjà
     dans la zone opaque visible. */
  animation: shinyShimmer 2.8s linear -0.7s infinite;
  will-change: transform, opacity;
}
/* Shimmer HOLO : deux bands multicolores aux mêmes positions (25% et 75%
   du gradient), avec hue-rotate continue par-dessus pour l'effet oil-slick.
   Opacité réduite (0.30 vs 0.45 avant) — l'effet était trop chargé. */
.deputy-card.holo  .shiny-fx::after,
.deputy-card-shadow.holo  .shiny-fx::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  background: linear-gradient(115deg,
    transparent 13%,
    rgba(255, 100, 200, 0.30) 19%,
    rgba(100, 200, 255, 0.30) 23%,
    rgba(255, 220, 100, 0.30) 27%,
    rgba(180, 100, 255, 0.30) 31%,
    transparent 37%,
    transparent 63%,
    rgba(255, 100, 200, 0.30) 69%,
    rgba(100, 200, 255, 0.30) 73%,
    rgba(255, 220, 100, 0.30) 77%,
    rgba(180, 100, 255, 0.30) 81%,
    transparent 87%);
  transform: translate3d(-50%, 0, 0);
  /* Mêmes délais qu'en shiny : -0.7s sur shinyShimmer (translate visible
     d'emblée) et -1s sur holoShimmerHue (cyan/orange dès l'apparition). */
  animation: shinyShimmer 2.8s linear -0.7s infinite, holoShimmerHue 4s linear -1s infinite;
  will-change: transform, opacity;
}
/* Shimmer keyframes : translation linéaire + fade-in/out COMPLET aux 2 bouts.
   Opacity passe à 0 aux extrémités → la band est totalement invisible au
   moment du loop. Le jump du transform (de translate(0) à translate(-50%))
   se fait pendant que l'opacité est nulle → 100% imperceptible.
   Fade plus large (18% de chaque côté = ~500ms) pour une transition douce. */
@keyframes shinyShimmer {
  0%   { transform: translate3d(-50%, 0, 0); opacity: 0; }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { transform: translate3d(0%, 0, 0);   opacity: 0; }
}
@keyframes holoShimmerHue {
  0%   { filter: hue-rotate(0deg); }
  100% { filter: hue-rotate(360deg); }
}

/* Étoile shiny sur la carte Collection (SVG net, contour propre) */
.dcard .shiny-star {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 4;
  width: 22px;
  height: 22px;
  display: inline-block;
  line-height: 0;
  pointer-events: none;
  filter: drop-shadow(0 1px 0 var(--ink)) drop-shadow(1px 0 0 var(--ink));
}
.dcard .shiny-star svg { display: block; }

/* Variante "shiny" : or + balayage shine, MÊME look que la carte brillante
   (item 8 : on réutilise --yellow + rare-shine-sweep, global index-page.css). */
.collection-fly.shiny {
  background: var(--yellow);
  color: var(--ink);
  border-color: var(--ink);
}
.collection-fly.shiny::after {
  content: ''; position: absolute; top: -40%; height: 180%; width: 18px;
  left: -30%; background: rgba(255, 255, 255, 0.92);
  transform: skewX(-16deg); pointer-events: none; z-index: 2;
  animation: rare-shine-sweep 2.8s ease-in-out infinite;
}
.collection-fly.shiny.shrunk::after { display: none; }
.collection-fly.shiny .cf-mark {
  font-size: 20px;
  font-weight: 700;
  color: var(--ink);
  margin-right: 4px;
}
/* Variante holographique : MÊMES bandes arc-en-ciel animées que le FOND des
   cartes holo (item 8 : --hue-* + rare-holo-scroll, global index-page.css). */
/* Holo : motif STRICTEMENT repris du fond de carte holo (js/card-frames.js renderHoloPole) :
   bandes VERTICALES 12px (6 teintes --hue-*) sur un pseudo-élément ROTATÉ 35°, qui défilent
   de 72px (1 période). Stripes verticales + scroll horizontal = seamless trivial (la version
   gradient-45° sautait — retour user 2026-06-15). */
.collection-fly.holo {
  background: var(--ink);          /* base sombre, masquée par le ::before holo */
  color: var(--ink);
  border-color: var(--ink);
}
.collection-fly.holo::before {
  content: '';
  position: absolute;
  inset: -120%;                    /* surdimensionné : couvre la boîte une fois rotaté 35° */
  background: repeating-linear-gradient(90deg,
    var(--hue-1) 0 12px, var(--hue-2) 12px 24px, var(--hue-3) 24px 36px,
    var(--hue-4) 36px 48px, var(--hue-5) 48px 60px, var(--hue-6) 60px 72px);
  transform: rotate(35deg);
  animation: holo-barbershop-bg 2.4s linear infinite;
  pointer-events: none;
  z-index: 0;
}
.collection-fly.holo .cf-mark,
.collection-fly.holo .cf-label-full,
.collection-fly.holo .cf-label-short { position: relative; z-index: 2; }
.collection-fly.holo .cf-mark {
  font-size: 20px;
  font-weight: 700;
  color: var(--ink);
  margin-right: 4px;
}
@media (prefers-reduced-motion: reduce) {
  .collection-fly.holo::before { animation: none; }
  .collection-fly.shiny::after { animation: none; left: 125%; }
}
/* Phase 2 — bumper rétréci :
   - `cf-label-full` + `cf-mark` passent en `display: none` (hors flow) →
     l'offsetWidth mesuré reflète juste « +1 » + padding (sinon le label
     long, même en opacity:0, contribuait à la layout width et `shrunkW`
     restait ≈ `fullW` → box ne rétrécissait pas).
   - `cf-label-short` devient `display: inline-block`, position static
     (en flow) → centré par le `justify-content: center` du flex parent.
   - L'explicit width posée en JS (fly.style.width = fullW → shrunkW) +
     `transition: width 280ms` (cf. .collection-fly) → la box rétrécit
     smooth depuis la pleine largeur jusqu'à la taille « +1 ». */
.collection-fly .cf-label-short { display: none; }
.collection-fly.shrunk .cf-label-full,
.collection-fly.shrunk .cf-mark { display: none; }
.collection-fly.shrunk .cf-label-short { display: inline-block; }
.collection-fly.shrunk {
  padding: 4px 11px 6px;
  font-size: 20px;
}

/* Bumper « +1 » simple (carte normale). Animation pilotée 100% par WAAPI
   dans bumpCollectionSimple (cf. app.js), PAS de keyframe CSS ici.
   Raison : sur iOS Safari, une CSS animation déclarée ici (même override
   inline `animation:none`) entrait en conflit avec WAAPI et empêchait le
   vol vers Collection (le bumper restait pop-in puis fadait sur place).
   Sans CSS animation, WAAPI prend la main proprement. */
.collection-bump {
  position: fixed;
  transform: translate(-50%, -50%) scale(0);
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1;
  padding: 4px 11px 6px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  color: var(--green);
  pointer-events: none;
  z-index: 600;
  white-space: nowrap;
  will-change: transform, opacity;
}

.photo-wrap {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 0;
  overflow: visible;
  border: var(--border-thick);
  background: var(--paper-2);
  container-type: inline-size;
}

/* ── Fond photo · architecture en layers (axes A → F de la spec) ─────────
   Cumule jusqu'à 4 background-image via CSS variables, défaut none → no-op
   si non posé par une règle aval. Empilement (top → bottom du visuel) :
     1. --bg-rarity   : axe D (shiny/holo) ou axe E (rang collection)
     2. --bg-famille  : axe C (post-révélation, gated par .revealed)
     3. --bg-role     : axe B (motif par data-role institutionnel)
     4. halftone neutre (axe A) : dots noirs 1px sur grille 22px, opacity .06
   Les ::before (stippling + scan-lines) et ::after (warm tint) restent
   intacts et continuent de jouer SUR la photo. Le fond ne se voit que sur
   les zones transparentes du portrait WebP α. */
.photo-wrap,
.shadow-photo-wrap {
  background-image:
    var(--bg-rarity,  none),
    var(--bg-famille, none),
    var(--bg-role,    none),
    radial-gradient(rgba(10,10,10,0.06) 1px, transparent 1px);
  background-size:
    var(--bg-rarity-size,  auto),
    var(--bg-famille-size, auto),
    var(--bg-role-size,    auto),
    22px 22px;
  background-position:
    var(--bg-rarity-pos,  0 0),
    var(--bg-famille-pos, 0 0),
    var(--bg-role-pos,    0 0),
    var(--bg-pos, 0 0);
  background-repeat: repeat;
}

/* ── Tache de café occasionnelle ─────────────────────────────────────────
   Tache de café (texture coffee-ring randomisée parmi 6) avec son centre
   près du rebord de la photo, donnant l'effet "tasse posée à moitié sur" —
   la moitié hors photo doit être coupée au bord pour ne pas déborder sur
   le cadre carte. Comme photo-wrap a overflow:visible pour les effets
   shiny, on wrappe dans .coffee-stain-clip qui a overflow:hidden + inset 0.
   La .coffee-stain dedans est positionnée en JS.

   Z-INDEX 0 (et non 3 comme initialement) : la tache se pose DIRECTEMENT
   sur la photo, MAIS SOUS les pseudos ::before (stippling + scan lines,
   z 1) et ::after (overlay tint warm, z 1). Résultat : le café est
   "imprimé" dans la photo et reçoit les mêmes effets de grain/tint que
   le reste — il s'intègre au lieu de paraître collé par-dessus. Les
   stamps officiels (z 5+) restent au-dessus. */
.coffee-stain-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.coffee-stain {
  position: absolute;
  display: none;
  pointer-events: none;
  background: no-repeat center / contain;
  aspect-ratio: 1;
  /* width / left / top / transform / opacity / filter / mix-blend-mode :
     posés dynamiquement par applyCoffeeStain() dans app.js */
}
.coffee-stain.show { display: block; }

/* ── Modal Changelog ────────────────────────────────────────────────────
   S'ouvre au click sur #settings-version dans Paramètres > À propos.
   Fetch Public/VERSION.md, parse en markdown léger, affiche en liste
   scrollable inverse-chrono. Style néobrut paper-2 + tilt subtle. */
.changelog-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.6);
  display: none;
  align-items: center; justify-content: center;
  z-index: 2200;
  padding: 16px;
}
.changelog-backdrop.show { display: flex; }
.changelog-card {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: 10px 10px 0 var(--ink);
  max-width: min(680px, 96vw);
  width: 100%;
  max-height: min(85vh, 800px);
  display: flex; flex-direction: column;
  transform: rotate(-0.5deg);
  animation: changelog-pop-in 280ms cubic-bezier(0.25, 1.4, 0.5, 1.05) forwards;
  /* Container query : le titre .changelog-title scale relativement à la
     largeur du card (cf clamp(_, 5.2cqi, _) sur .changelog-title). */
  container-type: inline-size;
}
@keyframes changelog-pop-in {
  0%   { opacity: 0; transform: rotate(-0.5deg) scale(0.7); }
  60%  { opacity: 1; transform: rotate(-0.5deg) scale(1.04); }
  100% { opacity: 1; transform: rotate(-0.5deg) scale(1); }
}
.changelog-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 18px;
  border-bottom: var(--border-thick);
  background: var(--yellow);
  flex-shrink: 0;
}
.changelog-title {
  font-family: var(--font-display);
  /* Scale selon la largeur du modal (container query). Le card a
     container-type: inline-size, donc 1cqi = 1% de sa largeur. Le titre
     "NOTES DE MISE À JOUR" (~19 chars uppercase Archivo Black) tient sur
     1 ligne au-delà de ~320 px de largeur de modal à 5cqi, et reste
     lisible à 14 px sur les très petits écrans. */
  font-size: clamp(14px, 5.2cqi, 24px);
  letter-spacing: -0.02em;
  text-transform: uppercase;
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
}
.changelog-close {
  background: var(--paper);
  border: var(--border-thin);
  font-family: var(--font-display);
  font-size: 14px;
  padding: 2px 9px;
  cursor: pointer;
  color: var(--ink);
}
html.dlp-can-hover .changelog-close:hover { background: var(--ink); color: var(--paper); }
.changelog-body {
  padding: 18px 22px 22px;
  overflow-y: auto;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.55;
  scrollbar-width: thin;
  scrollbar-color: var(--ink) transparent;
}
.changelog-body::-webkit-scrollbar { width: 8px; }
.changelog-body::-webkit-scrollbar-thumb { background: var(--ink); }
.changelog-body h1 {
  font-family: var(--font-display);
  font-size: 18px;
  letter-spacing: -0.01em;
  margin: 6px 0 12px;
  text-transform: uppercase;
}
.changelog-body h2 {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 18px 0 8px;
  padding-bottom: 4px;
  border-bottom: 2px solid var(--ink);
}
.changelog-body h3 {
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.02em;
  margin: 16px 0 6px;
  padding: 4px 8px;
  background: var(--paper-2);
  border-left: 4px solid var(--ink);
}
.changelog-body p { margin: 6px 0; }
.changelog-body ul {
  margin: 4px 0 10px;
  padding-left: 18px;
  list-style: square;
}
.changelog-body ul li::marker { color: var(--ink); }
.changelog-body li { margin: 3px 0; }
.changelog-body li.nested {
  margin-left: 12px;
  font-size: 12px;
  opacity: 0.85;
}
.changelog-body code {
  font-family: var(--font-mono);
  font-size: 11px;
  background: var(--paper-2);
  padding: 1px 5px;
  border: 1px solid var(--ink);
}
.changelog-body strong { font-family: var(--font-display); font-weight: 700; }
.changelog-body blockquote {
  margin: 8px 0;
  padding: 6px 12px;
  background: var(--paper-2);
  border-left: 3px solid var(--ink);
  font-style: italic;
  opacity: 0.85;
}
.changelog-body hr {
  margin: 14px 0;
  border: none;
  border-top: 2px dashed var(--ink);
}
.changelog-body .changelog-loading {
  text-align: center;
  font-style: italic;
  opacity: 0.6;
  padding: 40px 0;
}

/* Bouton "v0.X.Y beta" — click ouvre la modal Changelog.
   <button> natif (pas <span>) : iOS Safari ignorait parfois les taps sur
   <span> inline coincé entre du texte, surtout pendant un scroll inertiel
   de la modal parente. Le <button> + role implicite + touch-action manip
   garantit que le tap est toujours dispatché.
   La zone cliquable inclut le badge "beta" — visuellement c'est un seul
   bouton, l'utilisateur ne devrait pas avoir à viser pile le numéro. */
.settings-version-btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 6px;
  margin: -4px 0;          /* compense le padding pour ne pas pousser la baseline */
  border-radius: 0;
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.18);
  transition: opacity 120ms;
}
.settings-version-btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
.settings-version-btn .settings-version-num {
  border-bottom: 1.5px dotted var(--ink);
  padding-bottom: 1px;
}
.settings-version-btn:active { opacity: 0.6; }
html.dlp-can-hover .settings-version-btn:hover { opacity: 0.7; }

/* ── Pop-up "Encourage classements" ──────────────────────────────────────
   Modal one-shot qui s'affiche à 25 élu·e·s rencontré·e·s pour proposer
   au user de saisir un pseudo et apparaître dans les classements. Style
   néobrut paper-2 + tilt léger, animation pop-in cohérente avec les
   autres modales (rare-explain, etc.). */
.lb-promo-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.55);
  display: none;
  align-items: center; justify-content: center;
  z-index: 2000;
  padding: 20px;
}
.lb-promo-backdrop.show { display: flex; }
.lb-promo-card {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: 10px 10px 0 var(--ink);
  max-width: min(420px, 88vw);
  width: 100%;
  padding: 28px 26px 22px;
  display: flex; flex-direction: column;
  align-items: center; text-align: center;
  gap: 14px;
  transform: rotate(-1.5deg);
  animation: lb-promo-pop-in 320ms cubic-bezier(0.25, 1.4, 0.5, 1.05) forwards;
}
@keyframes lb-promo-pop-in {
  0%   { opacity: 0; transform: rotate(-1.5deg) scale(0.5); }
  60%  { opacity: 1; transform: rotate(-1deg) scale(1.06); }
  100% { opacity: 1; transform: rotate(-1.5deg) scale(1); }
}
/* Direction A : podium néo-brut (or/argent/bronze). Les marches montent en
   gradins (bronze d'abord, OR en dernier) à l'ouverture. Couleurs médailles
   canoniques (cf. leaderboards-v3.css : --lb-gold/silver/bronze). */
.lb-promo-podium {
  display: flex; align-items: flex-end; justify-content: center; gap: 6px;
  height: 62px; margin-bottom: 2px;
}
.lb-promo-step {
  display: grid; place-items: center;
  font-family: var(--font-display); font-style: italic; font-weight: 800;
  color: var(--ink);
  border: 3px solid var(--ink); box-shadow: 3px 3px 0 var(--ink);
  width: 38px;
  transform-origin: bottom;
  animation: lb-promo-step-rise 360ms var(--ease-out) both;
}
.lb-promo-step--1 { height: 58px; width: 44px; background: var(--yellow); font-size: 22px; animation-delay: 380ms; }
.lb-promo-step--2 { height: 42px; background: #D9DEE3; font-size: 17px; animation-delay: 240ms; }
.lb-promo-step--3 { height: 30px; background: #E2A766; font-size: 17px; animation-delay: 120ms; }
@keyframes lb-promo-step-rise {
  from { transform: translateY(14px) scaleY(0.55); opacity: 0; }
  to   { transform: translateY(0) scaleY(1); opacity: 1; }
}
html.reduce-motion .lb-promo-step { animation: none !important; }
.lb-promo-title {
  font-family: var(--font-display);
  font-size: clamp(22px, 5vw, 30px);
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  margin: 0;
}
.lb-promo-text {
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  margin: 0;
}
.lb-promo-text b {
  font-family: var(--font-display);
  font-weight: 700;
  padding: 0 4px;
  background: linear-gradient(transparent 60%, var(--yellow) 60%);
}
.lb-promo-hint {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: 0.06em;
  opacity: 0.7;
  margin: -4px 0 0;
}
.lb-promo-actions {
  display: flex; flex-direction: column; gap: 8px;
  width: 100%;
  margin-top: 4px;
}
/* R5 redesign #9 : boutons = CTA V3 (ombre dure, press qui l'écrase façon
   navbar). Primaire vert, secondaire « ghost » paper. */
.lb-promo-btn {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 12px 18px;
  border: var(--border-thick);
  background: var(--paper-2);
  color: var(--ink);
  box-shadow: 4px 4px 0 var(--ink);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
html.dlp-can-hover .lb-promo-btn:hover {
  transform: translate(-2px, -2px);
  box-shadow: 6px 6px 0 var(--ink);
}
.lb-promo-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 1px 1px 0 var(--ink);
  transition-duration: 70ms;
}
.lb-promo-btn--primary {
  background: var(--green);
  color: var(--ink);
}
.deputy-photo {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  user-select: none;
}
/* Legacy : .scan-lines neutralisé (effets photo via pseudos ci-dessous). */
.scan-lines { display: none; }

/* ── Effets photo combo (reset from scratch) ───────────────────────────────
   Empilage de 4 effets sur les deux wrappers photo (jeu + carte suivante
   en shadow) :
   - box-shadow inset blanc fort : inner glow clair (halo lumineux interne)
   - ::before = stippling fin (turbulence haute fréquence) + scan lines
                 fines (linear-gradient 1 sur 4 px) en multiply
   - ::after  = overlay couleur chaude (tint doré, multiply)
   Aucun filter sur l'image elle-même : la couleur d'origine est préservée,
   les effets se posent par dessus. */
.photo-wrap, .shadow-photo-wrap {
  box-shadow: inset 0 0 60px 12px rgba(255,255,255,0.58);
}

.photo-wrap::before, .shadow-photo-wrap::before {
  content: '';
  position: absolute; inset: 0;
  /* Stippling fin (turbulence haute fréquence, comme G14) + scan lines fines
     (repeating linear, comme G15). Empilés dans un même pseudo via background
     multi-layer. */
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence baseFrequency='1.8' numOctaves='1' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 -0.8'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"),
    repeating-linear-gradient(0deg, rgba(0,0,0,0) 0 3px, rgba(0,0,0,0.10) 3px 4px);
  background-size: 120px 120px, 100% 4px;
  mix-blend-mode: multiply;
  opacity: 0.50;
  pointer-events: none;
  z-index: 1;
}
/* En mode shiny / holo : on retire à la fois les scan lines ET le stippling
   pour ne pas casser l'effet d'iridescence — seul l'overlay tint warm
   (cf. ::after) et l'inner glow restent. */
.deputy-card.shiny .photo-wrap::before,
.deputy-card.holo  .photo-wrap::before,
.deputy-card-shadow.shiny .shadow-photo-wrap::before,
.deputy-card-shadow.holo  .shadow-photo-wrap::before {
  background: none;
}
/* ── Fond photo · axe D rareté · HOLO (override placé APRÈS la règle
   `background: none` ci-dessus pour gagner la cascade — sinon le pattern
   est supprimé). Le pseudo ::before, libre en mode holo, porte ici les
   stripes pastel rainbow 115° + animation barber-pole via transform:
   translate (interpolation sub-pixel native).
   • z-index: 0 (override du z-index: 1 par défaut sur ::before, sinon le
     pattern passe PAR-DESSUS la photo).
   • inset: -130px (> translation max 127px, marge ~3px) : surface dépassant
     juste assez le wrap → la translation animée ne révèle jamais de gap au
     bord. Avant : -200px (overdraw 73px gaspillé). Win 5 perf 2026-05-17 :
     réduction de la paint area (380×380 → 246×246 sur card 240×240).
   • Sync inter-wraps via même animation-delay -0.6s. */
.deputy-card.holo .photo-wrap::before,
.deputy-card-shadow.holo .shadow-photo-wrap::before {
  background: repeating-linear-gradient(115deg,
    rgba(255,  0,  0, 0.22)  0   14px,
    rgba(255,255,  0, 0.22) 14px 28px,
    rgba(  0,255,  0, 0.22) 28px 42px,
    rgba(  0,191,255, 0.22) 42px 56px,
    rgba(140,  0,255, 0.22) 56px 70px) 0 0 / auto repeat;
  inset: -130px;
  z-index: 0;
  opacity: 1;
  mix-blend-mode: normal;
  animation: holo-pole 5.6s linear -0.6s infinite;
}
@keyframes holo-pole {
  0%   { transform: translate(0,        0);       }
  100% { transform: translate(126.88px, 59.16px); }
}
.photo-wrap::after, .shadow-photo-wrap::after {
  content: '';
  position: absolute; inset: 0;
  /* Overlay tinted warm (comme proposition I) — teinte chaude légère qui
     unifie les photos sans assombrir trop. */
  background: rgba(245, 195, 110, 0.24);
  mix-blend-mode: multiply;
  pointer-events: none;
  z-index: 1;
}
.corner {
  position: absolute;
  width: 16px; height: 16px;
  border: 3px solid var(--ink);
  pointer-events: none;
}
.corner.tl { top: 8px; left: 8px; border-right: 0; border-bottom: 0; }

/* Stamp ✓/✗ on card — neo-brutalist block */
.stamp {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) rotate(-8deg) scale(0.3);
  width: clamp(140px, 28vw, 240px);
  height: clamp(140px, 28vw, 240px);
  padding: 22px;
  border: 6px solid var(--ink);
  border-radius: 0;
  opacity: 0;
  pointer-events: none;
  z-index: 10;
  box-shadow: 10px 10px 0 0 var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
}
.stamp svg { width: 100%; height: 100%; display: block; }
.stamp[data-tone="correct"] { background: var(--green); color: var(--ink); }
.stamp[data-tone="wrong"]   { background: var(--m-gauche); color: var(--ink); }
.stamp.show {
  animation: stampSlam 360ms cubic-bezier(.18,.89,.32,1.28) forwards;
}
@keyframes stampSlam {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(-8deg) scale(2.2); }
  55%  { opacity: 1; transform: translate(-50%, -50%) rotate(-8deg) scale(0.94); }
  78%  { transform: translate(-50%, -50%) rotate(-8deg) scale(1.04); }
  100% { opacity: 1; transform: translate(-50%, -50%) rotate(-8deg) scale(1); }
}
.corner.tr { top: 8px; right: 8px; border-left: 0; border-bottom: 0; }
.corner.bl { bottom: 8px; left: 8px; border-right: 0; border-top: 0; }
.corner.br { bottom: 8px; right: 8px; border-left: 0; border-top: 0; }

/* ── Post-it Indice ──────────────────────────────────────────────────────── */
/* Sibling de .deputy-card dans .card-stack → déborde librement du cadre.
   Layout vertical 4 lignes (icône en pavé noir · "Demander" · "un indice"
   · prix). Animation : sway constant + respiration (durée FIXE pour ne
   pas restart sur chaque update --hint-amp, c'est ça qui causait le
   stagger). Seule l'amplitude est animée par JS. */
/* Le wrapper EST le post-it (bg, border, padding, rotation, animations).
   Les enfants (message + bouton) sont des zones de contenu transparentes
   À L'INTÉRIEUR de la même boîte → quand le bouton se rétracte, il rentre
   "dans" le message sans dédoublement visuel. */
.hint-postit-wrap {
  position: absolute;
  top: -10px;
  right: 4px;
  z-index: 25;
  background: var(--yellow);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  padding: 6px 8px;
  /* Hauteur verrouillée à la hauteur naturelle du contenu bouton, peu importe
     que le bouton soit visible ou rétracté. Empêche tout grossissement de
     hauteur du post-it. Border-box → ces 110px incluent padding + border. */
  min-height: 110px;
  max-height: 110px;
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 0;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  line-height: 1.1;
  rotate: 4deg;
  scale: 1;
  animation: hintSway 3.6s ease-in-out infinite,
             hintBreathe 1.6s ease-in-out infinite alternate;
  transition: opacity 200ms ease, transform 280ms cubic-bezier(.34, 1.56, .64, 1),
              translate 200ms cubic-bezier(.2, .9, .3, 1.1), box-shadow 200ms ease;
  pointer-events: none;
}
@keyframes hintSway {
  0%, 100% { rotate: 3deg; }
  50%      { rotate: 5deg; }
}
@keyframes hintBreathe {
  from { scale: 1; }
  to   { scale: calc(1 + var(--hint-amp, 0.02)); }
}

/* Bouton (carrier du clic + contenu icône/libellé/prix). Pas de bg/border :
   c'est le wrap qui fait le post-it. Peut se rétracter sans casser la boîte.
   align-self: center → garde sa hauteur naturelle même si le wrap est en
   align-items: stretch (pour que SEUL le message s'étire à la hauteur du
   bouton pour le séparateur — pas l'inverse). */
.hint-postit {
  pointer-events: auto;
  align-self: center;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  line-height: inherit;
  cursor: pointer;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  max-width: 240px;
  opacity: 1;
  overflow: hidden;
  transition: max-width 320ms cubic-bezier(.4, 0, .2, 1),
              opacity 200ms ease,
              margin-left 320ms cubic-bezier(.4, 0, .2, 1);
}
.hint-postit:focus-visible {
  filter: brightness(0.96);
}
/* Hover (souris/trackpad) : le post-it se SOULÈVE et son ombre grandit — langage
   néo-brut « lift » identique aux autres boutons (avant : simple brightness à peine
   visible, perçu « cassé »). On n'agit que sur translate/box-shadow du WRAP : le
   sway/breathe pilote rotate/scale, donc aucun conflit avec l'animation. */
html.dlp-can-hover .hint-postit-wrap:has(.hint-postit:hover) {
  translate: -3px -3px;
  box-shadow: 10px 10px 0 var(--ink);
}
html.dlp-can-hover .hint-postit:hover {
  filter: brightness(0.97);
}
/* Hint-out : le bouton se rétracte horizontalement (max-width 0) et fade.
   Visuellement il "rentre" dans la zone message (à sa gauche). Le wrap
   reste un post-it cohérent, juste plus court. */
.hint-postit.hint-out {
  max-width: 0;
  opacity: 0;
  pointer-events: none;
  margin-left: 0;
}

/* Zone message — sibling du bouton, à sa gauche. Pas de bg/border (le
   wrap fait le décor). Apparait en sliding (max-width 0 → 210px). */
.hint-postit-message {
  pointer-events: auto;
  align-self: stretch;                /* prend toute la hauteur du wrap pour
                                         que le padding vertical s'applique */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 16px;
  text-transform: none;
  letter-spacing: 0.01em;
  line-height: 1.2;
  color: var(--ink);
  max-width: 0;
  /* Padding 0 quand replié : sinon padding 3/4 reste effectif même avec
     max-width:0 (border-box clip le contenu mais le padding pousse 8px
     horizontal d'espace fantôme à gauche du bouton, qui apparaissait
     décentré). Réappliqué quand .has-message via override plus bas. */
  padding: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-width 360ms cubic-bezier(.34, 1.56, .64, 1),
              padding 360ms cubic-bezier(.34, 1.56, .64, 1),
              opacity 200ms ease 120ms;
}
/* Largeur dispo pour le message — priorité : élargir le bandeau le plus
   possible (jusqu'à la limite carte) AVANT de shrink la font-size côté JS.
   La limite haute (320px / 240px mobile) approche la largeur visuelle d'une
   carte de jeu sans déborder du cadre photo. */
.hint-postit-wrap.has-message .hint-postit-message {
  max-width: 320px;
  /* Padding réappliqué seulement quand le message est visible (cf. règle
     base avec padding: 0). Le padding-top/bottom 3px évite que le texte
     touche les bords du post-it ; padding-left/right 4px donne de l'air
     latéral entre le texte et le séparateur. */
  padding: 3px 4px;
  opacity: 1;
}

/* Séparateur vertical : élément dédié, align-self: stretch pour matcher la
   hauteur naturelle du bouton (qui définit la hauteur du wrap). N'affecte
   PAS la hauteur du wrap (élément vide, natural height = 0 → ne pèse pas
   dans le max-children calcul de flex). */
.hint-postit-sep {
  width: 0;
  align-self: stretch;
  background: var(--ink);
  margin: 0;
  opacity: 0;
  transition: width 360ms cubic-bezier(.34, 1.56, .64, 1),
              margin 360ms cubic-bezier(.34, 1.56, .64, 1),
              opacity 200ms ease 120ms;
}
.hint-postit-wrap.has-message .hint-postit-sep {
  width: 2px;
  margin: 0 8px;
  opacity: 1;
}
/* Si le bouton est rétracté : séparateur disparait (plus rien à séparer). */
.hint-postit-wrap.has-message:has(.hint-postit.hint-out) .hint-postit-sep {
  width: 0;
  margin: 0;
  opacity: 0;
}
/* Si le wrap n'a plus de contenu (bouton retiré ET pas de message), on cache
   tout le post-it pour ne pas laisser un cadre vide. */
.hint-postit-wrap:not(.has-message):has(.hint-postit.hint-out) {
  opacity: 0;
  transform: scale(0);
  pointer-events: none;
}
@media (max-width: 860px) {
  .hint-postit-message { font-size: 12px; }
  .hint-postit-wrap.has-message .hint-postit-message {
    max-width: 180px;
    margin-right: 4px;
  }
}
/* Icône ? dans un pavé noir (matche le pavé prix). */
.hint-postit > .ico {
  background: var(--ink);
  color: var(--yellow);
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
.hint-postit .hint-line {
  white-space: nowrap;
}
.hint-postit .cost {
  font-family: var(--font-mono);
  font-size: 17px;
  font-weight: 700;
  background: var(--ink);
  color: var(--yellow);
  padding: 4px 8px;
  margin-left: 0;
  letter-spacing: 0;
  align-self: stretch;   /* prend la largeur dispo du flex column */
  text-align: center;
}

/* (Le menu déroulant des 3 indices a été remplacé par un clic unique
   sur le post-it qui élimine un macro/groupe faux dans l'hémicycle.
   Les anciennes règles .hint-menu et .hint-option ont été supprimées.) */

/* ── Post-it « Ratios » (GP-03 V1-D) ─────────────────────────────────────
   Nouveau post-it qui déclenche le redimensionnement proportionnel de
   l'hémicycle (indice « Pourcentages »). Mirror visuel du hint jaune côté
   deputy-card : positionné en top-left du #hemicycle-container, tilt -4°
   au lieu de +4°, sway décalé 1.8s pour éviter l'effet « essuie-glaces ».
   Couleur paper-2 (cousin doux du jaune) — différenciation par la forme
   de l'icône (camembert exploded) plutôt que par chromatique forte.
   Validé via proto B (label « Ratios », compact ~70 × 78 px). */
.pct-postit {
  position: absolute;
  top: -10px;
  left: 4px;
  z-index: 25;
  background: var(--paper-2);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  padding: 6px 7px 4px;
  min-height: 90px;
  display: inline-flex;
  /* stretch (pas center) pour que .pct-body remplisse toute la hauteur
     du post-it. Ainsi margin-top: auto sur le label a de l'espace à
     consommer pour pousser label+cost vers le bas. */
  align-items: stretch;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.1;
  rotate: -4deg;
  cursor: pointer;
  /* Sway décalé de 1.8s vs hint (3.6s ÷ 2) → désynchronise les 2 post-its
     pour éviter l'effet « 2 essuie-glaces parallèles ». */
  animation: pctSway 3.6s ease-in-out infinite -1.8s;
  transition: max-width 320ms cubic-bezier(.4, 0, .2, 1),
              opacity 200ms ease,
              box-shadow 180ms ease,
              transform 180ms ease;
  overflow: hidden;
  max-width: 240px;
}
@keyframes pctSway {
  0%, 100% { rotate: -3deg; }
  50%      { rotate: -5deg; }
}
html.reduce-motion .pct-postit { animation: none; }

html.dlp-can-hover .pct-postit:hover {
  box-shadow: 5px 5px 0 0 var(--ink);
  transform: translate(-2px, -2px) rotate(-4deg);
  animation-play-state: paused;
}
.pct-postit:focus-visible {
  outline: 2px solid var(--violet);
  outline-offset: 2px;
}
/* Mobile : le post-it garde sa taille pleine (min-height 90 = desktop
   normal, contenu lisible et bien espacé). On le décale plus haut via
   top: -50px pour qu'il déborde sur la zone du cadre photo au-dessus,
   tout en gardant la limite inférieure inchangée — léger overlap visuel
   sur le haut de l'arc, jamais sur les sub-groupes du centre.
   DOM order place .game-panel après .card-stack → pas besoin de
   z-index élevé pour passer au-dessus du cadre photo. */
@media (max-width: 860px) {
  .pct-postit {
    top: -50px;
    left: -2px;
  }
}
/* État rétracté : Active (indice consommé) / Locked (pas assez de pts) /
   Hidden (mode incompatible) — tous identiques visuellement comme
   .hint-postit.hint-out. Width 0 + opacity 0 + animation off. */
.pct-postit.pct-out {
  max-width: 0 !important;
  opacity: 0;
  pointer-events: none;
  padding-left: 0;
  padding-right: 0;
  border-left-width: 0;
  border-right-width: 0;
  animation: none;
  box-shadow: 0 0 0 0 var(--ink);
}
/* Bouton Ratios (pct-postit) pendant le reveal : ON NE LE CACHE PLUS (retour user
   2026-06-15) — le cadre de dépouillement le RECOUVRE entièrement, donc le shrink-out +
   réapparition n'apportait rien et causait un jump au retour. On garde juste
   pointer-events:none (clic intercepté par le cadre). pct-hidden-by-reveal n'est plus posée. */
.game-panel:has(.reveal-verdict.show) .pct-postit {
  pointer-events: none;
}
.pct-postit .pct-body {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  /* Pas de justify-content : on utilise margin-top: auto sur .pct-lbl
     pour pousser le bloc texte (label + cost) vers le bas. Résultat :
     icône en haut, espace flex auto, puis label + cost ancrés en bas. */
  gap: 2px;
  padding: 0;
}
.pct-postit .pct-ico {
  width: 34px;
  height: 34px;
  display: block;
  flex: 0 0 auto;
  color: var(--ink);
  margin: 0 auto;
}
.pct-postit .pct-lbl {
  font-size: 13px;
  white-space: nowrap;
  text-align: center;
  letter-spacing: 0.04em;
  margin-top: auto;  /* pousse label + cost vers le bas du post-it */
}
.pct-postit .pct-cost {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0;
  background: var(--ink);
  color: var(--paper-2);
  padding: 3px 6px;
  text-align: center;
  white-space: nowrap;
  margin-top: 2px;
}
/* État actif du toggle Ratios (refonte 2026-05-31) : l'hémicycle est en mode
   proportionnel → post-it rempli vert (= « activé »), ombre plus marquée. Le
   label affiche alors « Égaliser » (cf. updatePctCostDisplay). */
.pct-postit.is-active {
  background: var(--green);
  box-shadow: 5px 5px 0 0 var(--ink);
}
html.dlp-can-hover .pct-postit.is-active:hover { box-shadow: 6px 6px 0 0 var(--ink); }

/* ── Bouton « Passer » (top-left de la carte) ─────────────────────────────
   Petit sticker néo-brut perché sur le coin haut-gauche du cadre. Saute le RP
   sans pénalité (cf. skipQuestion). Masqué (.hidden) en Speed/Survie/Daily. */
.skip-rp {
  position: absolute;
  top: -10px;
  left: -8px;
  z-index: 30;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 9px 5px 7px;
  background: var(--paper-2);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1;
  rotate: -3deg;
  cursor: pointer;
  transition: box-shadow 160ms ease, transform 160ms ease;
}
.skip-rp .skip-ico { width: 14px; height: 14px; flex: 0 0 auto; display: block; }
.skip-rp .skip-lbl { white-space: nowrap; }
html.dlp-can-hover .skip-rp:hover {
  box-shadow: 5px 5px 0 0 var(--ink);
  transform: translate(-2px, -2px) rotate(-3deg);
}
.skip-rp:active {
  transform: translate(1px, 1px) rotate(-3deg);
  box-shadow: 2px 2px 0 0 var(--ink);
}
.skip-rp:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }

/* ── Right col / Panel ───────────────────────────────────────────────────── */
.right-col { display: flex; flex-direction: column; gap: var(--space-4); height: 100%; min-height: 0; }

.game-panel {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: var(--space-4);
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  position: relative;
  /* overflow: visible explicite pour que l'animation de pop du verdict
     puisse déborder du cadre (revealPop scale 1.15 → "burst out" du cadre).
     Sinon le default visible peut être implicitement clippé par un parent. */
  overflow: visible;
}
.panel-title {
  /* Caché PARTOUT (avant : caché seulement mobile). User feedback : superflu
     une fois le jeu compris, libère de la place verticale dans le game-panel. */
  display: none;
  font-family: var(--font-display);
  font-size: clamp(13px, 1.4vw, 18px); text-transform: uppercase;
  align-items: center; gap: 10px;
  flex-shrink: 0;
  white-space: nowrap;
  overflow: hidden;
}
.num-tag {
  font-family: var(--font-mono);
  font-size: 12px;
  background: var(--ink-2); color: var(--paper);
  padding: 2px 8px;
  flex-shrink: 0;
}

/* ── Hémicycle ───────────────────────────────────────────────────────────── */
.hemicycle-wrap {
  flex: none;
  aspect-ratio: 2 / 1;
  position: relative;
  overflow: visible;
}
.hemicycle-wrap svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  transition: opacity var(--dur-base);
}

/* ── Bouton retour hémicycle (mobile uniquement) ──────────────────────
   Post-it tilté -3deg, paper-2 (off-white plus chaud que le main paper),
   chevron-left hand-drawn (stroke 3px, linecap round) + label mono court.
   Apparait quand le wrap a .is-armed (un macro est sélectionné, les
   sub-groupes sont affichés). Display: none par défaut → activé seulement
   en media (max-width: 760px). Coexiste avec hint-postit (top-right) et
   lives-postit (top-left) sans collision : posé en bas-droite. */
/* Garde stricte : le bouton retour ne doit JAMAIS être visible sans
   .is-armed sur le wrap. !important volontaire — c'est un invariant, pas
   une préférence. Filet de sécurité au cas où un futur ajout oublierait
   de passer par _setArmedMacro() côté JS. Désactive aussi pointer-events
   pour éviter qu'un bouton invisible reste cliquable. */
.hemicycle-wrap:not(.is-armed) .hemicycle-back {
  display: none !important;
  pointer-events: none !important;
}

/* Post-it « Retour » : même langage que les autres post-its du jeu (Passer/Indice)
   — icône chevron + label, typo display, fond paper-2, bordure épaisse, ombre dure,
   tilt + sway léger. Affiché seulement quand un camp est armé (cf. .is-armed). */
.hemicycle-back {
  position: absolute;
  bottom: 4px;
  right: 6px;
  display: none;
  align-items: center;
  gap: 5px;
  padding: 5px 10px 5px 8px;
  background: var(--paper-2);
  color: var(--ink);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1;
  cursor: pointer;
  rotate: 3deg;
  z-index: 10;
  opacity: 0;
  pointer-events: none;
  /* Sway léger (comme Passer/Indice), désynchronisé. PAS de !important pour ne pas
     l'emporter sur l'override de hover. reduce-motion le coupe (cf. plus bas). */
  animation: backSway 3.9s ease-in-out infinite;
  transition: opacity 200ms var(--ease-out), scale 220ms cubic-bezier(.34,1.56,.64,1);  /* (item 4) scale-in/out */
  touch-action: manipulation;
  white-space: nowrap;
}
/* L'icône chevron : la règle legacy `.hemicycle-wrap svg{position:absolute;inset:0;
   width:100%}` (ancien hémicycle) capturait aussi cette icône → 73px, recouvrant le
   texte. On la remet en flux, 14px, à gauche du label (spécificité + !important). */
.hemicycle-back .hemicycle-back-ico {
  position: static !important;
  inset: auto !important;
  width: 14px !important;
  height: 14px !important;
  flex: 0 0 auto;
  display: block;
}
.hemicycle-back-label { display: inline-block; }
.hemicycle-back:active {
  animation: none;
  rotate: 3deg;
  translate: 2px 2px;
  box-shadow: 1px 1px 0 0 var(--ink);
}
/* Sway du post-it Retour (oscille autour de son tilt de repos +3deg). */
@keyframes backSway { 0%, 100% { rotate: 2deg; } 50% { rotate: 4deg; } }
html.reduce-motion .hemicycle-back,
html.anim-reduced .hemicycle-back { animation: none !important; rotate: 3deg !important; }
.pop-sector {
  transition: transform var(--dur-fast) var(--ease-out);
  transform-box: view-box;
  transform-origin: 50% 90%;
  will-change: transform;
}
/* Hover : seulement sur périphériques avec un vrai hover (souris/trackpad).
   Sur tactile (iOS, Android), :hover reste collé après le tap → quand un
   sub-group apparaît sous le doigt à l'expansion d'un macro, il prend tout
   de suite l'état hover et se présente comme pré-sélectionné.
   Gate via html.dlp-can-hover (posé en bootstrap inline si matchMedia hover
   et hors fenêtre 'Aperçu mobile' du panneau cheats). */
/* B — Lift au lieu de scale plat. translateY -3px au sommet (transform-origin
   50% 90%) + scale léger 1.05. Sensation "le sector se soulève vers le clic"
   plus perceptible qu'un simple scale uniforme. */
html.dlp-can-hover .pop-sector:hover  { transform: translateY(-3px) scale(1.05); }
html.dlp-can-hover .hemicycle-wrap svg.easy .macro:hover { transform: translateY(-3px) scale(1.05); }
html.dlp-can-hover .hemicycle-wrap svg.animating .pop-sector:hover { transform: none; }
.hemicycle-wrap svg.easy .macro {
  transition: transform var(--dur-fast) var(--ease-out);
  transform-box: view-box;
  transform-origin: 50% 90%;
  cursor: pointer;
  will-change: transform;
}
.hemicycle-wrap svg.easy .macro:active { transform: scale(0.94); transition-duration: 70ms; }
.hemicycle-wrap svg.animating .pop-sector { transition: none; }
.pop-sector.armed  { transform: scale(1.12); }
.pop-sector:active { transform: scale(0.94); transition-duration: 70ms; }
.pop-sector.armed-pulse { animation: armedPulse 700ms infinite alternate; }
@keyframes armedPulse {
  from { transform: scale(1.08); }
  to   { transform: scale(1.18); }
}

.sector-reveal-correct,
.sector-reveal-wrong {
  transform-box: view-box;
  transform-origin: 50% 90%;
}
.sector-reveal-correct { animation: sectorCorrect 0.45s var(--ease-pop) forwards; }
.sector-reveal-wrong   { animation: sectorWrong 0.38s ease-out forwards; }
@keyframes sectorCorrect {
  0% { transform: scale(1); }
  50% { transform: scale(1.13); }
  100% { transform: scale(1.04); }
}
@keyframes sectorWrong {
  0%, 100% { transform: scale(1); }
  25% { transform: scale(0.95) rotate(-2deg); }
  60% { transform: scale(0.98) rotate(1deg); }
}

/* ── Élimination (indices) — flash puis grisage ─────────────────── */
.elim-flash {
  transform-box: view-box;
  transform-origin: 50% 90%;
  animation: elimFlash 700ms ease-out forwards;
}
@keyframes elimFlash {
  0%   { filter: brightness(1)   drop-shadow(0 0 0 transparent); transform: scale(1); }
  20%  { filter: brightness(2.2) drop-shadow(0 0 14px var(--red)); transform: scale(1.12); }
  40%  { filter: brightness(0.6); transform: scale(0.95); }
  60%  { filter: brightness(2.2) drop-shadow(0 0 14px var(--red)); transform: scale(1.08); }
  80%  { filter: brightness(0.6); transform: scale(0.96); }
  100% { filter: brightness(1); transform: scale(1); }
}
.eliminated {
  pointer-events: none;
}
.eliminated path {
  filter: grayscale(1);
  transition: opacity 200ms ease-out, fill 200ms ease-out;
}

/* Indication "Touche une famille…" : retirée pour libérer la place sous
   l'hémicycle. L'élément reste dans le DOM (accédé par app.js) mais caché. */
.hover-hint { display: none !important; }

/* ── Reveal screen — overlay over l'aire hémicycle.
   Position absolute relative au .game-panel pour que :
     1. Le cadre garde sa taille (hemicycle stays in flow via visibility:hidden)
     2. Le reveal sit dans la zone hémicycle, pas au-dessous
   Pas de bg/border/shadow propre = transparent, sit dans le cadre game-panel.
   .reveal-verdict et .reveal-progress sont SORTIS de .reveal-screen (siblings
   in game-panel) pour pouvoir attaches directement au game-panel (verdict en
   post-it qui déborde du cadre, progress collée au bord bas). */
.reveal-screen {
  position: absolute;
  /* Desktop : top à 138px (108 + 30 d'air sup, user retour). Le verdict
     suit (top: 22 = -8 + 30). Le contenu reveal descend donc de 30px par
     rapport à la version précédente. Mobile a son propre override (top:76). */
  top: 138px;
  left: var(--space-4);
  right: var(--space-4);
  bottom: var(--space-4);
  padding: 0;
  gap: var(--space-3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 250ms var(--ease-out);
  display: none;
  overflow: visible;
  z-index: 2;
  /* R3.2 — perspective pour que le flip 3D de .reveal-party soit visible
     (sinon rotateY rend une projection plate sans profondeur). 800px =
     valeur classique pour effet de carte qui se retourne. */
  perspective: 800px;
}
/* Reveal overlay bg — placé DANS #hemicycle-container, couvre exactement
   l'hémicycle via inset 0. Couleur identique au cadre game-panel
   (var(--paper)). Fade-in à l'apparition du reveal, fade-out au retrait
   — via :has() sur la game-panel qui detecte l'état .visible sur
   reveal-screen. L'hémicycle ne bouge JAMAIS (reste visible derrière
   ce bg). hemicycle-container a z-index: 0 pour créer son propre
   stacking context — sans ça, le bg ici à z-index 1 risquerait de
   couvrir le reveal-screen (z-index 2 dans game-panel). */
/* z-index: 2 = au-dessus de .deputy-card (z:1) pour que .pct-postit
   (z:25 dans ce stacking context) passe par-dessus le cadre photo
   quand il déborde vers le haut sur mobile. Reste sous .reveal-verdict
   (z:30) et .reveal-screen (z:2, départagé par DOM order — wrap avant). */
.hemicycle-wrap { z-index: 2; }
.reveal-overlay-bg {
  position: absolute;
  /* Limites adaptées au cadre game-panel : déborde de 4 px en haut /
     gauche / droite pour couvrir le socle hémicycle (offset 4), mais
     PAS en bas. Le débordement vers le bas (talons + drop shadow du
     socle) reste visible derrière le voile crème — préserve le
     bord-bottom du game-panel + sa box-shadow + la barre rouge
     .reveal-progress qui sinon étaient masqués. */
  inset: -4px -4px 0 -4px;
  background: var(--paper);
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1);
  z-index: 1;
}
.game-panel:has(.reveal-screen.visible) .reveal-overlay-bg {
  opacity: 1;
}
/* Bouton retour pendant le reveal : ON NE LE CACHE PLUS (retour user 2026-06-15). Le cadre
   de dépouillement (plus haut) le RECOUVRE entièrement → le faire disparaître/réapparaître
   ne servait à rien et provoquait un jump au retour. Il reste donc en place (masqué par le
   cadre). pointer-events:none par sécurité (clic intercepté par le cadre de toute façon). */
.game-panel:has(.reveal-screen.visible) .hemicycle-back {
  pointer-events: none;
}
/* Wrappers col-left/col-right transparents → items positionnés par grid-area.
   Mobile : empilage vertical 5 lignes (verdict, name, party, sub, sigle).
   Desktop ≥ 860px : 2 colonnes (R2 layout adaptatif) — verdict + party
   pills à GAUCHE, name + sub-info à DROITE. Donne une carte d'identité
   compacte type Pokédex au lieu d'un long flux vertical. */
.reveal-col-left,
.reveal-col-right { display: contents; }
.reveal-screen.visible .reveal-name          { grid-area: name; }
.reveal-screen.visible .reveal-party         { grid-area: party; }
.reveal-screen.visible .reveal-party-sub     { grid-area: sub; }
.reveal-screen.visible .reveal-groupe-sigle  { grid-area: sigle; }
.reveal-screen.visible {
  display: grid;
  /* Layout 3-row (verdict est sorti, positionné en absolute sur game-panel).
     Name pleine largeur, puis party + sub-info en 2-col bas. */
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "name    name"
    "party   sub"
    "party   sigle";
  column-gap: 16px;
  row-gap: 8px;
  align-items: start;
  /* align-content: start UNIFIÉ (mobile + desktop) → contenu collé en haut
     du reveal-screen (juste sous le verdict slap-on). Évite la grosse zone
     vide entre verdict et nom qu'on avait avec center. Cohérent avec
     l'unification des deux layouts mobile/desktop. */
  align-content: start;
  opacity: 1;
  pointer-events: auto;
}
@media (min-width: 860px) {
  .reveal-screen.visible { column-gap: 24px; }
}
/* Hemicycle déplacé en visibility:hidden (cf. plus bas), garde son espace.
   Hover-hint reste display:none (pas besoin de garder son espace, il est
   absolument positionné de toute façon). */
.game-panel:has(.reveal-screen.visible) .hover-hint {
  display: none;
}
.reveal-verdict {
  /* SORTI du grid reveal-screen — positionné absolute sur .game-panel pour
     déborder du cadre comme un post-it slap-on. Hidden par défaut, montré
     quand reveal-screen.visible (via :has).

     Taille UNIFIÉE mobile + desktop (clamp 48-72) — sur mobile (~400px viewport)
     8vw=32 → clampé à min 48 (gros et impactant). Sur desktop ~1000px+,
     8vw=80 → clampé à max 72 (tient dans le cadre sans déborder).

     max-width borné pour donner une borne réelle au fitRevealVerdict() : sans
     ça (width: fit-content) le scrollWidth = clientWidth, la mesure est inutile.
     Les verdicts longs ("Techniquement correct !", "Essaie encore !") sont
     ensuite shrinkés par JS jusqu'à rentrer dans cette borne. */
  position: absolute;
  top: 22px;
  left: 14px;
  z-index: 30;  /* au-dessus des post-its (hint/pct = 25) pour rester visible */
  display: none;
  font-family: var(--font-display);
  font-size: clamp(48px, 8vw, 72px);
  line-height: 1;
  text-transform: uppercase;
  padding: 12px 22px 14px;
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  width: fit-content;
  max-width: calc(100% - 28px);
  white-space: nowrap;
  transform: rotate(-2deg);
}
/* .show class posée par showRevealVerdictNow(JS) à T+0 dans answer() —
   verdict apparait AVANT le reste du reveal (showReveal à T+450). */
.reveal-verdict.show {
  display: inline-block;
  /* Anim par défaut si data-tone absent (très rare). */
  animation: revealPop 360ms var(--ease-pop) both;
}
.reveal-verdict[data-tone="correct"] { background: var(--green);  color: var(--ink); }
.reveal-verdict[data-tone="wrong"]   { background: var(--red);    color: var(--paper); }
.reveal-verdict[data-tone="almost"]  { background: var(--orange); color: var(--ink); }
/* almost-correct / almost-wrong = "quasi-victoire / quasi-défaite", stripes
   diagonales blanches/sombres pour signaler la nuance vs un verdict plein. */
.reveal-verdict[data-tone="almost-correct"] {
  background:
    repeating-linear-gradient(135deg, transparent 0 14px, rgba(255,255,255,0.28) 14px 28px),
    var(--green);
  color: var(--ink);
}
.reveal-verdict[data-tone="almost-wrong"] {
  background:
    repeating-linear-gradient(135deg, transparent 0 14px, rgba(255,255,255,0.18) 14px 28px),
    var(--red);
  color: var(--paper);
}
/* Campagne (retour user 2026-06-13) — DEUX nuances d'orange pour différencier
   « presque mais pas tout à fait » : near = mvt ADJACENT (orange→JAUNE, plus
   encourageant) ; far = bon camp mais mvt raté (orange→ROUGE, plus sévère). */
.reveal-verdict[data-tone="almost-near"] { background: #ffc24a; color: var(--ink); }
.reveal-verdict[data-tone="almost-far"]  { background: #ff6a3a; color: var(--ink); }
/* Bandeau « Dépouillement… » (retour user 2026-06-15) : placeholder NEUTRE (ardoise) qui
   OCCUPE le slot du verdict pendant la scène de scoring pour réserver sa HAUTEUR, évitant
   que le vrai verdict (qui surgit à la fin) ne pousse tout le contenu vers le bas. Même
   élément #reveal-verdict → même hauteur. Entrée DOUCE (revealPop, jamais le slam balistique)
   + léger pulse « en cours ». is-banner retiré quand le vrai verdict prend le relais. */
.reveal-verdict[data-tone="depouillement"] { background: var(--ink); color: var(--paper); }
.reveal-verdict.is-banner {
  animation: revealPop 320ms var(--ease-pop) both,
             bannerPulse 1.4s ease-in-out 320ms infinite !important;
}
@keyframes bannerPulse {
  0%, 100% { opacity: 0.82; }
  50%      { opacity: 1; }
}
@keyframes revealPop {
  /* R3.1+ — verdict avec WEIGHT : start plus petit (0.3 = plus de travel),
     overshoot peak plus dramatique (1.3 vs 1.18), rebound down plus profond
     (0.92), micro-bounce up (1.05) avant settle final. 5 keyframes au lieu
     de 4 = sensation "tampon lourd qui slam, rebondit, oscille, se pose".
     Combiné avec duration bumpée à 460ms pour le temps de FEEL le rebond. */
  0%   { opacity: 0; transform: rotate(-2deg) scale(0.3); }
  42%  { opacity: 1; transform: rotate(-2deg) scale(1.3); }
  62%  {              transform: rotate(-2deg) scale(0.92); }
  78%  {              transform: rotate(-2deg) scale(1.05); }
  100% {              transform: rotate(-2deg) scale(1); }
}

/* R1 — Verdict-specific : glow coloré pulsé + tilt opposé pour wrong.
   Tous utilisent revealBallistic (cf. keyframes plus bas) : animation en 2
   temps "balle lancée vers le haut puis retombe au sol", durée 600ms,
   impact (=touche le sol) à T+468ms. Le screen shake côté JS (Juice
   reactToVerdict) est retardé à T+468 pour synchroniser avec l'impact.
   Tilt diff entre correct/almost (-2deg) et wrong (+2deg) via --verdict-tilt. */
.reveal-verdict[data-tone="correct"] {
  --verdict-glow: 56, 211, 159;  /* var(--green) en RGB */
  --verdict-tilt: -2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1700ms ease-out 500ms;
}
.reveal-verdict[data-tone="almost"] {
  --verdict-glow: 255, 154, 60;  /* var(--orange) */
  --verdict-tilt: -2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1500ms ease-out 500ms;
}
.reveal-verdict[data-tone="wrong"] {
  --verdict-glow: 255, 59, 59;   /* var(--red) */
  --verdict-tilt: 2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1600ms ease-out 500ms;
}
/* almost-correct hérite du glow vert mais avec une pulse plus courte (moins
   de fanfare = "on accepte mais c'est un peu juste"). */
.reveal-verdict[data-tone="almost-correct"] {
  --verdict-glow: 56, 211, 159;
  --verdict-tilt: -2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1400ms ease-out 500ms;
}
/* almost-wrong : tilt opposé (=wrong), glow moins long. */
.reveal-verdict[data-tone="almost-wrong"] {
  --verdict-glow: 255, 59, 59;
  --verdict-tilt: 2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1300ms ease-out 500ms;
}
/* almost-near (mvt adjacent, orange-jaune) : lean encourageant (-2deg). */
.reveal-verdict[data-tone="almost-near"] {
  --verdict-glow: 255, 194, 74;
  --verdict-tilt: -2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1500ms ease-out 500ms;
}
/* almost-far (bon camp, mvt raté, orange-rouge) : lean sévère (+2deg). */
.reveal-verdict[data-tone="almost-far"] {
  --verdict-glow: 255, 106, 58;
  --verdict-tilt: 2deg;
  animation: revealBallistic 600ms both,
             revealVerdictGlow 1500ms ease-out 500ms;
}
@keyframes revealVerdictGlow {
  /* IMPORTANT : aligner sur --sh-md (= la règle statique de .reveal-verdict)
     pour qu'il n'y ait pas de jump visible quand l'animation termine et que
     la box-shadow revient à la valeur statique. Avant c'était --sh-sm partout
     dans le keyframe → fin d'animation = jump --sh-sm → --sh-md instantané. */
  0%   { box-shadow: var(--sh-md); }
  18%  { box-shadow: var(--sh-md), 0 0 28px 7px rgba(var(--verdict-glow), 0.85); }
  100% { box-shadow: var(--sh-md); }
}
@keyframes revealPopWrong {
  /* R3.1+ — wrong tilt droite (+2deg). Plus dramatique encore que correct :
     overshoot 1.34, rebound 0.88, oscillation visible. Stamp rageant.
     ⚠ Legacy : conservée pour rétrocompat éventuelle. Les .reveal-verdict
     utilisent désormais revealBallistic (animation 2-temps + screen shake). */
  0%   { opacity: 0; transform: rotate(2deg) scale(0.3); }
  40%  { opacity: 1; transform: rotate(2deg) scale(1.34); }
  60%  {              transform: rotate(2deg) scale(0.88); }
  76%  {              transform: rotate(2deg) scale(1.07); }
  100% {              transform: rotate(2deg) scale(1); }
}
/* ── Verdict ballistic : animation 2 temps "balle lancée + chute" ───────
   Métaphore 3D : le verdict est "loin" du joueur au départ (scale 0.2,
   opacity 0), monte vers lui en décélérant (gravité ralentit la balle),
   reste suspendu au peak, puis retombe brutalement et "touche le sol"
   à 78% (T+468ms sur 600ms). Le screen shake est déclenché côté JS au
   moment de l'impact pour synchroniser. Tilt diff via --verdict-tilt.
   Timing breakdown :
   • 0-55%  (330ms) : montée + décélération — ease-out cubic
   • 55-62% (42ms)  : pause au peak ("balle suspendue à l'apex")
   • 62-78% (96ms)  : chute brutale + accélération — ease-in cubic
   • 78-100% (132ms): petit rebond et settle final */
@keyframes revealBallistic {
  0%   {
    opacity: 0;
    transform: rotate(var(--verdict-tilt, -2deg)) scale(0.2);
    animation-timing-function: cubic-bezier(0.1, 0.6, 0.3, 1);
  }
  55%  {
    opacity: 1;
    transform: rotate(var(--verdict-tilt, -2deg)) scale(1.5);
    animation-timing-function: linear;
  }
  62%  {
    opacity: 1;
    transform: rotate(var(--verdict-tilt, -2deg)) scale(1.5);
    animation-timing-function: cubic-bezier(0.7, 0, 0.95, 0.7);
  }
  78%  {
    opacity: 1;
    transform: rotate(var(--verdict-tilt, -2deg)) scale(1.0);
    animation-timing-function: cubic-bezier(0.3, 0.5, 0.6, 1);
  }
  85%  {
    transform: rotate(var(--verdict-tilt, -2deg)) scale(0.94);
  }
  100% {
    transform: rotate(var(--verdict-tilt, -2deg)) scale(1.0);
  }
}
.reveal-name {
  font-family: var(--font-display);
  font-size: clamp(24px, 4vw, 44px);
  line-height: 1.05;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  /* R3.5 — typewriter : pas d'animation parent, chaque char anime indé.
     Si reduce-motion, JS fallback à textContent direct (= apparait instantané
     mais après le verdict, donc OK). */
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
}
/* R3.5 — chaque caractère du nom est wrappé en .typewriter-char par
   typewriterName(el, text) en JS. Vrai effet typewriter = char SNAP
   visible + cascade percevable. Per-char anim COURTE (90ms) avec snap
   d'opacity au tout début (5%) puis pop scale. Évite l'effet "fade lent
   collectif" précédent (220ms duration = 16+ chars en train de fade en
   parallèle = bouillie). */
.typewriter-char {
  display: inline-block;
  opacity: 0;
  animation: typewriter-char-in 90ms cubic-bezier(.2, 1.4, .6, 1) forwards;
  will-change: transform, opacity;
}
@keyframes typewriter-char-in {
  /* Opacity SNAP à 1 dès 5% (=4.5ms) → char visible quasi-instantanément.
     Scale pop continue jusqu'à 100% pour effet "frappe" élastique. */
  0%   { opacity: 0; transform: scale(0.4); }
  5%   { opacity: 1; transform: scale(0.6); }
  60%  { opacity: 1; transform: scale(1.18); }
  100% { opacity: 1; transform: scale(1); }
}
/* .reveal-party-wrap a été retiré du HTML — animation déplacée sur les items */
/* R3.2 — pill famille en flip 3D (rotateY 90→0) au lieu de slide latéral.
   Effet "carte qui se retourne pour révéler la famille" — plus dramatique
   que le slide générique, distinct des autres éléments. perspective sur
   le parent .reveal-screen pour que le flip 3D rende correctement. */
.reveal-party {
  animation: revealPartyFlip 480ms cubic-bezier(.34, 1.4, .64, 1) both 220ms;
  transform-origin: center center;
  backface-visibility: hidden;
}
@keyframes revealPartyFlip {
  0%   { opacity: 0; transform: rotate(-1deg) rotateY(85deg); }
  60%  { opacity: 1; transform: rotate(-1deg) rotateY(-12deg); }
  100% {              transform: rotate(-1deg) rotateY(0deg); }
}
/* R3.3 — sub (Parti X) : slide depuis le BAS au lieu de la gauche.
   Différencie visuellement la couche "metadata" du nom (qui slide gauche). */
.reveal-party-sub   { animation: revealSlideUp 400ms var(--ease-out) both 320ms; }
.reveal-party {
  align-self: flex-start;
  justify-self: start;  /* sinon le grid stretch et le pill prend toute la largeur */
  display: inline-block;
  width: fit-content;
  /* R2 — min-width pour éviter pills très courts (e.g. famille en 2-3 lettres
     au futur) qui se retrouveraient riquiquis et déséquilibreraient le visu. */
  min-width: 84px;
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(16px, 2.4vw, 26px);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  padding: 8px 14px 10px;
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  transform: rotate(-1deg);
}
/* Parti / Groupe sub-info — hiérarchie visuelle CLAIRE :
   parti = primary (plus gros, opaque), groupe = secondary (plus petit, dim).
   Libellés "Parti " / "Groupe " posés inline en JS (pas pseudo ::before)
   pour masquer l'élément quand l'info manque. */
.reveal-party-sub {
  font-family: var(--font-mono);
  font-size: 17px;          /* primary — plus visible */
  letter-spacing: 0.10em;
  text-transform: uppercase;
  font-weight: 700;
  margin-top: 6px;
  line-height: 1.1;
  opacity: 0.95;
  color: var(--ink);
}
.reveal-groupe-sigle {
  font-family: var(--font-mono);
  font-size: 12px;          /* secondary — plus petit que parti */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  margin-top: 2px;
  line-height: 1.1;
  opacity: 0.55;            /* dim → secondary visuel */
  color: var(--ink);
  /* R3.3 — sigle slide depuis le BAS (au lieu de la gauche) pour
     différencier des autres éléments. Arrive en dernier (400ms après
     le pop verdict). */
  animation: revealSlideUp 400ms var(--ease-out) both 400ms;
}
@keyframes revealSlideUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes revealSlide {
  from { opacity: 0; transform: translateX(-30px); }
  to   { opacity: 1; transform: translateX(0); }
}

.reveal-meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: 4px;
  animation: revealSlide 400ms var(--ease-out) both 350ms;
}
/* Auto-advance progress bar — sortie de reveal-screen, sibling direct du
   .game-panel pour coller au bord inférieur du cadre (containing block =
   game-panel position:relative). Display:none par défaut, montrée quand
   reveal visible (via :has). */
.reveal-progress {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  height: 6px;
  background: var(--ink);
  overflow: hidden;
  display: none;
  z-index: 3;
}
.game-panel:has(.reveal-screen.visible) .reveal-progress {
  display: block;
}
.reveal-progress-fill {
  height: 100%;
  background: var(--green);
  width: 100%;
  transform-origin: left;
  animation: progress var(--auto-dur, 3200ms) linear forwards;
}
/* Couleur de la barre de progression assortie au verdict (data-verdict posé
   en JS sur .game-panel, sibling de la progress bar). */
.game-panel[data-verdict="correct"] .reveal-progress-fill { background: var(--green); }
.game-panel[data-verdict="almost"]  .reveal-progress-fill { background: var(--orange); }
.game-panel[data-verdict="wrong"]   .reveal-progress-fill { background: var(--red); }
@keyframes progress {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

/* Bouton "Suivant" — post-it slap-on bottom-right du game-panel.
   Mirror du verdict (top-left) : même esprit overflow + tilt opposé.
   Display:none par défaut, montré quand reveal-screen.visible (via :has). */
.reveal-next {
  position: absolute;
  /* bottom -22px : button mostly below cadre, top edge croise la progress bar
     (à 6px du bottom). z-index 6 > progress z-index 3 → button visible
     PAR-DESSUS la barre. */
  bottom: -22px;
  right: 14px;
  z-index: 6;
  display: none;
  font-family: var(--font-display);
  background: #2e2e2e;  /* noir plus clair (#2e2e2e — au-dessus de --ink-2 #1a1a1a) */
  color: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 8px 16px 10px;
  cursor: pointer;
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  align-items: center;
  gap: 8px;
  transform: rotate(-2deg);  /* même tilt que le verdict (cohérence) */
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.game-panel:has(.reveal-screen.visible) .reveal-next {
  display: inline-flex;
}
.reveal-next:focus-visible {
  transform: rotate(-2deg) translate(-2px, -2px);
  box-shadow: var(--sh-lg);
}
html.dlp-can-hover .reveal-next:hover {
  transform: rotate(-2deg) translate(-2px, -2px);
  box-shadow: var(--sh-lg);
}
.reveal-next:active {
  transform: rotate(-2deg) translate(2px, 2px);
  box-shadow: var(--sh-sm);
  transition-duration: 70ms;
}
.reveal-next .ico {
  width: 14px;
  height: 14px;
  color: var(--paper);
}

/* ── Controls panel (stats compact 3 cards + modes) ───────────────────────── */
.controls-panel {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: var(--space-4);
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  justify-content: center;
}
.section-title {
  font-family: var(--font-display);
  font-size: clamp(13px, 1.4vw, 18px);
  text-transform: uppercase;
  letter-spacing: -0.01em;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.section-title.section-mode { margin-top: var(--space-3); }
.section-tag {
  background: var(--ink-2);
  color: var(--paper);
  padding: 4px 6px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
}
.section-tag .ico { width: 16px; height: 16px; }

.stats-compact { display: flex; gap: var(--space-3); }
.stat-card {
  background: var(--paper-2);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 10px 12px;
  flex: 1;
  min-width: 0;
  position: relative;
  overflow: visible;
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.stat-card[data-tone="points"]    { background: #4b3cd1; color: var(--paper); }
.stat-card[data-tone="streak"]    { background: var(--orange); }
.stat-card[data-tone="acc"]       { background: var(--green); }
.stat-card[data-tone="lives"]     { background: var(--red); color: var(--paper); }
.stat-card[data-tone="bulletins"] { background: var(--yellow); color: var(--ink); }

.stat-head {
  display: flex; justify-content: space-between; align-items: center;
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 700;
  opacity: 0.85;
}
.stat-head .ico {
  width: 22px; height: 22px;
  background: var(--ink-2); color: var(--paper);
  border-radius: 50%;
  /* grid + place-items: center centre mathématiquement exact, indépendant
     de la taille du conteneur. Marche identique desktop (22px) et mobile
     (18px via media query). */
  display: grid;
  place-items: center;
}
/* SVG remplit ENTIÈREMENT le cercle .ico : 22×22 = même taille que le
   container, zéro marge. Suppression de la marge de 1px qui était une
   source de désalignement perceptible (centre SVG ≠ centre .ico s'il y
   a 1px de marge non symétrique due au sub-pixel rounding du grid).
   Mobile descend à 18×18 via media query, mais le principe est le même :
   SVG ≡ container, pas de marge entre les deux. */
.stat-head .ico svg {
  width: 22px;
  height: 22px;
  display: block;
}
.stat-card[data-tone="points"] .stat-head .ico { background: var(--paper); color: #4b3cd1; }
.stat-card[data-tone="lives"]  .stat-head .ico { background: var(--paper); color: var(--red); }
.stat-card[data-tone="bulletins"] .stat-head .ico { background: var(--ink); color: var(--yellow); }
.stat-num {
  font-family: var(--font-display);
  font-size: clamp(22px, 2.6vw, 36px); line-height: 1;
  letter-spacing: -0.02em;
}
.stat-sub {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 700;
  opacity: 0.95;
}
.stat-sub-acc {
  display: flex;
  align-items: center;
  gap: 10px;
}
.stat-sub-acc .acc-good,
.stat-sub-acc .acc-bad {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 900;
}
.stat-sub-acc .acc-sep {
  opacity: 0.5;
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 900;
}
.stat-sub-acc .ico {
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0.9;
}
.stat-sub-acc .ico svg {
  width: 16px;
  height: 16px;
}
.stat-sub:not(.stat-sub-acc) span {
  font-family: var(--font-display);
  font-size: 16px;
  letter-spacing: -0.01em;
  font-weight: 900;
  text-transform: none;
  vertical-align: -1px;
}

.modes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
  flex-shrink: 0;
}
.mode-btn {
  font-family: var(--font-display);
  font-size: clamp(10px, 1.1vw, 13px); letter-spacing: 0.02em; text-transform: uppercase;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  padding: 8px 14px;
  cursor: pointer; color: var(--ink);
  /* Transitions étendues à background/color/border-color (cf. .family-pill). */
  transition: transform var(--dur-fast), box-shadow var(--dur-fast),
              background-color 180ms ease, color 180ms ease,
              border-color 180ms ease;
  display: flex; align-items: center; gap: 6px;
  white-space: nowrap;
  flex: 1;
  justify-content: center;
  min-width: 80px;
}
.mode-btn:focus-visible { transform: translate(-1px,-1px); box-shadow: 4px 4px 0 0 var(--ink); }
html.dlp-can-hover .mode-btn:hover { transform: translate(-1px,-1px); box-shadow: 4px 4px 0 0 var(--ink); }
.mode-btn:active:not(.active) {
  transform: translate(2px, 2px) scale(0.97);
  box-shadow: var(--sh-sm);
  transition-duration: 70ms;
}
.mode-btn.active {
  box-shadow: var(--sh-md); transform: translate(-2px,-2px);
}
.mode-btn.active[data-mode="normal"]      { background: var(--blue);     color: var(--paper); }
.mode-btn.active[data-mode="easy"]        { background: var(--green);    color: var(--ink); }
.mode-btn.active[data-mode="speed"]       { background: var(--yellow);   color: var(--ink); }
.mode-btn.active[data-mode="survie"]      { background: var(--m-gauche); color: var(--paper); }
.mode-btn.active[data-mode="daily"]       { background: var(--violet);   color: var(--paper); }
.mode-btn.active[data-mode="specialiste"] { background: var(--pink);     color: var(--ink); }
.mode-btn .ico { width: 16px; height: 16px; }

/* Sélecteur d'option de mode (Spécialiste : famille / Survie : vies /
   Speed : durée). Container + pills partagés.
   Ligne défilable (swipe horizontal) plutôt que wrap : cohérence avec
   .filters de stats.html / .lb-tabs de leaderboards.html — quand les pills
   ne tiennent pas en largeur, on scroll plutôt que de retourner à la ligne
   (préserve la hiérarchie visuelle d'un picker = 1 ligne). */
.specialiste-family-picker,
.specialiste-role-picker,
.mode-option-picker {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-top: 8px;
}
.specialiste-family-picker::-webkit-scrollbar,
.specialiste-role-picker::-webkit-scrollbar,
.mode-option-picker::-webkit-scrollbar { display: none; }
.specialiste-family-picker > *,
.specialiste-role-picker > *,
.mode-option-picker > * { flex-shrink: 0; white-space: nowrap; }
/* Le picker de rôle suit immédiatement le picker de bloc en mode Spécialiste —
   on resserre fortement l'interligne pour qu'ils forment un bloc visuel. */
.specialiste-role-picker { margin-top: 2px; }

/* Spécialiste actif → controls-panel prend sa hauteur intrinsèque (bloc + rôle
   pickers doublent la hauteur normale) et game-panel se contracte pour
   absorber le manque. On masque le titre « Quel est son groupe… » et on
   coupe l'aspect-ratio fixe de l'hémicycle (comportement mobile) pour que le
   SVG suive la hauteur disponible.
   PENDANT le reveal : on masque les sélecteurs bloc/rôle, le controls-panel
   reprend sa taille naturelle (score + modes seulement) et le game-panel
   revient au layout desktop normal → le reveal screen s'affiche tel quel,
   plus de troncature. */
.right-col:has(#specialiste-family-picker:not(.hidden)):not(:has(.reveal-screen.visible)) .controls-panel {
  flex: 0 0 auto;
}
.right-col:has(#specialiste-family-picker:not(.hidden)):not(:has(.reveal-screen.visible)) .game-panel {
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
}
.right-col:has(#specialiste-family-picker:not(.hidden)):not(:has(.reveal-screen.visible)) .game-panel .panel-title {
  display: none;
}
.right-col:has(#specialiste-family-picker:not(.hidden)):not(:has(.reveal-screen.visible)) .hemicycle-wrap {
  flex: 1 1 0;
  min-height: 0;
  aspect-ratio: unset;
  width: 100%;
}
/* Cache les pickers tant que le reveal screen est visible — libère l'espace
   du controls-panel pour qu'il ne pousse plus le game-panel à se rétrécir. */
.right-col:has(.reveal-screen.visible) #specialiste-family-picker,
.right-col:has(.reveal-screen.visible) #specialiste-role-picker {
  display: none;
}
.specialiste-family-picker .picker-label,
.specialiste-role-picker .picker-label,
.mode-option-picker .picker-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.7;
  margin-right: 2px;
}
.family-pill,
.role-pill,
.pick-pill {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  background: var(--paper);
  border: var(--border-thin);
  padding: 5px 10px;
  cursor: pointer;
  color: var(--ink);
  /* Transitions étendues à background/color/border-color : la bascule
     active/inactive est maintenant smooth (avant : instantanée). */
  transition: transform var(--dur-fast), box-shadow var(--dur-fast),
              background-color 180ms ease, color 180ms ease,
              border-color 180ms ease;
}
.family-pill:focus-visible,
.role-pill:focus-visible,
.pick-pill:focus-visible {
  transform: translate(-1px, -1px);
  box-shadow: 2px 2px 0 0 var(--ink);
}
html.dlp-can-hover .family-pill:hover,
html.dlp-can-hover .role-pill:hover,
html.dlp-can-hover .pick-pill:hover {
  transform: translate(-1px, -1px);
  box-shadow: 2px 2px 0 0 var(--ink);
}
.family-pill:active:not(.active),
.role-pill:active:not(.active),
.pick-pill:active:not(.active) {
  transform: translate(1px, 1px) scale(0.96);
  box-shadow: var(--sh-sm);
  transition-duration: 70ms;
}
/* « Tous » : neutre noir (pas de couleur de bloc associée). */
.family-pill.active[data-family="all"]    { background: var(--ink);     color: var(--paper); border-color: var(--ink); }
.family-pill.active[data-family="gauche"] { background: var(--m-gauche); color: var(--paper); border-color: var(--ink); }
.family-pill.active[data-family="droite"] { background: var(--m-droite); color: var(--paper); border-color: var(--ink); }
.family-pill.active[data-family="divers"] { background: var(--m-divers); color: var(--paper); border-color: var(--ink); }
.family-pill.active[data-family="facho"]  { background: var(--m-facho);  color: var(--paper); border-color: var(--ink); }
/* Rôle actif : violet sobre, mêmes codes (paper/ink) que les autres pickers. */
.role-pill.active { background: #4b3cd1; color: var(--paper); border-color: var(--ink); }
/* Survie : pill active = rouge (heart-tone). Speed : pill active = jaune. */
#survie-lives-picker  .pick-pill.active { background: var(--red);    color: var(--paper); border-color: var(--ink); }
#speed-duration-picker .pick-pill.active { background: var(--yellow); color: var(--ink);   border-color: var(--ink); }

.mode-desc {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--ink);
  opacity: 0.9;
  margin: 10px 2px 0;
  line-height: 1.4;
}

/* ── Mobile-only ─────────────────────────────────────────────────────────── */
.stats-trigger            { display: none; }
.mobile-footer-nav        { display: none; }
.stats-backdrop           { display: none; }
.stats-close-btn          { display: none; }
.stat-card .pin-tag       { display: none; }  /* pas de sélection sur desktop */

/* ── Combo system : intro center-screen + post-it pin (Balatro-like) ──────
   Palette procédurale heatmap : jaune → orange → rouge → rose néon → bleu
   flamme → cyan/blanc. Variables locales définies par tier puis consommées
   par .combo-intro et .combo-pin pour rester cohérents. */
.combo-intro, .combo-pin {
  --tier-bg: var(--yellow);
  --tier-fg: var(--ink);
  --tier-glow: rgba(255,210,63,0.55);
  --tier-stroke: var(--ink);
  --tier-accent: var(--orange);
}
[data-tier="1.5"] { --tier-bg: #ffd23f; --tier-fg: #0a0a0a; --tier-glow: rgba(255,210,63,0.55);  --tier-accent: #ffae00; }
[data-tier="2"]   { --tier-bg: #ff6b1a; --tier-fg: #fdf6e3; --tier-glow: rgba(255,107,26,0.65);  --tier-accent: #ffd23f; --tier-stroke: #0a0a0a; }
[data-tier="3"]   { --tier-bg: #ff3b3b; --tier-fg: #fdf6e3; --tier-glow: rgba(255,59,59,0.7);    --tier-accent: #ffd23f; --tier-stroke: #0a0a0a; }
[data-tier="5"]   { --tier-bg: #ff2a6d; --tier-fg: #fdf6e3; --tier-glow: rgba(255,42,109,0.75);  --tier-accent: #ffd23f; --tier-stroke: #0a0a0a; }
[data-tier="7"]   { --tier-bg: #0080ff; --tier-fg: #fdf6e3; --tier-glow: rgba(0,128,255,0.7);   --tier-accent: #ffffff; --tier-stroke: #0a0a0a; }
[data-tier="10"]  { --tier-bg: #00e5ff; --tier-fg: #0a0a0a; --tier-glow: rgba(0,229,255,0.9);   --tier-accent: #ff2a6d; --tier-stroke: #0a0a0a; }

/* ── Intro bumper : pop-in centre-écran, morphing vers la position du pin ─
   --morph-tx / --morph-ty : translation cible (mesurée en JS depuis le pin).
   Sans valeurs JS, l'animation morphe vers le coin haut-droit par défaut. */
.combo-intro {
  --morph-tx: 35vw;
  --morph-ty: -32vh;
  position: fixed;
  top: 38%;
  left: 50%;
  z-index: 600;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.4) rotate(-4deg);
  transition: none;
}
.combo-intro.show { animation: comboIntroMorph 1900ms cubic-bezier(.17,.89,.32,1.25) forwards; }
.combo-intro-card {
  position: relative;
  background: var(--tier-bg);
  color: var(--tier-fg);
  border: var(--border-thick);
  box-shadow: var(--sh-lg), 0 0 60px 8px var(--tier-glow);
  padding: 18px clamp(26px, 5vw, 44px) 22px;
  text-align: center;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: clamp(220px, 38vw, 380px);
  overflow: visible;
}
.combo-intro-mult {
  font-size: clamp(56px, 11vw, 110px);
  line-height: 0.92;
  display: flex; align-items: baseline; justify-content: center; gap: 0.05em;
}
.combo-intro-x { font-size: 0.62em; opacity: 0.85; }
.combo-intro-title {
  font-size: clamp(22px, 4vw, 36px);
  margin-top: 4px;
  letter-spacing: 0.12em;
}
.combo-intro-sub {
  margin-top: 8px;
  font-family: var(--font-body);
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0.01em;
  font-size: clamp(13px, 1.5vw, 16px);
  opacity: 1;
}
/* L'intro pop au centre, reste lisible ~700 ms, puis se déplace en
   shrinkant vers la position du pin (translate par variables CSS). À la
   fin (opacity 0 + scale petit), elle "atterrit" sur le pin qui prend
   le relais visuellement. */
@keyframes comboIntroMorph {
  /* Durée totale 1900 ms : pop ≈ 170 ms · settle ≈ 130 ms · HOLD ≈ 1200 ms
     · morph 280 ms · fade 120 ms. Hold étendu (était 728 ms) pour laisser
     le temps de lire le palier de combo (×3, ×5…) avant le morph vers le pin. */
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3)  rotate(-10deg); }
  9%   { opacity: 1; transform: translate(-50%, -50%) scale(1.15) rotate(-3deg); }
  16%  { opacity: 1; transform: translate(-50%, -50%) scale(1)    rotate(-3deg); }
  79%  { opacity: 1; transform: translate(-50%, -50%) scale(1)    rotate(-3deg); }
  94%  { opacity: 0.85; transform: translate(calc(-50% + var(--morph-tx) * 0.85), calc(-50% + var(--morph-ty) * 0.85)) scale(0.28) rotate(2deg); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--morph-tx)), calc(-50% + var(--morph-ty))) scale(0.18) rotate(6deg); }
}

/* ── Pin post-it : compact, accolé au score (Balatro-like) ────────────────
   État stable = display: inline-flex via .show, transform rotate(6deg)
   scale(1). Les anims (.show-pop / .bump / .leave) jouent sans 'forwards'
   pour revenir au state stable défini par .show — c'était la raison du
   bug "pin invisible au-delà de ×1.5". */
.combo-pin {
  position: absolute;
  top: -14px;
  right: -16px;
  /* 12 = au-dessus du pin-tag "Épinglée" (z-index 10) qui peut cohabiter
     sur la même stat-card. Le combo doit toujours rester visible — c'est
     l'info la plus dynamique. */
  z-index: 12;
  display: none;
  align-items: center;
  gap: 4px;
  padding: 6px 12px 7px;
  background: var(--tier-bg);
  color: var(--tier-fg);
  border: var(--border-thick);
  box-shadow: var(--sh-sm), 0 0 18px 2px var(--tier-glow);
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 1;
  letter-spacing: 0.02em;
  transform: rotate(6deg);
  transform-origin: 50% 50%;
  pointer-events: none;
  white-space: nowrap;
  opacity: 1;
}
.combo-pin.show     { display: inline-flex; }
.combo-pin.show-pop { animation: comboPinPop  360ms cubic-bezier(.17,.89,.32,1.4); }
.combo-pin.bump     { animation: comboPinBump 420ms cubic-bezier(.17,.89,.32,1.4); }
.combo-pin.leave {
  display: inline-flex;
  animation: comboPinLeave 420ms cubic-bezier(.55,.05,.7,.2) forwards;
}
/* Idle wobble : 2 keyframes + animation-direction:alternate → un seul
   aller-retour sinusoïdal continu, sans pause aux 4 paliers du
   précédent design. Keyframes en valeurs littérales (pas de
   calc(var(...))) pour permettre l'accélération GPU. will-change
   promeut le pin sur sa propre couche compositor. */
.combo-pin.show:not(.show-pop):not(.bump):not(.leave) {
  will-change: transform;
}
.combo-pin[data-tier="1.5"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle15 1.30s ease-in-out infinite alternate;
}
.combo-pin[data-tier="2"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle2 1.10s ease-in-out infinite alternate;
}
.combo-pin[data-tier="3"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle3 0.90s ease-in-out infinite alternate;
}
.combo-pin[data-tier="5"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle5 0.70s ease-in-out infinite alternate;
}
.combo-pin[data-tier="7"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle7 0.50s ease-in-out infinite alternate;
}
.combo-pin[data-tier="10"].show:not(.show-pop):not(.bump):not(.leave) {
  animation: comboPinIdle10 0.35s ease-in-out infinite alternate;
}
@keyframes comboPinIdle15 {
  from { transform: rotate(5.2deg) scale(0.985); }
  to   { transform: rotate(6.8deg) scale(1.015); }
}
@keyframes comboPinIdle2 {
  from { transform: rotate(4.7deg) scale(0.975); }
  to   { transform: rotate(7.3deg) scale(1.025); }
}
@keyframes comboPinIdle3 {
  from { transform: rotate(4deg) scale(0.96); }
  to   { transform: rotate(8deg) scale(1.04); }
}
@keyframes comboPinIdle5 {
  from { transform: rotate(3.2deg) scale(0.945); }
  to   { transform: rotate(8.8deg) scale(1.055); }
}
@keyframes comboPinIdle7 {
  from { transform: rotate(2.2deg) scale(0.925); }
  to   { transform: rotate(9.8deg) scale(1.075); }
}
@keyframes comboPinIdle10 {
  from { transform: rotate(1deg) scale(0.9); }
  to   { transform: rotate(11deg) scale(1.1); }
}
.combo-pin-num {
  /* Pas de stroke ni text-shadow : on garde le chiffre net et lisible. */
}
@keyframes comboPinPop {
  0%   { transform: rotate(-15deg) scale(0);    opacity: 0; }
  60%  { transform: rotate(10deg)  scale(1.25); opacity: 1; }
  100% { transform: rotate(6deg)   scale(1);    opacity: 1; }
}
@keyframes comboPinBump {
  0%   { transform: rotate(6deg)  scale(1); }
  35%  { transform: rotate(-4deg) scale(1.35); }
  70%  { transform: rotate(10deg) scale(0.95); }
  100% { transform: rotate(6deg)  scale(1); }
}
@keyframes comboPinLeave {
  0%   { transform: rotate(6deg)   scale(1)   translate(0,0);       opacity: 1; }
  30%  { transform: rotate(-18deg) scale(1.1) translate(8px,-6px);  opacity: 1; }
  100% { transform: rotate(40deg)  scale(0.6) translate(60px, 80px); opacity: 0; }
}

/* Mobile : pin à GAUCHE du chiffre. Wrap `.trigger-num-wrap` sert de
   contexte de positionnement. Padding-left appliqué quand le pin est
   `.show` → le chiffre est repoussé à droite pour libérer la place,
   et "point" reste lisible à sa position naturelle. */
.trigger-num-wrap {
  position: relative;
  display: inline-block;
  transition: padding-left 0.20s ease;
}
/* Padding scoped à `data-tone="points"` : `:has()` matche la structure DOM
   indépendamment du `display:none` posé sur le combo-pin pour les autres
   stats — sans ce scope, le padding fuyait sur streak/acc. */
.stats-trigger[data-tone="points"] .trigger-num-wrap:has(.combo-pin.show) {
  padding-left: 42px;
}
/* Tier ×1.5 = 4 caractères (×1.5) au lieu de 2 (×2/×3/etc.) → pin plus large.
   Compense le padding-left pour éviter le chevauchement sur le chiffre des points. */
.stats-trigger[data-tone="points"] .trigger-num-wrap:has(.combo-pin[data-tier="1.5"].show) {
  padding-left: 56px;
}
.stats-trigger .combo-pin {
  top: -8px;
  left: -6px;
  right: auto;
  font-size: 16px;
  padding: 4px 8px 5px;
}
/* Combo pin caché quand la stat affichée n'est pas "points" (le multiplicateur
   n'a de sens que pour les points). */
.stats-trigger:not([data-tone="points"]) .combo-pin { display: none !important; }

/* ── Flammes (≥ ×5) — gooey filter sur le post-it lui-même ─────────────
   Les `.cflame-p` sont enfants directs du post-it / card. Le filtre
   SVG (feMerge : outline + goo + SourceGraphic) trace UNE seule
   bordure continue autour de la silhouette gooey (cadre + flammes).
   La bordure CSS est mise transparente pour ne pas dupliquer le tracé.
   Trade-off accepté : les coins du cadre sont légèrement arrondis
   (effet gooey), mais le stroke reste cohérent avec celui de la flamme.
   drop-shadow enchaîné après pour le hard shadow + glow. */
.combo-pin[data-tier="5"],
.combo-pin[data-tier="7"],
.combo-pin[data-tier="10"] {
  filter: url(#cflame-fire-pin)
          drop-shadow(0 0 3px var(--tier-glow));
  border-color: transparent;
  box-shadow: none;
}
.combo-intro[data-tier="5"]  .combo-intro-card,
.combo-intro[data-tier="7"]  .combo-intro-card,
.combo-intro[data-tier="10"] .combo-intro-card {
  filter: url(#cflame-fire-intro)
          drop-shadow(0 0 10px var(--tier-glow));
  border-color: transparent;
  box-shadow: none;
}

/* Pastilles en gouttes verticales (border-radius asymétrique pour
   amorcer la forme de langue de flamme). Cachées par défaut, visibles
   uniquement à partir du tier ×5. `bottom: calc(100% - 8px)` → les
   particules démarrent 8 px à l'intérieur du post-it pour que la
   flamme émerge depuis l'intérieur du cadre, sans gap visible. */
.cflame-p {
  position: absolute;
  bottom: calc(100% - 8px);
  width: 11px;
  height: 15px;
  margin-left: -5.5px;
  background: var(--tier-bg);
  border-radius: 50% 50% 45% 45% / 65% 65% 35% 35%;
  pointer-events: none;
  animation: cflameRise var(--dur, 0.55s) var(--delay, 0s) linear infinite;
  will-change: transform;
  display: none;
}
.combo-pin[data-tier="5"]   .cflame-p,
.combo-pin[data-tier="7"]   .cflame-p,
.combo-pin[data-tier="10"]  .cflame-p,
.combo-intro[data-tier="5"]  .cflame-p,
.combo-intro[data-tier="7"]  .cflame-p,
.combo-intro[data-tier="10"] .cflame-p {
  display: block;
}
.combo-intro-card .cflame-p {
  width: 22px;
  height: 30px;
  margin-left: -11px;
  bottom: calc(100% - 16px);
}

@keyframes cflameRise {
  0%   { transform: translate(0, 0) scale(1, 1); opacity: 1; }
  40%  { transform: translate(calc(var(--tx, 0px) * 0.35), calc(var(--ty, -30px) * 0.45)) scale(0.76, 0.62); opacity: 1; }
  100% { transform: translate(var(--tx, 0px), var(--ty, -30px)) scale(0.84, 0.6); opacity: 0; }
}

/* ── Bumper « Combo perdu » ─────────────────────────────────────────────── */
.combo-lost {
  position: fixed;
  top: 38%;
  left: 50%;
  z-index: 590;
  pointer-events: none;
  display: none;
  align-items: center;
  gap: 10px;
  padding: 10px 22px 12px;
  background: var(--ink);
  color: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  font-family: var(--font-display);
  font-size: clamp(20px, 3.5vw, 30px);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  transform: translate(-50%, -50%) rotate(-2deg);
  opacity: 0;
}
.combo-lost.show {
  display: inline-flex;
  animation: comboLostBump 1100ms cubic-bezier(.17,.89,.32,1.25) forwards;
}
.combo-lost-x {
  color: var(--red);
  font-size: 1.2em;
  -webkit-text-stroke: 1px var(--paper);
}
@keyframes comboLostBump {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.5) rotate(-8deg); }
  15%  { opacity: 1; transform: translate(-50%, -50%) scale(1.12) rotate(-2deg); }
  25%  { transform: translate(-50%, -50%) scale(1) rotate(-2deg); }
  75%  { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(-2deg); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9) rotate(2deg); }
}

/* ── Achievement toast ───────────────────────────────────────────────────── */
.ach-toast-zone {
  position: fixed;
  bottom: 20px;
  right: 20px;
  /* z-index 700 > settings-backdrop (600) pour que les toasts (sync, succès,
     etc.) restent visibles AU-DESSUS du menu Paramètres. Sinon le toast
     "score synchronisé" était caché derrière le menu sur mobile. */
  z-index: 700;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--space-2);
  pointer-events: none;
}
/* Le reveal a priorité : on cache les toasts pendant qu'il est visible
   (sinon, en cas de cascade de succès, ils chevauchent le reveal).
   On masque la zone ET on met les anims en pause sur les toasts pré-existants,
   pour que le swipe-in soit bien visible au moment où le reveal disparaît. */
body:has(.reveal-screen.visible) .ach-toast-zone {
  visibility: hidden;
}
body:has(.reveal-screen.visible) .ach-toast {
  animation-play-state: paused !important;
}
.ach-toast {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: var(--space-3);
  display: flex;
  gap: var(--space-3);
  align-items: center;
  min-width: 280px;
  max-width: 360px;
  cursor: pointer;
  pointer-events: auto;   /* override parent .ach-toast-zone pointer-events:none */
  /* Desktop : swipe IN depuis le BAS (la zone est en bottom-right).
     Mobile : override dans la media query plus bas (swipe depuis le haut).
     Visible ~4.5s — assez long pour lire le nom + desc, sans rester
     plombé à l'écran. Stagger 320 ms entre toasts en cascade. */
  animation: toastInUp 560ms cubic-bezier(.34, 1.56, .64, 1) backwards,
             toastOutDown 420ms ease 4.5s forwards;
}
/* Sur mobile, les toasts apparaissent en HAUT (sous le titre) avec swipe-in
   depuis le haut. La media query DOIT être placée APRÈS la règle de base
   ci-dessus, sinon la cascade fait gagner la règle desktop (même spécificité). */
@media (max-width: 860px) {
  .ach-toast-zone {
    /* Collés en haut d'écran (env(safe-area-inset-top) pour iOS notch).
       On accepte de masquer le titre du jeu — un succès, c'est plus important. */
    top: calc(env(safe-area-inset-top, 0px) + 8px);
    bottom: auto;
    right: 12px;
    left: 12px;
    align-items: stretch;
    flex-direction: column;
  }
  .ach-toast {
    min-width: 0;
    max-width: 100%;
    animation: toastInDown 560ms cubic-bezier(.34, 1.56, .64, 1) backwards,
               toastOutUp   420ms ease 4.5s forwards;
  }
}
/* Click → dismiss immédiat (swipe-out rapide vers la droite). */
.ach-toast.dismiss {
  animation: toastDismiss 240ms cubic-bezier(.55, 0, .8, .2) forwards !important;
}
/* Cadre d'icône du toast = MÊME look que les cartes de succès V3 (page Succès) :
   carré PENCHÉ + ombre dure + fond coloré par RANG (mêmes couleurs que
   achievements-v3.css), glyphe ink. (retour user : cohérence toast ↔ cartes.) */
.ach-toast .ico-big {
  width: 44px; height: 44px;
  background: #cd8b5e; color: var(--ink);   /* fallback = bronze */
  display: grid; place-items: center;
  flex-shrink: 0;
  border: var(--border-thick);
  box-shadow: 3px 3px 0 var(--ink);
  transform: rotate(-3deg);
}
.ach-toast[data-tier="bronze"] .ico-big   { background: #cd8b5e; color: var(--ink); }
.ach-toast[data-tier="silver"] .ico-big   { background: #c2c8d0; color: var(--ink); }
.ach-toast[data-tier="gold"] .ico-big     { background: #f3c52e; color: var(--ink); }
.ach-toast[data-tier="platinum"] .ico-big { background: #9bd6e8; color: var(--ink); }
.ach-toast .ach-label {
  font-family: 'Space Mono', monospace;   /* V3, cohérent cartes de succès */
  font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase;
  opacity: 0.6;
}
.ach-toast .ach-name {
  font-family: var(--font-display);   /* Archivo Black, comme le nom des cartes */
  font-size: 15px; text-transform: uppercase;
}
.ach-toast .ach-desc {
  font-family: 'Space Mono', monospace;   /* V3, cohérent cartes de succès */
  font-size: 12px;
  margin-top: 2px;
}
/* Desktop : arrive depuis le BAS (la zone est en bottom-right). */
@keyframes toastInUp {
  from { transform: translateY(140px) rotate(2deg);  opacity: 0; }
  60%  { opacity: 1; }
  to   { transform: translateY(0)     rotate(0);     opacity: 1; }
}
@keyframes toastOutDown {
  to { transform: translateY(140px) rotate(-2deg); opacity: 0; }
}
/* Mobile : arrive depuis le HAUT (la zone est en top). */
@keyframes toastInDown {
  from { transform: translateY(-140px) rotate(-2deg); opacity: 0; }
  60%  { opacity: 1; }
  to   { transform: translateY(0)      rotate(0);     opacity: 1; }
}
@keyframes toastOutUp {
  to { transform: translateY(-140px) rotate(2deg); opacity: 0; }
}
/* Click → dismiss latéral (rapide, pour différencier de l'auto-out). */
@keyframes toastDismiss {
  to { transform: translateX(460px) rotate(6deg); opacity: 0; }
}

/* ── Score bump ──────────────────────────────────────────────────────────── */
.score-bump {
  position: absolute;
  top: -10px;
  left: 50%;
  font-family: var(--font-display);
  font-size: 28px;
  line-height: 1;
  padding: 2px 11px 6px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  pointer-events: none;
  z-index: 50;
  white-space: nowrap;
  animation: scoreBump 1.15s ease-out both;
  will-change: transform, opacity;
}
@keyframes scoreBump {
  0%   { transform: translateX(-50%) translateY(0)     rotate(var(--bump-rot, -6deg)) scale(0);    opacity: 0; }
  20%  { transform: translateX(-50%) translateY(0)     rotate(var(--bump-rot, -6deg)) scale(1.2);  opacity: 1; }
  40%  { transform: translateX(-50%) translateY(0)     rotate(var(--bump-rot, -6deg)) scale(1);    opacity: 1; }
  70%  { transform: translateX(-50%) translateY(-20px) rotate(var(--bump-rot, -6deg)) scale(1);    opacity: 1; }
  100% { transform: translateX(-50%) translateY(-58px) rotate(var(--bump-rot, -6deg)) scale(0.8);  opacity: 0; }
}

/* ── Speed timer (post-it jaune, sablier, millisecondes) ──────────────────
   Positionné en OVERHANG du cadre .hemicycle-wrap : top/right négatifs pour
   déborder du cadre comme un post-it slap-on (cohérent avec hint, lives,
   daily). Plus gros qu'avant (font 26 vs 22) pour porter visuellement.
   Hierarchie : z-index 30 le pose au-dessus de l'hémicycle.

   Animations en jeu :
   - .speed-timer wrapper : sway lent (oscillation -5deg↔-3deg, 4.2s) +
     breathe (scale 1↔1.04, 2.6s). Deux loops infinis qu'on combine en
     keyframe unique pour ne pas écraser le transform.
   - .speed-timer .ico (sablier) : flip 180° en 400ms + hold 600ms, cycle
     2s avec 2 flips pour boucler sans saut (0→180→360 = 0). 1 flip / s
     visuellement, synchronisé avec le décrément du compteur entier.
   - .speed-timer.warning (≤3s restant) : passe en rouge, scale pulse plus
     rapide (existant). Override le sway.
   - Bloc et contenu tournent ensemble : l'icône (en plus de son flip) suit
     le tilt -4deg du post-it. */
/* Sway partagé des post-its de carte (vies / daily) — anime la propriété
   `rotate:` (compose avec `transform: scale` du bump). Cohérent hint/skip. */
@keyframes postitSway {
  0%, 100% { rotate: -6deg; }
  50%      { rotate: -3deg; }
}
/* Idle dédié du post-it cœur (survie). Le sway partagé à 3° est imperceptible
   sur une forme compacte/quasi-symétrique comme le cœur (≠ rectangle daily où
   3° déplace visiblement les coins). On élargit donc l'amplitude (−9°↔−2°, soit
   ~7° vs 3°) et on ajoute un léger battement via la prop INDIVIDUELLE `scale:`
   (1↔1.05) — thématique « cœur qui bat », qui compose proprement avec le
   `transform: scale(1.18)` du `.bump` (perte de vie) sans le neutraliser. */
@keyframes livesHeartIdle {
  0%, 100% { rotate: -9deg; scale: 1; }
  50%      { rotate: -2deg; scale: 1.05; }
}
/* (retour user 2026-06-15) Perte de vie JOUÉE EN DERNIER : le cœur SAUTE (grossit) puis
   RETOMBE SEC (squash) — animée sur la prop `scale:` (compose avec le rotate de base), elle
   remplace l'idle le temps de l'anim (.life-lost retirée → l'idle reprend). */
.lives-postit.life-lost {
  animation: lifeLostJump 1150ms cubic-bezier(0.3, 1.25, 0.5, 1) both;
}
@keyframes lifeLostJump {
  0%   { scale: 1; }
  16%  { scale: 1.46; }     /* GROSSIT */
  22%  { scale: 1.46; }
  64%  { scale: 1.42; }     /* tenu gros pendant la CASSE en deux (lisible) */
  82%  { scale: 0.9; }      /* squash en se RECOLLANT */
  100% { scale: 1; }        /* taille normale */
}
/* ── CŒUR BRISÉ (retour user 2026-06-16) — à la perte de vie : le cœur grossit, se CASSE en
   deux moitiés (cassure zigzag), le float « -1 » part, puis les moitiés se RECOLLENT et le
   cœur reprend sa taille. Deux moitiés clippées (zigzag interlock), cachées en temps normal ;
   le cœur entier (.lives-heart-whole) s'efface le temps de la casse. ── */
.lives-heart .heart-half { opacity: 0; transform-box: view-box; transform-origin: 12px 13px; }
.lives-heart .heart-half .lives-heart-shape {
  fill: var(--red); stroke: var(--ink); stroke-width: 1; stroke-linejoin: round; stroke-linecap: round;
}
.lives-postit.life-lost .lives-heart-whole { animation: heartWholeHide 1150ms ease both; }
.lives-postit.life-lost .heart-half-l { animation: heartCrackL 1150ms cubic-bezier(.3, 1.3, .5, 1) both; }
.lives-postit.life-lost .heart-half-r { animation: heartCrackR 1150ms cubic-bezier(.3, 1.3, .5, 1) both; }
@keyframes heartWholeHide {
  0%, 16% { opacity: 1; }
  22%, 68% { opacity: 0; }    /* CASSÉ : entier caché, les moitiés prennent le relais (tenu) */
  82%, 100% { opacity: 1; }   /* RECOLLÉ */
}
@keyframes heartCrackL {
  0%, 16% { opacity: 0; transform: translate(0, 0) rotate(0deg); }
  22%     { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  40%, 66% { opacity: 1; transform: translate(-2.8px, 0.6px) rotate(-19deg); }  /* écarté (gauche), tenu */
  80%     { opacity: 1; transform: translate(0, 0) rotate(0deg); }              /* recollé */
  82%, 100% { opacity: 0; transform: translate(0, 0) rotate(0deg); }
}
@keyframes heartCrackR {
  0%, 16% { opacity: 0; transform: translate(0, 0) rotate(0deg); }
  22%     { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  40%, 66% { opacity: 1; transform: translate(2.8px, 0.6px) rotate(19deg); }    /* écarté (droite), tenu */
  80%     { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  82%, 100% { opacity: 0; transform: translate(0, 0) rotate(0deg); }
}
/* Float « −1 » rouge qui s'élève depuis le cœur (posé par spawnLifeLossFloat). */
.life-loss-float {
  position: fixed;
  transform: translate(-50%, -50%);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 30px;
  line-height: 1;
  color: var(--red);
  -webkit-text-stroke: 2.5px var(--ink);
  z-index: 9999;
  pointer-events: none;
  opacity: 0;
  white-space: nowrap;
}
.life-loss-float.go { animation: lifeLossFloat 1000ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }
@keyframes lifeLossFloat {
  0%   { opacity: 0; transform: translate(-50%, -50%)  scale(0.5) rotate(-8deg); }
  18%  { opacity: 1; transform: translate(-50%, -85%)  scale(1.2) rotate(-4deg); }
  65%  { opacity: 1; transform: translate(-50%, -155%) scale(1)   rotate(-2deg); }
  100% { opacity: 0; transform: translate(-50%, -215%) scale(0.9) rotate(0deg); }
}
.speed-timer {
  position: absolute;
  /* Top-DROITE de la carte (sorti de l'hémicycle, désormais enfant de .card-stack).
     Le top-GAUCHE est occupé par « Passer » + l'indice. */
  top: 10px;
  right: -16px;
  z-index: 45;
  padding: 10px 16px 11px;
  font-family: var(--font-display);
  font-size: 26px;
  color: var(--ink);
  line-height: 1;
  letter-spacing: -0.02em;
  display: inline-flex;
  align-items: center;
  gap: 4px;                      /* sablier collé au chiffre (retour user) → pastille moins large */
  white-space: nowrap;
  transform-origin: 60% 60%;
  animation: speedTimerSway 4.2s ease-in-out infinite;
}
.speed-timer::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--yellow);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  z-index: -1;
  transform-origin: center;
  transition: background-color 200ms;
  animation: speedTimerBreathe 2.6s ease-in-out infinite;
}
.speed-timer .ico,
.speed-timer .speed-num,
.speed-timer .speed-unit {
  position: relative; /* au-dessus du ::before */
}
.speed-timer .ico {
  width: 15px;                  /* sablier plus petit (retour user) → pastille moins large */
  height: 15px;
  flex: 0 0 15px;
  display: grid;
  place-items: center;
  /* Flip rythmé du sablier — sync 1 flip / seconde avec le compte à rebours.
     Mis en PAUSE quand le timer est arrêté (.paused : reveal, modale, pause). */
  animation: speedTimerFlip 2s linear infinite;
  transform-origin: center;
}
/* Timer à l'arrêt → le sablier se fige aussi (retour user 2026-06-14). */
.speed-timer.paused .ico { animation-play-state: paused; }
.speed-timer .ico > svg {
  width: 15px;
  height: 15px;
  display: block;
}
.speed-timer .speed-num {
  font-variant-numeric: tabular-nums;
  min-width: 40px;
  text-align: right;
  flex: 0 0 auto;
}
/* Centièmes en petit (retour user 2026-06-14) : la pastille est moins large. */
.speed-timer .speed-num .speed-dec {
  font-size: 0.5em;
  opacity: 0.6;
  font-weight: 700;
  vertical-align: baseline;
}
.speed-timer .speed-unit {
  font-size: 0.55em;
  opacity: 0.6;
  margin-left: -2px;
  flex: 0 0 auto;
}
.speed-timer.warning { color: var(--paper); }
.speed-timer.warning::before {
  background: var(--red);
  animation: timerPulse 380ms infinite alternate;
}
/* Sway lent du wrapper — léger balancement néobrut entre -5deg et -3deg
   sur l'axe du tilt naturel. Cohérent avec hint-postit. */
@keyframes speedTimerSway {
  0%, 100% { transform: rotate(-5deg); }
  50%      { transform: rotate(-3deg); }
}
/* Breathe sur le ::before (fond + border + shadow) : scale 1.04 max,
   subtle, sans déplacer le contenu (qui reste au-dessus en position
   relative). */
@keyframes speedTimerBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}
/* Flip sablier : 0°→180° en 400ms (= 20% de 2s), hold 600ms à 180°,
   puis 180°→360° en 400ms, hold 600ms à 360°. 360° = 0° donc le cycle
   reboucle sans saut. 1 flip de 180° toutes les secondes. */
@keyframes speedTimerFlip {
  0%   { transform: rotate(0deg); }
  20%  { transform: rotate(180deg); }
  50%  { transform: rotate(180deg); }
  70%  { transform: rotate(360deg); }
  100% { transform: rotate(360deg); }
}
@keyframes timerPulse {
  from { transform: scale(1); }
  to   { transform: scale(1.08); }
}

/* ── Confirm modal (changement de mode) ────────────────────────────── */
.confirm-backdrop {
  position: fixed;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(10,10,10,0.6);
  z-index: 250;
  padding: 20px;
}
.confirm-backdrop.show { display: flex; }
.confirm-card {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  padding: var(--space-5) var(--space-5) var(--space-4);
  max-width: 440px;
  width: 100%;
  text-align: center;
  animation: confirmIn 0.32s var(--ease-pop) both;
}
@keyframes confirmIn {
  from { transform: scale(0.7) rotate(-2deg); opacity: 0; }
  to   { transform: scale(1) rotate(0); opacity: 1; }
}
.confirm-card h2 {
  font-family: var(--font-display);
  font-size: clamp(24px, 4vw, 32px);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-3);
}
.confirm-card p {
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.45;
  margin-bottom: var(--space-4);
  opacity: 0.85;
}
/* Description du mode cible — cadre néobrut discret pour distinguer
   l'avertissement (en haut) de la description-rappel (juste en dessous). */
.confirm-card .confirm-desc {
  font-size: 13px;
  font-family: var(--font-body);
  line-height: 1.4;
  margin-top: calc(-1 * var(--space-3));
  margin-bottom: var(--space-4);
  padding: 10px 12px;
  background: var(--paper-2);
  border-left: 4px solid var(--ink);
  opacity: 0.9;
  text-align: left;
}
.confirm-card .confirm-desc[hidden] { display: none; }
.confirm-buttons {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
}
.confirm-buttons button {
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 12px 22px;
  border: var(--border-thick);
  cursor: pointer;
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--sh-sm);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .confirm-buttons button:hover {
  transform: translate(-2px, -2px);
  box-shadow: var(--sh-md);
}
.confirm-buttons .btn-primary {
  background: var(--ink);
  color: var(--paper);
}

/* ── Score bump anchors sur Série et Précision ──────────────────────── */
.stat-card { position: relative; }

/* ── Daily post-it ───────────────────────────────────────────────────
   Compteur jaune tilté à gauche de la photo, mode Daily uniquement. */
.daily-postit {
  position: absolute;
  /* Top-DROITE de la carte (le top-gauche est pris par « Passer » + l'indice). */
  top: 14px;
  right: -18px;
  left: auto;
  background: var(--yellow);
  color: var(--ink);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 9px 13px 10px;
  z-index: 45;                /* AU-DESSUS de la carte (z:20) — plus « sous le stack ». */
  font-family: var(--font-display);
  text-align: center;
  min-width: 86px;
  pointer-events: none;
  rotate: -6deg;              /* tilt sur la prop `rotate:` → compose avec le scale du bump */
  animation: postitSway 4.3s ease-in-out infinite;
  transition: transform 280ms var(--ease-pop);
}
.daily-postit.bump { transform: scale(1.18); }
.daily-postit-label,
.daily-postit-num,
.daily-postit-sub { display: block; }
.daily-postit-label {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.7;
  margin-bottom: 4px;
}
.daily-postit-num {
  font-size: 26px;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.daily-postit-num .daily-postit-sep {
  opacity: 0.5;
  margin: 0 1px;
}
.daily-postit-sub {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-top: 4px;
  opacity: 0.7;
}

/* ── Lives post-it ── cœur découpé "papier kraft" ────────────────────
   Forme via SVG inline asymétrique (lobe gauche plus haut, pointe
   décalée). Le path porte fill rouge + stroke noir épais ; le chiffre
   est un <text> centré. Drop-shadow néobrut qui suit la silhouette
   (vs box-shadow rectangulaire). Pas de label, pas de "X/Y" : juste
   le nombre — si tu vois le cœur, t'es en survie. */
.lives-postit {
  --postit-rotate: -6deg;
  position: absolute;
  /* Top-DROITE de la carte (le top-gauche est pris par « Passer » + l'indice). */
  top: 8px;
  right: -22px;
  left: auto;
  width: 104px;
  height: 96px;
  z-index: 45;               /* AU-DESSUS de la carte (z:20). */
  pointer-events: none;
  rotate: var(--postit-rotate);   /* sur la prop `rotate:` → compose avec le scale du bump */
  scale: 1;                       /* prop individuelle animée par livesHeartIdle (battement) */
  animation: livesHeartIdle 3.4s ease-in-out infinite;
  transition: transform 280ms var(--ease-pop);
  filter: drop-shadow(6px 6px 0 var(--ink));
}
.lives-postit.bump { transform: scale(1.18); }
.lives-heart {
  width: 100%;
  height: 100%;
  overflow: visible;
  display: block;
}
.lives-heart-shape {
  fill: var(--red);
  stroke: var(--ink);
  /* Stroke-width en user units du viewBox 24×24. 1 unit ≈ 4.3px à
     l'échelle 104×96 desktop — épaisseur cohérente avec les autres
     post-its néobrut (--bw-thick = 4px). */
  stroke-width: 1;
  stroke-linejoin: round;
  stroke-linecap: round;
  transform-origin: 50% 50%;
  transform-box: view-box;
}
.lives-heart-label {
  /* Petit label "VIES" au-dessus du chiffre, dans la partie haute du corps
     du cœur (sous le V notch entre les lobes). Mono, paper, opacity réduite
     pour qu'il ne fight pas le chiffre dominant en-dessous. */
  font-family: var(--font-mono);
  font-size: 2.4px;
  font-weight: 700;
  fill: var(--paper);
  text-anchor: middle;
  dominant-baseline: central;
  letter-spacing: 0.15em;
  opacity: 0.78;
  pointer-events: none;
}
.lives-heart-num {
  font-family: var(--font-display);
  /* font-size en user units du viewBox 24 : 9.5 ≈ 40% de la hauteur du
     cœur. Render final ~41px en desktop. Position à y=13.5 = optiquement
     centré dans le corps du cœur (compensation du V au sommet qui rend
     le centre géométrique trop bas). */
  font-size: 9.5px;
  font-weight: 900;
  fill: var(--paper);
  text-anchor: middle;
  dominant-baseline: central;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

@media (max-width: 760px) {
  .daily-postit {
    top: 8px;
    right: 6px;
    left: auto;
    padding: 7px 10px 8px;
    min-width: 72px;
    rotate: -5deg;
  }
  .daily-postit.bump { transform: scale(1.15); }
  .daily-postit-num { font-size: 20px; }
  .lives-postit {
    --postit-rotate: -5deg;
    top: 4px;
    right: 4px;
    left: auto;
    width: 84px;
    height: 78px;
    filter: drop-shadow(5px 5px 0 var(--ink));
  }
  .speed-timer { top: 6px; right: 4px; }
}

/* ── Collection fly-in (carte shiny ou holographique : pop-in + shrink + fly) ──
   Phase 1 : pop-in BIG sur la carte avec label « +1 SHINY » / « +1 HOLO ».
   Phase 2 : shrink via .shrunk + envol vers le bouton Collection.
   La transition d'échelle entre les 2 phases est portée par l'animation JS
   (Web Animations API) — ici on règle juste l'apparence statique.
   `width` est posée explicitement en JS (mesurée à la création) et transite
   en CSS quand `.shrunk` est ajoutée → la box rétrécit smooth (avant : snap
   instantané dû au display:none des labels). */
.collection-fly {
  position: fixed;
  z-index: 600;
  pointer-events: none;
  background: var(--green);
  color: var(--ink);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 10px 16px;
  font-family: var(--font-display);
  font-size: 22px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transform: translate(-50%, -50%) scale(0);
  white-space: nowrap;
  overflow: hidden;
  box-sizing: border-box;
  transition: width 280ms cubic-bezier(.55, 0, .35, 1),
              padding 280ms cubic-bezier(.55, 0, .35, 1),
              font-size 280ms cubic-bezier(.55, 0, .35, 1);
}
/* B5.5 — mesure fullW/shrunkW dans flyCollection : transitions coupées via
   classe (PAS d'écriture inline de style.transition, quirk WAAPI iOS qui
   pouvait geler le sticker à scale(0), cf. commit b03b06a7). */
.collection-fly.measuring { transition: none !important; }
.collection-fly .ico { width: 18px; height: 18px; }

/* ── Loading ─────────────────────────────────────────────────────────────── */
/* Overlay PLEIN ÉCRAN (couvre le header + tout le contenu) pendant le chargement
   → l'utilisateur ne voit QUE le spinner, jamais d'éléments à demi-chargés /
   déplacés. À la fin (#loading.hidden), le contenu apparaît avec son anim
   d'entrée (reveal). Partagé jeu + Collection. */
#loading {
  position: fixed; inset: 0; z-index: 9999;
  background: var(--paper);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 20px; gap: 18px;
}
.spinner {
  width: 44px; height: 44px;
  border: 4px solid var(--paper-2);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading p {
  font-family: var(--font-mono);
  font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;
}

#error { display: flex; flex-direction: column; align-items: center; padding: 60px 20px; }
.error-box {
  background: var(--paper-2);
  border: var(--border-thick); box-shadow: var(--sh-lg);
  padding: 36px 44px; text-align: center;
}
.error-icon { font-size: 2.5rem; margin-bottom: 12px; }
#error-msg { font-family: var(--font-body); font-size: 1rem; margin-bottom: 20px; }
.btn-retry {
  font-family: var(--font-display);
  font-size: 15px; text-transform: uppercase;
  background: var(--ink-2); color: var(--paper);
  border: var(--border-thick); box-shadow: var(--sh-sm);
  padding: 12px 28px; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.btn-retry:focus-visible {
  transform: translate(-2px, -2px);
  box-shadow: var(--sh-md);
}
html.dlp-can-hover .btn-retry:hover {
  transform: translate(-2px, -2px);
  box-shadow: var(--sh-md);
}
.btn-retry:active {
  transform: translate(2px, 2px) scale(0.97);
  box-shadow: var(--sh-sm);
  transition-duration: 70ms;
}

/* ══════════════════════════════════════════════════════════════════════════
   GAME OVER / DAILY TERMINÉ — carte de score "style Balatro" (récap dense)
   Mêmes classes réutilisables (.go-*) pour les 2 modals (gameover + daily).
   ────────────────────────────────────────────────────────────────────────── */
.gameover-backdrop {
  position: fixed; inset: 0;
  background: rgba(10,10,10,0.78);
  z-index: 700;
  display: none;
  padding: var(--space-3);
  overflow-y: auto;
}
.gameover-backdrop.show {
  display: flex;
  /* safe center : centré quand ça rentre, sinon top-aligned (évite que le
     haut de la modale se fasse couper si elle dépasse le viewport — Safari
     historique ignorait safe et coupait, c'est désormais bien supporté). */
  align-items: safe center;
  justify-content: center;
  padding-top: max(var(--space-4), 4vh);
  padding-bottom: var(--space-4);
}
.gameover-card,
.go-card {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-lg);
  max-width: 520px;
  width: 100%;
  text-align: center;
  animation: revealPop 400ms var(--ease-pop) both;
  display: flex;
  flex-direction: column;
}

/* Direction C — collage post-it. Accent coloré PAR MODE (posé en variable sur
   la carte par showGameOver / hardcodé pour le daily) : survie=rouge,
   speed=jaune, daily=bleu, normal/facile=vert, entraînement=prune. Le ruban
   d'en-tête + le bouton Rejouer + le badge record consomment --go-accent. */
.go-card { --go-accent: var(--red); --go-accent-ink: var(--paper); }
.go-card--survie      { --go-accent: var(--red);    --go-accent-ink: var(--paper); }
.go-card--speed       { --go-accent: var(--yellow); --go-accent-ink: var(--ink);   }
.go-card--daily       { --go-accent: var(--blue);   --go-accent-ink: var(--paper); }
.go-card--normal      { --go-accent: var(--green);  --go-accent-ink: var(--ink);   }
.go-card--easy        { --go-accent: var(--green);  --go-accent-ink: var(--ink);   }
.go-card--specialiste { --go-accent: var(--purple); --go-accent-ink: var(--ink);   }

/* En-tête = RUBAN tamponné tilté (plus de barre pleine largeur). Centré,
   posé en haut du collage, légèrement remonté pour « chevaucher » le bord. */
.go-head {
  align-self: center;
  margin: 20px 0 4px;
  padding: 9px 22px 8px;
  border: var(--border-thick);
  box-shadow: 5px 5px 0 var(--ink);
  background: var(--go-accent);
  color: var(--go-accent-ink);
  display: inline-flex; flex-direction: column; align-items: center; gap: 2px;
  transform: rotate(-2deg);
}
.go-mode-tag {
  font-family: 'Space Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.85;
}
.go-title {
  font-family: var(--font-display);
  font-size: clamp(20px, 5vw, 38px);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  margin: 0;
  line-height: 1.05;
  /* Permet au titre de wrapper si tronqué (cas "DAILY TERMINÉ" sur iPhone
     SE) plutôt que de déborder hors de la modale. */
  max-width: 100%;
  word-break: break-word;
  hyphens: auto;
}

/* Score géant central + badge record/perfect ─────────────────────────── */
.go-score-wrap {
  padding: 22px var(--space-4) 18px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.go-score-label {
  font-family: 'Space Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.65;
}
.go-score {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(52px, 12vw, 78px);
  line-height: 1;
  letter-spacing: -0.03em;
  /* R5 redesign #7 : score héros posé sur un sticker jaune tilté (au lieu de
     flotter sur le paper) → en fait le point focal, langage néo-brut. */
  background: var(--yellow);
  color: var(--ink);
  border: 3px solid var(--ink);
  box-shadow: 6px 6px 0 var(--ink);
  padding: 2px 22px 8px;
  transform: rotate(-2deg);
  animation: goScoreIn 600ms var(--ease-out) both;
}
@keyframes goScoreIn {
  from { transform: rotate(-2deg) scale(0.4); opacity: 0; }
  60%  { transform: rotate(-2deg) scale(1.08); opacity: 1; }
  to   { transform: rotate(-2deg) scale(1);    opacity: 1; }
}
.go-record-badge {
  margin-top: 4px;
  display: inline-block;
  background: var(--ink);
  color: var(--yellow);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 12px;
  transform: rotate(-2deg);
  animation: recordPop 500ms var(--ease-pop) 350ms both;
}
@keyframes recordPop {
  from { transform: rotate(-2deg) scale(0.4); opacity: 0; }
  60%  { transform: rotate(-2deg) scale(1.1); opacity: 1; }
  to   { transform: rotate(-2deg) scale(1);   opacity: 1; }
}
.go-record-badge[hidden] { display: none; }

/* Lignes de stats — collage de post-it pins dispersés (flex-wrap, pas de
   grille rigide). Chaque pin se pose librement, tilté, façon scrapbook. ── */
.go-stats-row {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 10px;
  padding: 0 var(--space-4) 10px;
}
.go-stats-row--bonus { padding-bottom: 14px; }
/* Direction C : chaque stat = post-it pin pastel tilté (couleur + rotation via
   nth-child ci-dessous). flex:0 0 auto → non étiré, dispersé. Texte clair sur
   fonds sombres (red/purple/blue), sombre sinon. */
.go-stat {
  flex: 0 0 auto;
  min-width: 86px;
  background: var(--paper-2);
  border: 3px solid var(--ink);
  box-shadow: 3px 3px 0 var(--ink);
  padding: 9px 14px 8px;
  display: flex; flex-direction: column; gap: 2px;
}
.go-stats-row .go-stat:nth-child(1) { background: var(--green);  color: var(--ink);   transform: rotate(-2.2deg); }
.go-stats-row .go-stat:nth-child(2) { background: var(--red);    color: var(--paper); transform: rotate(1.6deg); }
.go-stats-row .go-stat:nth-child(3) { background: var(--purple); color: var(--paper); transform: rotate(-1.4deg); }
.go-stats-row--bonus .go-stat:nth-child(1) { background: var(--orange); color: var(--ink);   transform: rotate(1.8deg); }
.go-stats-row--bonus .go-stat:nth-child(2),
.go-stat--shiny { background: var(--yellow); color: var(--ink);   transform: rotate(-1.6deg); }
.go-stats-row--bonus .go-stat:nth-child(3),
.go-stat--holo  { background: var(--blue);   color: var(--paper); transform: rotate(2deg); }
.go-stat-value {
  font-family: 'Syne', sans-serif;
  font-style: italic; font-weight: 800;
  font-size: clamp(20px, 4vw, 26px);
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.go-stat-label {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.82;
}

/* Bloc des succès débloqués cette partie ─────────────────────────────── */
/* R5 redesign #7 : succès = stickers (plus de boîte dashed « checklist »). */
.go-ach {
  margin: 0 var(--space-4) 14px;
  padding: 0;
  background: none;
  border: none;
  text-align: left;
}
.go-ach[hidden] { display: none; }
.go-ach-head {
  display: inline-block;
  font-family: 'Space Mono', monospace;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 10px;
  padding: 4px 11px;
  background: var(--ink);
  color: var(--paper);
  transform: rotate(-1.5deg);
}
/* Liste scrollable interne sur mobile ET desktop (long runs Survie peuvent
   débloquer 10+ succès — la modale exploserait en hauteur sans cap). Cap
   resserré à 22vh desktop pour que la modale entière tienne dans le viewport
   sans scroll externe ; mobile descend à 18vh dans la media query (stats
   sont déjà compressées au-dessus). */
.go-ach-list {
  list-style: none;
  padding: 4px 8px 6px 4px;   /* marge à droite/bas pour les ombres des stickers */
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 22vh;
  overflow-y: auto;
  overflow-x: hidden;   /* les stickers tiltés + ombres ne doivent pas créer de scroll horizontal */
  scrollbar-width: thin;
  scrollbar-color: var(--ink) transparent;
}
.go-ach-list::-webkit-scrollbar { width: 6px; }
.go-ach-list::-webkit-scrollbar-thumb { background: var(--ink); border-radius: 3px; }
.go-ach-list li {
  display: flex; align-items: center; gap: 8px;
  font-family: 'Space Mono', monospace;
  font-size: 13px;
  text-align: left;
  padding: 7px 11px;
  background: var(--paper);
  border: 2px solid var(--ink);
  box-shadow: 3px 3px 0 var(--ink);
}
.go-ach-list li:nth-child(odd)  { transform: rotate(-0.8deg); }
.go-ach-list li:nth-child(even) { transform: rotate(0.8deg); }
.go-ach-list li::before {
  content: '✓';
  font-family: var(--font-display);
  font-weight: 800;
  color: var(--ink);
  background: var(--green);
  border: 2px solid var(--ink);
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px;
  flex-shrink: 0;
}
.go-ach-list .go-ach-name {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin-right: 6px;
}
.go-ach-list .go-ach-desc { opacity: 0.7; font-size: 12px; }

/* Mobile : la modale entière déborde du viewport. La rotation -2deg de
   revealPop reste (volonté esthétique) MAIS on réduit la taille globale
   de la carte pour que la bounding box rotée tienne dans le viewport.
   Backdrop garde un padding raisonnable (16px) pour donner de la marge
   d'absorption à la rotation (~11px de bbox excess sur card 600px tall).
   Liste succès en scroll interne pour ne pas exploser la hauteur. */
@media (max-width: 600px) {
  .gameover-backdrop { padding: 36px; }
  .gameover-backdrop.show { padding-top: max(24px, 4vh); padding-bottom: 24px; }
  /* Header plus compact */
  .go-head { padding: 9px 12px 7px; }
  .go-mode-tag { font-size: 9px; letter-spacing: 0.10em; }
  /* Score géant rétréci */
  .go-score-wrap { padding: 14px 12px 10px; }
  .go-score { font-size: clamp(40px, 11vw, 60px); }
  .go-score-label { font-size: 10px; }
  .go-record-badge { font-size: 11px; padding: 5px 10px; margin-top: 2px; }
  /* Stats rows resserrés */
  .go-stats-row { padding: 0 12px 8px; gap: 6px; }
  .go-stats-row--bonus { padding-bottom: 10px; }
  .go-stat { padding: 7px 4px; }
  .go-stat-value { font-size: clamp(15px, 4vw, 20px); }
  .go-stat-label { font-size: 8px; letter-spacing: 0.06em; }
  /* Bloc succès + liste scrollable interne */
  .go-ach { margin: 0 12px 10px; padding: 9px; }
  .go-ach-head { font-size: 11px; margin-bottom: 6px; }
  .go-ach-list {
    /* Override mobile : cap encore plus serré pour libérer de la place aux
       stats au-dessus, qui sont déjà compressées par la media query. */
    max-height: 18vh;
  }
  .go-ach-list .go-ach-name { font-size: 11px; }
  .go-ach-list .go-ach-desc { font-size: 11px; }
  .go-ach-list li { font-size: 12px; padding: 5px 8px; gap: 6px; }
  /* Footer + bouton replay compacts */
  .go-comeback { font-size: 10px; padding: 0 12px 8px; }
  .go-actions { padding: 0 12px 12px; }
}

.go-comeback {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.6;
  margin: 0 var(--space-4) 14px;
}

.go-actions {
  padding: 0 var(--space-4) var(--space-4);
  display: flex; justify-content: center;
}
/* CTA façon navbar V3, thématisé par mode (--go-accent), tilté, press qui
   écrase l'ombre. */
.gameover-replay {
  font-family: var(--font-display);
  font-size: 15px;
  background: var(--go-accent, var(--green)); color: var(--go-accent-ink, var(--ink));
  border: var(--border-thick); box-shadow: 6px 6px 0 var(--ink);
  padding: 13px 30px; cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  transform: rotate(-2deg);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
html.dlp-can-hover .gameover-replay:hover {
  transform: rotate(-2deg) translate(-2px, -2px);
  box-shadow: 8px 8px 0 0 var(--ink);
}
.gameover-replay:active {
  transform: rotate(-2deg) translate(3px, 3px);
  box-shadow: 2px 2px 0 0 var(--ink);
  transition-duration: 70ms;
}

.hidden { display: none !important; }

/* ── Navigation mobile — styles de BASE (B5.4, sortis du @media 860px) ──────
   L'AFFICHAGE de cette nav est piloté par l'ASPECT-RATIO (game-v3.css:1883 et
   chrome-v3.css masquent en ratio ≥ 1/1 avec !important), pas par la largeur.
   Quand ces styles vivaient dans @media (max-width:860px), un iPad Pro
   portrait (1024px de large, ratio < 1) affichait la nav SANS eux : liens
   bleus soulignés, icône/texte non empilés. Les sortir est inoffensif :
   .mobile-footer-nav est display:none de base et masquée en desktop par le
   ratio — ils ne s'expriment que quand la nav est réellement affichée. */
.mobile-footer-nav {
  display: flex;
  gap: clamp(3px, 1vw, 6px);
  align-items: center;
  /* Pas de flex-wrap : tout reste sur une ligne. Les boutons texte se
     partagent l'espace via flex:1 1 auto (grow proportionnel au contenu),
     le texte rétrécit via clamp() si nécessaire. */
}
.mobile-nav-btn {
  /* Sizing par contenu (flex-basis: auto), shrink si besoin pour rester
     sur une ligne, grow proportionnellement au contenu pour combler la
     largeur disponible — chaque bouton s'étire en fonction de la longueur
     de son label (« Classements » > « Succès » > « Jeu »). */
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: clamp(2px, 0.8vw, 7px);
  font-family: var(--font-display);
  /* Le texte rétrécit en viewport étroit (de 12px à 8px sur iPhone mini).
     Plage 8-12px pour permettre la compression jusqu'à ~360px sans
     débordement. */
  font-size: clamp(8px, 2.4vw, 12px);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  padding: 10px clamp(3px, 1.2vw, 14px);
  cursor: pointer;
  white-space: nowrap;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mobile-nav-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 1px 1px 0 0 var(--ink);
}
.mobile-nav-btn .ico { flex-shrink: 0; }
/* Bouton "icône seule" : ne prend pas de place horizontale superflue,
   laisse les boutons texte profiter du flex:1 restant. */
.mobile-nav-btn-icon-only {
  flex: 0 0 auto;
  width: 44px;
  padding: 10px 0;
  justify-content: center;
}

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 860px) {
  /* Padding latéral symétrique (16px chaque côté) — l'asymétrie 20px/16px
     précédente faisait déborder de 4px sur iPhone 16. */
  .app {
    padding: 12px 16px 16px 16px;
    overflow-y: visible;
    overflow-x: hidden;
    overflow-x: clip;
    max-width: 100%;
    width: 100%;
  }
  /* Pas d'overflow:hidden sur ces containers — sinon les box-shadow des
     enfants au bord droit/bas sont rognées. MAIS overflow-x: clip sur .main
     pour bloquer le débord horizontal causé par le swipe-out de .deputy-card
     (transform translate(±160vw)) qui sinon étend la zone de scroll sur
     iOS Safari. `clip` ne crée pas de scroll container, donc les shadows
     verticales restent libres. */
  .main { overflow: visible; overflow-x: clip; }
  /* gap 12px en mobile pour matcher .main gap (espace carte→hémicycle).
     Tous les espaces verticaux sont ainsi alignés sur 12px. */
  .right-col { overflow: visible; gap: 12px; }
  /* 7.5vw débordait pour les titres longs (« CLASSEMENTS », « COLLECTION »…)
     aux largeurs 481-860px : titre ≈ pleine largeur sans marge. Réduit pour
     qu'il rentre avec une marge confortable. */
  .site-title { font-size: clamp(15px, 5.6vw, 34px); }
  /* Sur mobile, le bandeau coloré du titre s'étend jusqu'au bord droit
     (au lieu de prendre uniquement la largeur du texte). margin-right
     négatif pour qu'il dépasse 4px du padding .app (= 12px du viewport),
     visuellement plus collé au bord droit que les autres éléments. */
  .site-title .accent {
    flex: 1 1 auto;
    margin-right: -4px;
  }
  .header .btn-collection, .header .btn-nav { display: none; }

  /* Bouton indice mobile : à DROITE de la photo, en superposition légère
     sur le coin haut-droit. Anciennement right: -4px (déborderait), mais
     depuis que .main n'a plus de padding-right asymétrique, le bouton se
     ferait clipper par overflow-x: clip. right: 4px le garde dans le visible. */
  .hint-postit-wrap {
    top: -8px;
    right: 4px;
    left: auto;
    /* Hauteur mobile plus compacte (font/icon/padding plus petits). */
    min-height: 86px;
    max-height: 86px;
  }
  .hint-postit {
    font-size: 10px;
    gap: 2px;
    /* Min-width pour éviter que le bouton se rétrécisse à un état intermédiaire
       (transition max-width interrompue ou flex-shrink du parent). */
    min-width: 84px;
    flex-shrink: 0;
  }
  .hint-postit.hint-out {
    /* Sauf en hint-out, où on veut que ça rétrécisse à 0. */
    min-width: 0;
  }
  /* Cost ("-1 PTS") + lignes texte plus petits sur mobile pour soulager
     en hauteur (le bouton se retrouvait trop étroit en haut/bas). */
  .hint-postit .cost {
    font-size: 13px;
    margin-top: 2px;
  }
  .hint-postit > .ico {
    width: 20px;
    height: 20px;
  }

  /* Footer navigation mobile : styles de BASE déplacés AVANT ce @media
     (cf. bloc « Navigation mobile — styles de BASE (B5.4) » plus haut) —
     l'affichage est piloté par l'aspect-ratio, pas par la largeur. Ne
     restent ici que les ajustements spécifiques aux petits écrans. */
  /* Très petits écrans (≤430px = iPhone 16 Pro et plus petit) : on garde
     icônes + texte (plus lisible) mais on compresse le bouton « Succès »
     en icône-seule pour libérer la largeur. */
  @media (max-width: 430px) {
    .mobile-footer-nav {
      width: 100%;
      max-width: 100%;
      overflow-x: clip;
      gap: 4px;
      align-items: stretch;
    }
    .mobile-nav-btn {
      /* flex 1 1 auto : chaque bouton fait sa taille de contenu + se
         partage l'espace restant équitablement. 'JEU' (court) reste petit,
         'CLASSEMENTS' (long) prend plus de place — aspect naturel. */
      flex: 1 1 auto;
      min-width: 0;
      padding: 10px 6px;
      font-size: clamp(10px, 2.7vw, 13px);
      gap: 4px;
      overflow: hidden;
      text-overflow: ellipsis;
      align-self: stretch;
    }
    .mobile-nav-btn .ico { width: 17px; height: 17px; }
  }
  /* Sur ≤430px, le bouton 'Succès' (href achievements.html) est compressé
     en icône-seule pour libérer la largeur et éviter le débord. Le texte
     'Succès' est masqué via font-size: 0 (le seul texte direct dans le
     <a>), l'icône garde sa taille via reset. */
  @media (max-width: 430px) {
    .mobile-nav-btn-icon-only { width: 36px; padding: 10px 0; }
    .mobile-nav-btn[href*="achievements.html"] {
      flex: 0 0 36px;
      width: 36px;
      padding: 10px 0;
      font-size: 0;
      gap: 0;
    }
    .mobile-nav-btn[href*="achievements.html"] .ico {
      width: 16px;
      height: 16px;
      font-size: initial;
    }
  }

  /* Variante : footer collé au bas de l'écran (pages Collection et Succès) */
  .mobile-footer-nav.is-fixed {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 50;
    background: var(--paper);
    border-top: var(--border-thick);
    padding: 10px 10px calc(14px + env(safe-area-inset-bottom, 0px)) 10px;
    margin: 0;
    max-width: 100%;
    overflow-x: clip;
    box-sizing: border-box;
  }
  body:has(.mobile-footer-nav.is-fixed) .app {
    /* Padding-bottom = hauteur de la nav + air. 84px = 78 + 6px d'air
       supplémentaire entre le bandeau Points et la nav fixe. */
    padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  }
  /* padding-right léger (6px) pour laisser respirer les box-shadow des
     cadres internes (game-panel, hemicycle-wrap, controls-panel) qui sont
     tirées en bas-droit.
     Row 1 (cadre photo) réduite de 12px au total pour libérer 6px à
     l'hémicycle + 6px à l'espace bandeau→nav. */
  .main { grid-template-columns: 1fr; grid-template-rows: clamp(158px, calc(100dvh - 412px), calc(46vh - 12px)) 1fr; gap: 12px; padding-right: 6px; }
  /* Reveal screen sur mobile : pas de cadre interne (le .game-panel parent
     a déjà son border + box-shadow), pas de fond, contenu collé en haut
     du cadre (align-content: start + padding-top minimal). */
  .reveal-screen {
    border: none;
    box-shadow: none;
    background: transparent;
    padding: 0 var(--space-3) var(--space-3) var(--space-3);
  }
  .reveal-screen.visible {
    align-content: start;
  }
  .shadow-photo-wrap, .photo-wrap { flex: 1; min-height: 0; width: auto; aspect-ratio: 2/3; align-self: center; max-width: 100%; }
  /* Carte photo : shadow alignée sur celle du game-panel (--sh-md) pour cohérence
     visuelle et économiser 4px en bas/à droite. La var --card-sh-base est aussi
     redéfinie : shinyGlow l'utilise dans ses keyframes → la carte shiny mobile
     reprend la MÊME ombre réduite que la carte normale (sans ça, la shiny
     dépassait le cadre hémicycle). */
  .deputy-card,
  .deputy-card-shadow { box-shadow: var(--sh-md); }
  .deputy-card { --card-sh-base: var(--sh-md); }
  /* Game-panel : padding asymétrique pour pousser le contenu (hémicycle) un
     peu plus bas dans le cadre + côtés très resserrés pour donner ~12px de
     plus à la largeur du SVG (= ~6px de plus en hauteur via le ratio 2:1). */
  .game-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 16px 4px 4px 4px; }
  /* Titre du panneau masqué en mobile : "Quel est son groupe parlementaire ?"
     superflu une fois le jeu compris ; libère l'espace pour la photo. */
  .panel-title { display: none; }
  .hemicycle-wrap { flex: 1; min-height: 0; aspect-ratio: unset; width: 100%; }
  /* Bouton retour mobile : visible uniquement quand hémicycle armé. Pop-in
     opacity + tilt préservé. Touch target 36px de haut (légèrement sous le
     44px Apple recommend mais OK pour un secondary action visible). */
  .hemicycle-wrap.is-armed .hemicycle-back {
    display: inline-flex;
    opacity: 1;
    pointer-events: auto;
    /* DESCENDU sous l'arc (bottom négatif : le wrap est plus court que le SVG qui déborde sous
       l'arc, donc un bottom positif RemonTait sur les secteurs bas et recouvrait leur texte). */
    bottom: -34px;
    right: 4px;
  }
  /* Mobile : verdict réduit à ~80% des valeurs précédentes (user retour
     "encore un peu trop gros"). clamp 34-45 (vs 42-56), padding 8/16/10
     (vs 10/20/12), top -3 (vs -4). Reveal-screen.top descendu à 64 (vs
     78) pour suivre le verdict plus petit. Padding latéral 4px (vs
     var(--space-4)=16px) pour profiter de la largeur écran. */
  .reveal-verdict {
    font-size: clamp(34px, 8vw, 45px);
    padding: 8px 16px 10px;
    top: -18px;
  }
  .reveal-screen {
    top: 61px;
    left: 4px;
    right: 4px;
    bottom: 4px;
  }
  .reveal-name      {
    /* Mobile : single-line nowrap aussi (cohérent avec base .reveal-name).
       Le \n inséré par JS entre prénom et nom devient un espace via nowrap.
       fitRevealName réduit font jusqu'à ce que tout tienne sur une ligne. */
    font-size: clamp(17px, 5.5vw, 26px);
    line-height: 1.1;
  }
  .reveal-party     {
    font-size: clamp(13px, 3.6vw, 18px);
    padding: 5px 10px 7px;
  }
  .reveal-party-sub {
    font-size: 13px;        /* primary mobile (avant 10px = plus petit que groupe) */
    margin-top: 4px;
    letter-spacing: 0.08em;
  }
  .reveal-groupe-sigle {
    font-size: 10px;        /* secondary mobile, < parti pour respecter hiérarchie */
    letter-spacing: 0.10em;
  }
  /* .reveal-progress n'est plus dans le grid : il est en position:absolute
     pour faire partie de la bordure inférieure (cf. règle plus haut). */
  /* Panneau stats : bottom-sheet */
  .controls-panel {
    position: fixed; bottom: calc(-100% - 30px); left: 0; right: 0;
    z-index: 200; justify-content: flex-start;
    border-top: var(--border-thick); border-left: var(--border-thick); border-right: var(--border-thick);
    box-shadow: -6px -6px 0 0 var(--ink);
    max-height: 82dvh; overflow-y: auto;
    transition: bottom 320ms var(--ease-pop);
  }
  .controls-panel.stats-open { bottom: 0; }
  .stats-close-btn {
    display: flex; align-self: flex-end; margin-bottom: var(--space-2);
    font-family: var(--font-display); font-size: 16px;
    background: var(--paper-2); border: var(--border-thick); box-shadow: var(--sh-sm);
    padding: 1px 10px; cursor: pointer; color: var(--ink);
  }
  .stats-backdrop {
    display: block; position: fixed; inset: 0;
    background: rgba(10,10,10,0.5); z-index: 199;
    visibility: hidden; opacity: 0; transition: opacity 0.2s;
  }
  .stats-backdrop.visible { visibility: visible; opacity: 1; }
  .stats-trigger {
    display: flex; align-items: center; gap: 10px;
    /* Couleur par défaut : violet (points). Override par data-tone selon
       la stat affichée — couleurs cohérentes avec les .stat-card du menu. */
    background: #4b3cd1; color: var(--paper);
    border: var(--border-thick); box-shadow: var(--sh-md);
    padding: 10px 14px; cursor: pointer; flex-shrink: 0; width: 100%;
    position: relative;
    transition: background 200ms ease, color 200ms ease;
  }
  .stats-trigger[data-tone="streak"] { background: var(--orange); color: var(--ink); }
  .stats-trigger[data-tone="acc"]    { background: var(--green); color: var(--ink); }
  .stats-trigger[data-tone="streak"] .trigger-hint .ico,
  .stats-trigger[data-tone="acc"] .trigger-hint .ico { color: var(--ink); }
  .stats-trigger:active { transform: translate(2px,2px); box-shadow: 1px 1px 0 0 var(--ink); }

  /* Stat-card pickable en mobile : tap pour la sélectionner comme stat
     affichée sur le bouton .stats-trigger. L'active porte data-active="true"
     → affiche un bookmark déchiré "Épinglée" qui pend du haut. */
  .stat-card[data-stat-pickable="true"] {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .stat-card[data-stat-pickable="true"]:active {
    transform: translate(1px, 1px);
  }
  /* Bookmark déchiré "Épinglée" — visible UNIQUEMENT quand data-active="true"
     sur la stat-card. Texte mono, fond ink + dot jaune, clip-path zigzag
     en bas pour l'effet bookmark déchiré. Anim drop+bounce à l'entrée. */
  .stat-card .pin-tag {
    display: none;
  }
  .stat-card[data-stat-pickable="true"][data-active="true"] .pin-tag {
    display: flex;
    position: absolute;
    top: -22px;
    left: 50%;
    transform: translateX(-50%) rotate(-3deg);
    transform-origin: 50% 0;
    background: var(--ink);
    color: var(--paper);
    padding: 5px 12px 14px;
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    border: var(--border-thin);
    box-shadow: 2px 2px 0 0 var(--paper);
    align-items: center;
    gap: 5px;
    clip-path: polygon(
      0% 0%, 100% 0%, 100% 78%,
      88% 95%, 75% 78%, 62% 95%, 50% 78%, 38% 95%, 25% 78%, 12% 95%, 0% 78%
    );
    animation: pinTagDrop 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
    z-index: 10;
    white-space: nowrap;
  }
  .stat-card[data-stat-pickable="true"][data-active="true"] .pin-tag::before {
    content: '';
    width: 6px;
    height: 6px;
    background: var(--yellow);
    border-radius: 50%;
    box-shadow: 0 0 0 1.5px var(--ink);
    display: inline-block;
  }
  @keyframes pinTagDrop {
    0%   { transform: translateX(-50%) translateY(-14px) rotate(-3deg); opacity: 0; }
    60%  { transform: translateX(-50%) translateY(3px) rotate(-3deg); opacity: 1; }
    100% { transform: translateX(-50%) translateY(0) rotate(-3deg); opacity: 1; }
  }
  /* Espace entre le titre Score et les stat-cards pour que le
     bandeau "Épinglée" (top:-22px tilté -3deg) ne masque pas le titre.
     6px = +3px par rapport au réglage initial, demandé pour respirer
     un peu plus au-dessus du bandeau dans les edge cases (long mot). */
  .controls-panel .section-title:has(+ .stats-compact) {
    margin-bottom: 6px;
  }
  /* Bloc points : nombre proéminent (×2 par rapport à avant) suivi de "points" en label */
  .trigger-pts { display: flex; align-items: baseline; gap: 8px; position: relative; }
  .trigger-num   { font-family: var(--font-display); font-size: 34px; line-height: 1; }
  .trigger-label {
    font-family: var(--font-display);
    font-size: 18px;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    white-space: nowrap;  /* « de précision » sur 1 ligne, jamais wrappé */
  }
  /* Hint à droite : "Stats et modes de jeu" + chevron up */
  .trigger-hint {
    margin-left: auto;
    display: inline-flex; align-items: center; gap: 6px;
    font-family: var(--font-mono); font-size: 11px; font-weight: 700;
    letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.95;
    text-align: right;
  }
  .trigger-hint .ico { color: var(--paper); }

  /* Stat-cards (visibles dans le bottom-sheet) : compactées en mobile pour
     ne pas saturer le sheet ouvert et laisser respirer le mode-de-jeu. */
  .stats-compact { gap: var(--space-2); }
  .stat-card     { padding: 7px 9px; gap: 2px; }
  .stat-head     { font-size: 9px; letter-spacing: 0.12em; }
  .stat-head .ico { width: 18px; height: 18px; }
  /* SVG remplit le cercle 18×18 sur mobile aussi (pas de marge). */
  .stat-head .ico svg { width: 18px; height: 18px; }
  .stat-num      { font-size: 22px !important; }
  .stat-sub      { font-size: 10px; letter-spacing: 0.02em; }
  .stat-sub-acc { gap: 4px; }
  .stat-sub-acc .acc-good,
  .stat-sub-acc .acc-bad { gap: 2px; font-size: 12px; }
  .stat-sub-acc .acc-sep { font-size: 12px; }
  .stat-sub-acc .ico,
  .stat-sub-acc .ico svg { width: 11px !important; height: 11px !important; }
  .stat-sub:not(.stat-sub-acc) span { font-size: 13px; }

  /* Mode buttons mobile : grille 2×3 (au lieu de 3×2 desktop) pour donner
     plus de place horizontale à chaque bouton — permet à "Entraînement"
     (12 caractères) de tenir sans compression + boost lisibilité globale
     du texte et de l'icône. Chaque cellule ~170-175 px sur iPhone 360-393. */
  .modes         { grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
  .mode-btn      { padding: 9px 12px; font-size: 12.5px !important; gap: 7px; min-width: 0; flex: none; }
  .mode-btn .ico { width: 16px; height: 16px; }
  .mode-btn .ico svg { width: 16px !important; height: 16px !important; }
  .mode-desc     { font-size: 12.5px; line-height: 1.35; }
  /* Sélecteur de famille : pills plus compactes en mobile */
  .specialiste-family-picker,
  .specialiste-role-picker { gap: 4px; }
  .family-pill,
  .role-pill { font-size: 10px; padding: 4px 8px; }
}

@media (max-width: 480px) {
  /* Padding gauche/droite généreux pour les box-shadow néobrut sans débord. */
  .app { padding: 10px 18px 14px 14px; }
  .reveal-meta { grid-template-columns: 1fr; }
  /* Titre réduit pour ne plus déborder (en 7.5vw il atteignait ≈30 px sur
     iPhone et la chaîne "DEVINE LE PARTI" + emoji + drop-shadow forçait .header
     plus large que .app content area). */
  .site-title { font-size: clamp(15px, 6vw, 24px); }
  /* .panel-title masqué via la règle 860px — pas d'override 480px nécessaire. */
  /* Stats-trigger : padding/gap un poil resserrés pour iPhone, mais on
     préserve la taille du num/label (priorité visuelle). */
  .stats-trigger { gap: 8px; padding: 9px 12px; }
  .trigger-num   { font-size: 30px; }
  .trigger-label { font-size: 16px; }
  .trigger-hint  { font-size: 10px; letter-spacing: 0.04em; gap: 4px; }
  .trigger-hint  .ico-svg { width: 16px !important; height: 16px !important; }
}

@media (prefers-reduced-motion: reduce) {
  /* Miroir de html.reduce-motion : pause les loops infinis bavards.
     Le reste des animations/transitions reste intact pour préserver
     les anims essentielles au feel (modal flip, reveal, swipe carte). */
  .hint-postit-wrap, .settings-donate,
  .rare-explain-card, .rare-explain-card::before,
  .pop-sector.armed-pulse {
    animation: none !important;
  }
}

/* ───────────────────────────────────────────────────────────────────────
   COLLECTION v3 : data-role appliqué sur la PHOTO (pas la carte entière).
   Designs thématiques par fonction. Toutes les déco scopées au masque de
   la photo → pas de débordement hors carte, pas de flicker pendant le
   swipe (cf. app.js applyRoleStyle).

   Sélecteurs partagés jeu/collection :
     .dcard .photo[data-role="..."]              (collection : div .photo)
     .deputy-card .photo-wrap[data-role="..."]   (jeu : conteneur img)
   ─────────────────────────────────────────────────────────────────────── */

/* Le compteur d'origine en bas-droite (correct/encounters) est remplacé
   par le rôle. Le label loc reste à gauche. */
.dcard .meta-loc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dcard .meta-role {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  background: var(--paper);
  border: 1.5px solid var(--ink);
  padding: 2px 6px 2.5px;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
  opacity: 0.95;
  flex-shrink: 0;
  max-width: 60%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dcard.locked .meta-role { opacity: 0.3; }

/* ─── Visuels par rôle ───────────────────────────────────────────────
   Wipé : on repart d'une feuille blanche pour les contours/masques de photo.
   Les premières tentatives (scotch, tricolore, timbre, RF, ondulé) sont
   archivées dans git, mais aucune n'était assez aboutie. Le data-role est
   conservé en JS pour qu'on puisse y revenir sereinement plus tard.
   Aujourd'hui : juste le label texte en bas-droite (.meta-role), aucun
   traitement visuel sur la photo. */

/* ─── Shiny version collection (= traitement jeu, mais en pause) ─────
   On reproduit la couche .shiny-fx du jeu : bordure dorée pulsante +
   shimmer diagonal. Pausés par défaut pour ne pas saturer la grille.
   Reprise au :hover (souris) ou via .shiny-active (tap mobile). */
.dcard .shiny-fx {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
  display: none;
}
.dcard.shiny .shiny-fx,
.dcard.holo  .shiny-fx { display: block; }
.dcard.shiny .shiny-fx::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Border retirée (cf. fix double-bordure cartes rares game card) : la
     bordure est désormais portée uniquement par .dcard .photo (animée via
     shinyPhotoBorder). Le ::before garde son glow inset uniquement. */
  animation: shinyBorderPulse 1.8s ease-in-out -0.9s infinite;
  animation-play-state: paused;
}
.dcard.shiny .shiny-fx::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 200%; height: 100%;
  background: linear-gradient(115deg,
    transparent 17%,
    rgba(255, 232, 120, 0.5) 25%,
    transparent 33%,
    transparent 67%,
    rgba(255, 232, 120, 0.5) 75%,
    transparent 83%);
  transform: translate3d(-50%, 0, 0);
  /* -0.7s ≈ 25 % du cycle : opacité 1, translation -38 %, bande dorée
     visible vers le tiers droit de la photo (vs opacity 0 en frame 0). */
  animation: shinyShimmer 2.8s linear -0.7s infinite;
  animation-play-state: paused;
  will-change: transform;
}
/* Bordure HOLO iridescente collection — mêmes keyframes que le jeu */
.dcard.holo .shiny-fx::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Border retirée (cf. fix double-bordure) : bordure portée par .dcard
     .photo via holoPhotoBorder. Le ::before garde son glow inset iridescent. */
  animation: holoBorderHue 3s linear -0.6s infinite;
  animation-play-state: paused;
}
/* Shimmer HOLO collection — gradient multicolore + hue-rotate.
   Opacité 0.30 (réduite vs 0.45) pour éviter la surcharge. */
.dcard.holo .shiny-fx::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 200%; height: 100%;
  background: linear-gradient(115deg,
    transparent 13%,
    rgba(255, 100, 200, 0.30) 19%,
    rgba(100, 200, 255, 0.30) 23%,
    rgba(255, 220, 100, 0.30) 27%,
    rgba(180, 100, 255, 0.30) 31%,
    transparent 37%,
    transparent 63%,
    rgba(255, 100, 200, 0.30) 69%,
    rgba(100, 200, 255, 0.30) 73%,
    rgba(255, 220, 100, 0.30) 77%,
    rgba(180, 100, 255, 0.30) 81%,
    transparent 87%);
  transform: translate3d(-50%, 0, 0);
  /* Même phase que shiny (.shimmer à -0.7s) + hue-rotate décalé pour avoir
     une teinte non-magenta de base sur la frame figée. */
  animation: shinyShimmer 2.8s linear -0.7s infinite, holoShimmerHue 4s linear -1s infinite;
  animation-play-state: paused;
  will-change: transform, opacity;
}
/* Reprise : hover (desktop), focus clavier, ou .shiny-active (tap tactile) */
.dcard.shiny:focus-within .shiny-fx::before,
.dcard.shiny:focus-within .shiny-fx::after,
.dcard.shiny.shiny-active .shiny-fx::before,
.dcard.shiny.shiny-active .shiny-fx::after,
.dcard.holo:focus-within .shiny-fx::before,
.dcard.holo:focus-within .shiny-fx::after,
.dcard.holo.shiny-active .shiny-fx::before,
.dcard.holo.shiny-active .shiny-fx::after {
  animation-play-state: running;
}
html.dlp-can-hover .dcard.shiny:hover .shiny-fx::before,
html.dlp-can-hover .dcard.shiny:hover .shiny-fx::after,
html.dlp-can-hover .dcard.holo:hover  .shiny-fx::before,
html.dlp-can-hover .dcard.holo:hover  .shiny-fx::after {
  animation-play-state: running;
}
/* Pas d'étoile statique : l'utilisateur veut le MÊME traitement que le jeu
   (cf. brief). La bordure dorée reste visible figée même animation en pause,
   ce qui distingue déjà la carte shiny dans la grille. */

/* ─── Bouton "Charger plus" (lazy load Collection) ─────────────────── */
.load-more-wrap {
  margin: var(--space-4) 0 var(--space-3);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
.load-more-wrap.hidden { display: none; }
.load-more-info {
  font-family: var(--font-mono);
  font-size: 11px;
  opacity: 0.7;
  letter-spacing: 0.04em;
}
.load-more-btn {
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 12px 28px;
  font-family: var(--font-display);
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  cursor: pointer;
  color: var(--ink);
  transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
.load-more-btn:focus-visible {
  transform: translate(-3px, -3px);
  box-shadow: 7px 7px 0 0 var(--ink);
  outline: none;
}
html.dlp-can-hover .load-more-btn:hover {
  transform: translate(-3px, -3px);
  box-shadow: 7px 7px 0 0 var(--ink);
  outline: none;
}
.load-more-btn:active {
  transform: translate(0, 0);
  box-shadow: var(--sh-sm);
}

/* Suppression de l'ancienne étoile shiny (remplacée par .shiny-fx) */
.dcard .shiny-star { display: none !important; }

/* ── Tampons sérigraphie (sceau RF rond + tampon rect institutionnel) ──
   Visibles uniquement sur la carte de jeu (.deputy-card + shadow) ; pas
   dans la collection. Positions (tl/tr/bl/br) et rotation (--rot) sont
   randomisées à chaque draw par applyStamps() en JS. Taille relative au
   container photo via cqi (container query inline-size). */
.fx-stamp {
  position: absolute;
  pointer-events: none;
  z-index: 5;
  /* Couleur et opacité randomisées par tampon via applyStamps (range de
     rouges/bordeaux + opacité variable) → chaque tampon a sa propre teinte
     d'encre, comme dans la réalité. */
  color: var(--stamp-color, #b53a2a);
  opacity: var(--stamp-opacity, 0.78);
  /* Filter de déformation (SVG turbulence + displacement map) appliqué aux
     tracés du tampon — cercles, texte, border deviennent légèrement
     irréguliers, comme une vraie encre tamponnée à la main. La rotation
     est conservée via combinaison de filter + transform. */
  filter: url(#stamp-warp) drop-shadow(0 0 0.4px rgba(181,58,42,0.35));
  transform: rotate(var(--rot, -10deg));
  transform-origin: center center;
}
/* Position de base par coin + offsets random additionnels via custom
   properties (--offset-x / --offset-y), pour que chaque tampon ne tombe pas
   exactement au même endroit à chaque élu. Plus large en vertical qu'en
   horizontal — l'œil pardonne mieux la dérive verticale. */
.fx-stamp[data-pos="tl"] {
  top: calc(6% + var(--offset-y, 0%));
  left: calc(5% + var(--offset-x, 0%));
  right: auto; bottom: auto;
}
.fx-stamp[data-pos="tr"] {
  top: calc(6% + var(--offset-y, 0%));
  right: calc(5% + var(--offset-x, 0%));
  left: auto; bottom: auto;
}
.fx-stamp[data-pos="bl"] {
  bottom: calc(7% + var(--offset-y, 0%));
  left: calc(5% + var(--offset-x, 0%));
  top: auto; right: auto;
}
.fx-stamp[data-pos="br"] {
  bottom: calc(7% + var(--offset-y, 0%));
  right: calc(5% + var(--offset-x, 0%));
  top: auto; left: auto;
}

/* Tampon rond — sceau RF (texte sur arc). Taille augmentée (36 → 44 cqi)
   + offsets resserrés (5-6 % → 1-2 %) pour pousser le sceau dans les bords
   du card. Sur desktop comme sur mobile, le sceau dépasse visuellement de
   la photo et peut se faire couper par le cadre (overflow hidden) du card. */
.fx-stamp.fx-stamp-rf {
  width: 44cqi;
  aspect-ratio: 1;
  height: auto;
  min-width: 96px;
  max-width: 156px;
}
/* Le SVG du sceau est injecté en inline par applyStamps → fill: currentColor
   pour que les paths héritent de --stamp-color (cf. .fx-stamp parent) qui
   randomize une teinte rouge bordeaux par carte. Les sceaux fournis n'ont
   pas d'attribut fill explicite, donc l'héritage marche directement. */
.fx-stamp.fx-stamp-rf svg {
  width: 100%;
  height: 100%;
  display: block;
  fill: currentColor;
}
/* Offsets spécifiques au sceau RF : positionnement plus extérieur que les
   tampons de base, pour qu'il puisse mordre sur le bord du card.
   Custom properties --offset-x / --offset-y conservées. */
.fx-stamp.fx-stamp-rf[data-pos="tl"] {
  top: calc(1% + var(--offset-y, 0%));
  left: calc(1% + var(--offset-x, 0%));
  right: auto; bottom: auto;
}
.fx-stamp.fx-stamp-rf[data-pos="tr"] {
  top: calc(1% + var(--offset-y, 0%));
  right: calc(1% + var(--offset-x, 0%));
  left: auto; bottom: auto;
}
.fx-stamp.fx-stamp-rf[data-pos="bl"] {
  bottom: calc(2% + var(--offset-y, 0%));
  left: calc(1% + var(--offset-x, 0%));
  top: auto; right: auto;
}
.fx-stamp.fx-stamp-rf[data-pos="br"] {
  bottom: calc(2% + var(--offset-y, 0%));
  right: calc(1% + var(--offset-x, 0%));
  top: auto; left: auto;
}

/* Tampon rectangulaire — institution + DOSSIER N° + numéro (3 lignes).
   Traits épaissis (border 2.5 → 4 px) pour cohérence avec le sceau. */
.fx-stamp.fx-stamp-rect {
  font-family: var(--font-display);
  letter-spacing: 0.10em;
  border: 4px solid currentColor;
  padding: 1.6cqi 3cqi 1.4cqi;
  line-height: 1.1;
  text-align: center;
  box-shadow: inset 0 0 0 2px rgba(181,58,42,0.25);
  max-width: 62%;
  white-space: nowrap;
}
.fx-stamp.fx-stamp-rect .l1 {
  display: block;
  font-size: 3.0cqi;
  letter-spacing: 0.16em;
}
.fx-stamp.fx-stamp-rect .l2 {
  display: block;
  font-size: 3.0cqi;
  letter-spacing: 0.14em;
  margin-top: 2px;
  opacity: 0.85;
}
.fx-stamp.fx-stamp-rect .l3 {
  display: block;
  font-size: 5.2cqi;     /* numéro mis en valeur */
  letter-spacing: 0.04em;
  margin-top: 1px;
}
/* Réduction des tampons sur mobile : RF un peu plus que le rect pour rester
   équilibrés sur les petits écrans. */
@media (max-width: 860px) {
  .fx-stamp.fx-stamp-rf {
    width: 34cqi;        /* desktop 44cqi → mobile 34cqi */
    min-width: 76px;
    max-width: 116px;
  }
  .fx-stamp.fx-stamp-rect {
    border-width: 3px;
    padding: 1.4cqi 2.6cqi 1.2cqi;
    box-shadow: inset 0 0 0 1.5px rgba(181,58,42,0.25);
  }
  .fx-stamp.fx-stamp-rect .l1 { font-size: 2.6cqi; }
  .fx-stamp.fx-stamp-rect .l2 { font-size: 2.6cqi; }
  .fx-stamp.fx-stamp-rect .l3 { font-size: 4.4cqi; }
}
/* Plancher de lisibilité pour les très petits écrans. */
@media (max-width: 480px) {
  .fx-stamp.fx-stamp-rect .l1 { font-size: 8.5px; }
  .fx-stamp.fx-stamp-rect .l2 { font-size: 8.5px; }
  .fx-stamp.fx-stamp-rect .l3 { font-size: 12.5px; }
}

/* Sécurité : pas de tampons dans la Collection (cf. .dcard) — au cas où le
   HTML viendrait à dupliquer la structure photo-wrap. */
.dcard .fx-stamp { display: none; }

/* ── Glossaire partagé ────────────────────────────────────────────────────
   Termes en gras dans les descriptions (succès, modes de jeu…) avec
   tooltip flottant au hover/tap. CSS partagé entre achievements.html et
   index.html ; comportement dans glossary.js. */
.gloss-term {
  font-weight: 700;
  border-bottom: 1.5px dotted currentColor;
  cursor: help;
  padding-bottom: 1px;
}
.gloss-term:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Tooltip flottant — un seul élément réutilisé dans tout le document. */
.gloss-tooltip {
  position: fixed;
  z-index: 1000;
  max-width: min(340px, calc(100% - 24px));
  background: var(--paper);
  color: var(--ink);
  border: var(--border-thick);
  box-shadow: var(--sh-md);
  padding: 12px 14px;
  opacity: 0;
  pointer-events: none;
  transform: translateY(4px);
  transition: opacity 140ms var(--ease-out), transform 140ms var(--ease-out);
  left: 0; top: 0;
}
.gloss-tooltip.visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.gloss-tip-label {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1;
  margin-bottom: 7px;
  opacity: 0.7;
}
.gloss-tip-def {
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.5;
}

/* ──────────────────────────────────────────────────────────────────────────
   Juice utilitaires — flash plein écran, shake gradué, float-delta, pulse.
   Pilotés par Public/juice.js. Gated par prefers-reduced-motion + .reduce-motion.
   Réf : audit anims 2026-05-14, plan Balatro tier 0.
   ────────────────────────────────────────────────────────────────────────── */

/* Flash : voile plein écran semi-transparent. Mix-blend screen → ne masque
   pas le contenu, "teinte" l'écran brièvement (240-280ms). */
.dlp-flash {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  background: var(--flash-color, var(--green));
  mix-blend-mode: screen;
  opacity: 0;
}
.dlp-flash.active {
  animation: dlp-flash-pulse var(--flash-dur, 260ms) ease-out forwards;
}
@keyframes dlp-flash-pulse {
  0%   { opacity: 0.45; }
  100% { opacity: 0;    }
}

/* Shake — 3 intensités. Le néo-brutaliste amplifie déjà les déplacements
   grâce aux bordures épaisses ; on reste modeste (2-7px). */
@keyframes dlp-shake-s {
  0%,100% { transform: translate(0,0); }
  20%     { transform: translate(-2px, 1px); }
  40%     { transform: translate(2px, -1px); }
  60%     { transform: translate(-1px, 2px); }
  80%     { transform: translate(2px, 0); }
}
@keyframes dlp-shake-m {
  0%,100% { transform: translate(0,0); }
  15% { transform: translate(-4px, 2px) rotate(-0.4deg); }
  30% { transform: translate(4px, -3px) rotate(0.3deg); }
  45% { transform: translate(-3px, 3px) rotate(-0.3deg); }
  60% { transform: translate(4px, -2px) rotate(0.4deg); }
  80% { transform: translate(-2px, 1px); }
}
@keyframes dlp-shake-l {
  0%,100% { transform: translate(0,0); }
  10% { transform: translate(-6px, 4px) rotate(-0.8deg); }
  25% { transform: translate(6px, -5px) rotate(0.7deg); }
  40% { transform: translate(-5px, 6px) rotate(-0.6deg); }
  55% { transform: translate(7px, -3px) rotate(0.8deg); }
  70% { transform: translate(-4px, 2px) rotate(-0.4deg); }
  85% { transform: translate(3px, -1px); }
}
.shake-s { animation: dlp-shake-s 380ms ease-out; }
.shake-m { animation: dlp-shake-m 440ms ease-out; }
.shake-l { animation: dlp-shake-l 480ms ease-out; }

/* Punch — petit "jump hors de l'écran" subtil. Réservé au verdict CORRECT
   (au lieu du shake qui ressemblait trop au RATÉ). Sensation de pop joyeux,
   pas de translation latérale. */
@keyframes dlp-punch {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.024); }
  65%  { transform: scale(0.992); }
  100% { transform: scale(1); }
}
.dlp-punch { animation: dlp-punch 320ms cubic-bezier(.34, 1.56, .64, 1); transform-origin: center center; }

/* Wobble — rotation oscillante sans translation. Réservé au verdict ALMOST
   (« hmm si proche ») : feedback différent du RATÉ tout en signalant l'échec.
   Doit rester PLUS DOUX que shake-m (wrong) — sémantique "presque" = échec
   gentil. 3 swings au lieu de 4, amplitude réduite de moitié (max 0.35°). */
@keyframes dlp-wobble {
  0%,100% { transform: rotate(0); }
  25%     { transform: rotate(-0.35deg); }
  55%     { transform: rotate(0.25deg); }
  80%     { transform: rotate(-0.12deg); }
}
.dlp-wobble { animation: dlp-wobble 360ms ease-out; transform-origin: center center; }

/* Float-delta — chiffre flottant ancré sur (x, y) viewport, monte et fade.
   Aligné sur l'esthétique néobrutaliste de .score-bump / .collection-bump :
   petit cadre paper + bordure épaisse + ombre dure (pas de text-shadow nu).
   transform centré (-50%, -50%) pour pivoter autour du point d'ancrage. */
.dlp-float-delta {
  position: fixed;
  pointer-events: none;
  z-index: 9998;
  font-family: var(--font-display);
  line-height: 1;
  padding: 2px 10px 4px;
  background: var(--paper);
  border: var(--border-thick);
  box-shadow: var(--sh-sm);
  white-space: nowrap;
  letter-spacing: 0.02em;
  animation: dlp-float-rise 1000ms cubic-bezier(.34, 1.56, .64, 1) forwards;
  will-change: transform, opacity;
}
@keyframes dlp-float-rise {
  0% {
    transform: translate(-50%, -50%) translate(var(--fd-dx, 0), 0) scale(0.5);
    opacity: 0;
  }
  18% {
    transform: translate(-50%, -50%) translate(var(--fd-dx, 0), -6px) scale(1.25);
    opacity: 1;
  }
  65% {
    transform: translate(-50%, -50%) translate(var(--fd-dx, 0), -42px) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) translate(var(--fd-dx, 0), -88px) scale(0.85);
    opacity: 0;
  }
}

/* Pulse-change — flash sur une stat-card quand sa valeur change.
   Combine scale (1.045) + glow par box-shadow temporaire. La couleur du
   glow est pilotée par --pulse-color (posée par juice.js pulseChange selon
   direction : gain=vert, perte=rouge). */
.pulse-change {
  animation: dlp-pulse-change 260ms ease-out;
}
@keyframes dlp-pulse-change {
  0%   { transform: scale(1);    box-shadow: var(--sh-md); }
  40%  { transform: scale(1.045); box-shadow: 0 0 0 4px var(--pulse-color, var(--green)), var(--sh-lg); }
  100% { transform: scale(1);    box-shadow: var(--sh-md); }
}

/* Note : ancienne tentative @keyframes dlp-pulse-change-trigger supprimée.
   Le pulse sur .stats-trigger (mobile) se fait désormais via un overlay
   position:fixed spawné par juice.js spawnFixedPulse() — toutes les
   approches CSS (ring extérieur, ring inset, bg flash) butaient sur
   .main { overflow-x: clip } ou sur des particularités <button> iOS. */

/* Confetti — particule rectangulaire colorée. Dimensions/position posées
   inline par juice.js. Animation pilotée par WAAPI. */
.dlp-confetti {
  position: fixed;
  pointer-events: none;
  z-index: 9997;
  border: 1px solid var(--ink);
  will-change: transform, opacity;
}

/* Survie critical — rumble sur le post-it cœur (préserve la rotation via
   --postit-rotate, sinon le rotate -7/-5deg saute pendant l'anim) + heartbeat
   "lub-dub" sur le path SVG (rythme ~95 bpm, stressé) + vignette rouge pulsée
   en arrière-plan. Les trois animations s'orchestrent : wrapper tremble, cœur
   bat, écran respire en rouge. */
@keyframes dlp-rumble {
  0%,100% { transform: rotate(var(--postit-rotate, 0deg)) translate(0, 0); }
  20%     { transform: rotate(var(--postit-rotate, 0deg)) translate(-1px, 1px); }
  40%     { transform: rotate(var(--postit-rotate, 0deg)) translate(1px, -1px); }
  60%     { transform: rotate(var(--postit-rotate, 0deg)) translate(-1px, 0); }
  80%     { transform: rotate(var(--postit-rotate, 0deg)) translate(1px, 1px); }
}
@keyframes dlp-heartbeat {
  0%, 55%, 100% { transform: scale(1); }
  18%          { transform: scale(1.10); }
  30%          { transform: scale(0.94); }
  42%          { transform: scale(1.05); }
}
body.survie-critical .lives-postit {
  animation: dlp-rumble 220ms infinite linear !important;
}
body.survie-critical .lives-heart-shape {
  animation: dlp-heartbeat 0.62s ease-in-out infinite;
}
body.survie-critical::after {
  content: '';
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 600;  /* sous les modales (700) mais au-dessus du gameplay */
  background: radial-gradient(ellipse at center, transparent 55%, rgba(255, 59, 59, 0.32) 100%);
  animation: dlp-vignette-pulse 1.4s ease-in-out infinite;
}
@keyframes dlp-vignette-pulse {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

/* Game over recap — stagger reveal des stats + petit pop chacune.
   --stagger-delay posé inline par app.js. */
.go-stagger-in {
  animation: go-stagger-in 380ms cubic-bezier(.34, 1.56, .64, 1) var(--stagger-delay, 0ms) backwards;
}
@keyframes go-stagger-in {
  0%   { opacity: 0; transform: translateY(16px) scale(0.85); }
  60%  { opacity: 1; transform: translateY(-2px) scale(1.04); }
  100% { transform: translateY(0) scale(1); }
}

/* Daily perfect — glow doré pulsé sur le badge "Perfect 10/10". */
body .daily-perfect-glow {
  animation: dlp-daily-perfect-glow 1600ms ease-in-out infinite alternate !important;
}
@keyframes dlp-daily-perfect-glow {
  0%   { box-shadow: var(--sh-sm), 0 0 0 0 rgba(245, 197, 24, 0); transform: rotate(-2deg) scale(1); }
  100% { box-shadow: var(--sh-sm), 0 0 32px 6px rgba(245, 197, 24, 0.7); transform: rotate(-2deg) scale(1.06); }
}

/* Achievement tier prestige — gold/platinum reçoivent un pre-pulse de
   l'icône (scale 1.5 + glow doré) qui démarre dès l'apparition du toast. */
.ach-toast[data-tier="gold"] .ico-big,
.ach-toast[data-tier="platinum"] .ico-big {
  animation: dlp-ach-tier-pulse 900ms cubic-bezier(.34, 1.56, .64, 1) backwards;
}
@keyframes dlp-ach-tier-pulse {
  /* rotate(-3deg) sur chaque frame → la tilt du cadre (comme les cartes) persiste
     pendant et après la pulse (sinon scale() écrasait la rotation). */
  0%   { transform: scale(0.4) rotate(-3deg);  filter: brightness(1) drop-shadow(0 0 0 transparent); }
  35%  { transform: scale(1.45) rotate(-3deg); filter: brightness(1.4) drop-shadow(0 0 14px rgba(245, 197, 24, 0.85)); }
  70%  { transform: scale(0.95) rotate(-3deg); filter: brightness(1.15) drop-shadow(0 0 6px rgba(245, 197, 24, 0.5)); }
  100% { transform: scale(1) rotate(-3deg);    filter: none; }
}
.ach-toast[data-tier="platinum"] .ico-big {
  animation-duration: 1100ms;
}

/* Respect du toggle in-app ET du media query OS.
   On désactive UNIQUEMENT le motion-écran (shake/punch/wobble/flash) +
   float-delta + confetti + rumble. La pulse-change sur stat-card est jugée
   tolérable (effet local, scale 1.045 bref) — elle reste pour ne pas
   saccader l'UX. La vignette rouge survie reste statique (pas d'anim) pour
   conserver le signal d'urgence sans pulse. */
@media (prefers-reduced-motion: reduce) {
  .dlp-flash, .dlp-float-delta, .dlp-confetti { display: none !important; }
  .shake-s, .shake-m, .shake-l,
  .dlp-punch, .dlp-wobble,
  .go-stagger-in, .daily-perfect-glow,
  .ach-toast[data-tier="gold"] .ico-big,
  .ach-toast[data-tier="platinum"] .ico-big { animation: none !important; }
  body.survie-critical .lives-postit,
  body.survie-critical .lives-heart-shape { animation: none !important; }
  body.survie-critical::after { animation: none !important; opacity: 0.7; }
  /* Speed timer : tue les anims polish (sway, breathe, flip sablier) mais
     garde la warning pulse (signal critique <3s, rouge). */
  .speed-timer:not(.warning), .speed-timer::before, .speed-timer .ico {
    animation: none !important;
  }
  /* Mirror du niveau Minimes pour les a11y OS-set (audit) : kill aussi les
     big card events + infinite shiny loops + reveal cascade. */
  .deputy-card.shiny.card-correct,
  .deputy-card.holo.card-correct,
  .deputy-card.comeback-fall,
  .deputy-card.shiny,
  .deputy-card-shadow.shiny,
  .deputy-card.shiny .role-outline,
  .deputy-card-shadow.shiny .role-outline,
  .deputy-card.holo .role-outline,
  .deputy-card-shadow.holo .role-outline,
  .reveal-verdict,
  .reveal-verdict.show,
  .reveal-name, .reveal-party, .reveal-party-sub, .reveal-groupe-sigle,
  .reveal-progress-fill,
  .typewriter-char { animation: none !important; }
}
html.reduce-motion .dlp-flash,
html.reduce-motion .dlp-float-delta,
html.reduce-motion .dlp-confetti { display: none !important; }
html.reduce-motion .shake-s,
html.reduce-motion .shake-m,
html.reduce-motion .shake-l,
html.reduce-motion .dlp-punch,
html.reduce-motion .dlp-wobble,
html.reduce-motion .go-stagger-in,
html.reduce-motion .daily-perfect-glow,
html.reduce-motion .ach-toast[data-tier="gold"] .ico-big,
html.reduce-motion .ach-toast[data-tier="platinum"] .ico-big { animation: none !important; }
html.reduce-motion body.survie-critical .lives-postit,
html.reduce-motion body.survie-critical .lives-heart-shape { animation: none !important; }
html.reduce-motion body.survie-critical::after { animation: none !important; opacity: 0.7; }

/* ──────────────────────────────────────────────────────────────────────────
   Phase next : card verdict pulses (A) + hover lift hémicycle (B) +
   game-panel combo pulse (C) + collection idle shimmer (D) +
   leaderboards row sweep (E).
   ────────────────────────────────────────────────────────────────────────── */

/* === A. CARD VERDICT PULSES ===
   Posées sur .deputy-card via JS dans answer() après calcul du verdictTone.
   Combinent box-shadow ring coloré + transform (tilt/shake/scale) sur 500-700ms.
   Cleanup automatique via setTimeout pour ne pas conflicter avec swipe-out. */
@keyframes cardPulseCorrect {
  0%   { box-shadow: var(--card-sh-base, var(--sh-lg)); transform: rotate(0); }
  20%  { box-shadow: 0 0 0 6px var(--green), var(--sh-lg); transform: rotate(1.5deg) scale(1.015); }
  60%  { box-shadow: 0 0 0 3px rgba(56, 211, 159, 0.5), var(--sh-lg); transform: rotate(-0.4deg); }
  100% { box-shadow: var(--card-sh-base, var(--sh-lg)); transform: rotate(0); }
}
.deputy-card.card-correct {
  animation: cardPulseCorrect 700ms cubic-bezier(.34, 1.4, .64, 1);
}
@keyframes cardPulseAlmost {
  0%   { box-shadow: var(--card-sh-base, var(--sh-lg)); transform: rotate(0); }
  25%  { box-shadow: 0 0 0 5px var(--orange), var(--sh-lg); transform: rotate(0.5deg); }
  60%  { box-shadow: 0 0 0 2px rgba(255, 154, 60, 0.5), var(--sh-lg); transform: rotate(-0.3deg); }
  100% { box-shadow: var(--card-sh-base, var(--sh-lg)); transform: rotate(0); }
}
.deputy-card.card-almost {
  animation: cardPulseAlmost 600ms ease-out;
}
@keyframes cardPulseWrong {
  0%, 100% { box-shadow: var(--card-sh-base, var(--sh-lg)); transform: translate(0, 0); }
  15%      { box-shadow: 0 0 0 6px var(--red), var(--sh-lg); transform: translate(-5px, 1px) rotate(-0.6deg); }
  30%      { transform: translate(5px, -1px) rotate(0.5deg); }
  45%      { box-shadow: 0 0 0 4px rgba(255, 59, 59, 0.6), var(--sh-lg); transform: translate(-3px, 1px); }
  60%      { transform: translate(3px, 0); }
  80%      { box-shadow: 0 0 0 1px rgba(255, 59, 59, 0.3), var(--sh-lg); transform: translate(-1px, 0); }
}
.deputy-card.card-wrong {
  animation: cardPulseWrong 500ms ease-out;
}

/* === C. GAME-PANEL COMBO PULSE ===
   Border colorée brève quand un combo banner se déclenche. Class posée
   en JS depuis showComboBanner. */
@keyframes panelComboPulse {
  0%, 100% { box-shadow: var(--sh-md); }
  35%      { box-shadow: 0 0 0 5px var(--orange), var(--sh-lg); }
}
.game-panel.combo-pulse {
  animation: panelComboPulse 600ms ease-out;
}

/* === D. SUPPRIMÉ ===
   L'idle shimmer aléatoire (toutes 3-6s flash sur une carte random) causait
   des saccades perceptibles sur la grille collection (~900 cards). Remplacé
   par un hover sweep dynamique au survol de chaque carte (cf. stats.html). */

/* === RARE FLASH (shiny + holo) ===
   Quand le verdict est CORRECT et la carte est SHINY ou HOLO, ajouter un
   flash doré (shiny) ou iridescent cyan→violet→vert (holo) en plus du pulse
   vert standard via card-correct. Effet "trésor" rare → renforce la dopamine
   sur les rares moments où une rare est correctement identifiée. */
.deputy-card.shiny.card-correct {
  animation: cardPulseCorrect 700ms cubic-bezier(.34, 1.4, .64, 1),
             shinyGoldenFlash 800ms ease-out;
}
@keyframes shinyGoldenFlash {
  0%   { filter: brightness(1) saturate(1); }
  20%  { filter: brightness(1.4) saturate(1.5) drop-shadow(0 0 22px rgba(245, 197, 24, 0.9)); }
  60%  { filter: brightness(1.15) saturate(1.2) drop-shadow(0 0 8px rgba(245, 197, 24, 0.5)); }
  100% { filter: brightness(1) saturate(1); }
}
.deputy-card.holo.card-correct {
  animation: cardPulseCorrect 700ms cubic-bezier(.34, 1.4, .64, 1),
             holoIridescentFlash 800ms ease-out;
}
@keyframes holoIridescentFlash {
  /* hue-rotate cycle 0→80→200→320→0 = traverse cyan→vert→violet→magenta.
     drop-shadow change de teinte aussi pour bien marquer l'iridescence. */
  0%   { filter: brightness(1) saturate(1) hue-rotate(0deg); }
  20%  { filter: brightness(1.35) saturate(1.5) hue-rotate(80deg)
                 drop-shadow(0 0 22px rgba(56, 211, 159, 0.85)); }
  50%  { filter: brightness(1.2) saturate(1.4) hue-rotate(200deg)
                 drop-shadow(0 0 18px rgba(176, 107, 255, 0.75)); }
  80%  { filter: brightness(1.1) saturate(1.25) hue-rotate(320deg)
                 drop-shadow(0 0 10px rgba(42, 77, 255, 0.55)); }
  100% { filter: brightness(1) saturate(1) hue-rotate(0deg); }
}

/* === COMEBACK FALL ===
   Wrong answer + 5+ erreurs consécutives → la carte "tombe" doucement
   (translateY modéré + tilt léger) au lieu du shake habituel. Sensation
   "le joueur sombre", subtile, pas brutale.

   Amplitudes réduites par rapport à la 1ère version (peak 50→22px, settle
   38→16px ; rotation peak -12°→-6°, settle -9°→-4°). L'effet doit
   s'imprimer comme une dégradation du moral, pas comme un crash.

   Forwards : la carte garde la pose tombée jusqu'à ce qu'advance() retire
   la classe pendant le swipe-out. PAS de setTimeout JS qui retire la classe
   à 1200ms (sinon snap back brutal en milieu de reveal). */
@keyframes cardComebackFall {
  0%   { transform: translate(0,0) rotate(0); box-shadow: var(--card-sh-base, var(--sh-lg)); }
  12%  { transform: translate(-3px, 1px) rotate(-0.4deg); }
  22%  { transform: translate(3px, -1px) rotate(0.3deg); }
  35%  { transform: translate(-1px, 0) rotate(0); box-shadow: 0 0 0 3px var(--red), var(--sh-lg); }
  65%  { transform: translate(0, 22px) rotate(-6deg); box-shadow: 0 0 0 1px rgba(255, 59, 59, 0.4), var(--sh-lg); }
  100% { transform: translate(0, 16px) rotate(-4deg); box-shadow: var(--card-sh-base, var(--sh-lg)); }
}
.deputy-card.comeback-fall {
  animation: cardComebackFall 1100ms cubic-bezier(.4, 0, .8, 1) forwards;
}

/* === E. LEADERBOARDS ROW HOVER SWEEP ===
   Highlight gradient qui passe de gauche à droite au hover. Pseudo-element
   ::before positionné absolute, anime left -100% → 100% au hover. */
.lb-row {
  position: relative;
  overflow: hidden;
}
.lb-row::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: -100%;
  width: 60%;
  background: linear-gradient(90deg, transparent, rgba(255, 210, 63, 0.18), transparent);
  pointer-events: none;
  transition: left 500ms ease-out;
  z-index: 0;
}
html.dlp-can-hover .lb-row:hover::before {
  left: 120%;
}
.lb-row > * { position: relative; z-index: 1; }

/* === UNIVERSAL PILL CLICK RING ===
   Triggered via JS class .just-activated (cf. pills-anim.js) sur tout pill
   de sélection (mode-btn, family/role/pick-pill, filter-pill, lb-tab,
   lb-filter-pill). Outline-based pulse → ne touche PAS le transform de
   l'élément (donc cohabite avec lift active translate, hover lift, etc.).
   La ring expand outward (offset 0→12px) ET fade (color → transparent)
   sur 400ms.

   Note : .filter-pill et .lb-tab/.lb-filter-pill ont aussi besoin d'avoir
   leurs propres transitions background/color/border-color (cf. stats.html
   et leaderboards.html inline CSS) pour que la bascule active/inactive
   soit smooth en plus du pulse au clic. */
@keyframes pillActivateRing {
  0%   { outline: 2px solid currentColor; outline-offset: 0; }
  100% { outline: 2px solid transparent;  outline-offset: 12px; }
}
.mode-btn.just-activated, .family-pill.just-activated, .role-pill.just-activated,
.pick-pill.just-activated, .filter-pill.just-activated, .lb-tab.just-activated,
.lb-filter-pill.just-activated {
  animation: pillActivateRing 400ms cubic-bezier(.4, 0, .2, 1) both;
}

/* Reduce-motion : neutralise tout ce que je viens d'ajouter ci-dessus. */
@media (prefers-reduced-motion: reduce) {
  .deputy-card.card-correct, .deputy-card.card-almost, .deputy-card.card-wrong,
  .deputy-card.shiny.card-correct, .deputy-card.holo.card-correct,
  .deputy-card.comeback-fall,
  .game-panel.combo-pulse,
  .mode-btn.just-activated, .family-pill.just-activated, .role-pill.just-activated,
  .pick-pill.just-activated, .filter-pill.just-activated, .lb-tab.just-activated,
  .lb-filter-pill.just-activated { animation: none !important; }
  .lb-row::before { display: none; }
}
html.reduce-motion .deputy-card.card-correct,
html.reduce-motion .deputy-card.card-almost,
html.reduce-motion .deputy-card.card-wrong,
html.reduce-motion .deputy-card.shiny.card-correct,
html.reduce-motion .deputy-card.holo.card-correct,
html.reduce-motion .deputy-card.comeback-fall,
html.reduce-motion .game-panel.combo-pulse,
html.reduce-motion .mode-btn.just-activated, html.reduce-motion .family-pill.just-activated,
html.reduce-motion .role-pill.just-activated, html.reduce-motion .pick-pill.just-activated,
html.reduce-motion .filter-pill.just-activated, html.reduce-motion .lb-tab.just-activated,
html.reduce-motion .lb-filter-pill.just-activated { animation: none !important; }
html.reduce-motion .lb-row::before { display: none; }

/* Audit Minimes : kill aussi les loops infinis sur cartes rares + verdict
   anims + reveal stagger + entrance pop-ins. Les fallbacks textuels (typewriter
   → textContent direct) sont gérés en JS quand isReduced() retourne true. */
html.reduce-motion .deputy-card.shiny,
html.reduce-motion .deputy-card-shadow.shiny { animation: none !important; }
html.reduce-motion .deputy-card.shiny .role-outline,
html.reduce-motion .deputy-card-shadow.shiny .role-outline,
html.reduce-motion .deputy-card.holo .role-outline,
html.reduce-motion .deputy-card-shadow.holo .role-outline { animation: none !important; }
/* Verdict pop + glow + reveal cascade : kill tout le drama du reveal screen.
   Le contenu reste visible (display intact), juste sans anim. */
html.reduce-motion .reveal-verdict,
html.reduce-motion .reveal-verdict.show,
html.reduce-motion .reveal-name,
html.reduce-motion .reveal-party,
html.reduce-motion .reveal-party-sub,
html.reduce-motion .reveal-groupe-sigle,
html.reduce-motion .reveal-progress-fill,
html.reduce-motion .typewriter-char { animation: none !important; }

/* ──────────────────────────────────────────────────────────────────────────
   ═══ NIVEAU RÉDUITS (anim-reduced) — kill du sous-ensemble disruptif ═══
   Mid-level entre Max (rien killed) et Minimes (reduce-motion = tout killed).
   Cible les vrais "screen-disturbing" effects : shake écran, confettis,
   flashes plein écran, gros translates de carte, infinite loops bavards.
   Garde les micro-feedbacks utiles : pop-ins entrée, pulses verdict,
   transitions, hover lifts, swipe-out, modal flip, typewriter.

   Prefers-reduced-motion système (a11y OS) → mappé à Minimes (.reduce-motion)
   pour l'utilisateur qui demande explicitement la réduction max via son OS.
   .anim-reduced est un choix UTILISATEUR via le picker settings 3 niveaux. */
html.anim-reduced .dlp-flash,
html.anim-reduced .dlp-float-delta,
html.anim-reduced .dlp-confetti { display: none !important; }
html.anim-reduced .shake-s,
html.anim-reduced .shake-m,
html.anim-reduced .shake-l,
html.anim-reduced .dlp-punch,
html.anim-reduced .dlp-wobble,
html.anim-reduced .go-stagger-in,
html.anim-reduced .daily-perfect-glow,
html.anim-reduced .ach-toast[data-tier="gold"] .ico-big,
html.anim-reduced .ach-toast[data-tier="platinum"] .ico-big { animation: none !important; }
html.anim-reduced body.survie-critical .lives-postit,
html.anim-reduced body.survie-critical .lives-heart-shape { animation: none !important; }
html.anim-reduced body.survie-critical::after { animation: none !important; opacity: 0.7; }
/* Big card events killed at Réduits (= aussi killed à Minimes via les rules
   existantes plus haut) : comeback fall (gros translate Y + tilt), shiny
   golden flash + holo iridescent flash (filter brightness/saturate spike). */
html.anim-reduced .deputy-card.shiny.card-correct,
html.anim-reduced .deputy-card.holo.card-correct,
html.anim-reduced .deputy-card.comeback-fall { animation: none !important; }
/* Infinite loops bavards : breathe/sway sur post-its + glow rares. */
html.anim-reduced .hint-postit-wrap,
html.anim-reduced .settings-donate,
html.anim-reduced .rare-explain-card,
html.anim-reduced .rare-explain-card::before,
html.anim-reduced .pop-sector.armed-pulse { animation: none !important; }
/* Glow infini autour des cartes shiny (box-shadow pulsé doré) + animation
   stroke-color des contours role-outline shiny/holo. La couleur de base
   reste visible (cf. fix borders.js) mais sans pulsing/cycling. */
html.anim-reduced .deputy-card.shiny,
html.anim-reduced .deputy-card-shadow.shiny { animation: none !important; }
html.anim-reduced .deputy-card.shiny .role-outline,
html.anim-reduced .deputy-card-shadow.shiny .role-outline,
html.anim-reduced .deputy-card.holo .role-outline,
html.anim-reduced .deputy-card-shadow.holo .role-outline { animation: none !important; }

/* ── Perf diag toggles (cheats panel) ─────────────────────────────────────
   Désactivations brutales utilisées par les boutons du panneau dev
   "Shiny anims ON/OFF" et "Holo anims ON/OFF". Permet de mesurer la part
   du framerate consommée par chaque famille d'effets. Sélecteurs très
   ciblés pour ne PAS toucher aux animations de transition de carte. */
html.perf-no-shiny .deputy-card.shiny .shiny-fx,
html.perf-no-shiny .deputy-card.shiny .shiny-fx::before,
html.perf-no-shiny .deputy-card.shiny .shiny-fx::after,
html.perf-no-shiny .deputy-card-shadow.shiny .shiny-fx,
html.perf-no-shiny .deputy-card-shadow.shiny .shiny-fx::before,
html.perf-no-shiny .deputy-card-shadow.shiny .shiny-fx::after {
  animation: none !important;
  background: none !important;
}
html.perf-no-shiny .deputy-card.shiny .photo-wrap,
html.perf-no-shiny .deputy-card-shadow.shiny .shadow-photo-wrap {
  --bg-rarity: none !important;
  animation: none !important;
}

html.perf-no-holo .deputy-card.holo .shiny-fx,
html.perf-no-holo .deputy-card.holo .shiny-fx::before,
html.perf-no-holo .deputy-card.holo .shiny-fx::after,
html.perf-no-holo .deputy-card-shadow.holo .shiny-fx,
html.perf-no-holo .deputy-card-shadow.holo .shiny-fx::before,
html.perf-no-holo .deputy-card-shadow.holo .shiny-fx::after {
  animation: none !important;
  background: none !important;
}
html.perf-no-holo .deputy-card.holo .photo-wrap,
html.perf-no-holo .deputy-card-shadow.holo .shadow-photo-wrap {
  --bg-rarity: none !important;
  animation: none !important;
}
