/* ============================================================
   QWESTIFY — style.css v3.0 — Propre, complet, sans accumulation
   ============================================================ */

/* 0. POLICE — Inter, servie depuis NOTRE origine
   ────────────────────────────────────────────────────────────
   Dix-neuf pages la chargeaient depuis les serveurs de polices de Google :
   l'adresse IP de chaque
   visiteur y partait à chaque page, sans consentement ni nécessité.
   Pour une application genevoise soumise à la nLPD, c'est une transmission de
   données personnelles à un tiers hors de Suisse — pour un service qu'on rend
   soi-même en 47 Ko.

   Une SEULE fonte variable (100→900) remplace les six graisses statiques :
   un fichier, une requête, et les valeurs intermédiaires du CSS (550, 610,
   650, 850, 950) deviennent enfin rendues au lieu de retomber sur la voisine.

   C'est le FICHIER EXACT que le navigateur téléchargeait déjà — même URL
   gstatic, même sous-ensemble latin. Le rendu est identique par construction.
   Licence OFL, provenance et SHA-256 : assets/fonts/INTER-LICENSE.txt

   font-display:swap conservé — même comportement d'affichage qu'avant : le
   texte s'affiche immédiatement dans la police système puis bascule. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('assets/fonts/inter-latin-variable.woff2') format('woff2');
  /* Sous-ensemble latin : couvre tout le français (àâçéèêëîïôùûü, Œœ, €). */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* 1. TOKENS */
:root{
  --ease:cubic-bezier(0.22,1,0.36,1);
  /* Couleurs */
  --orange:#FF6B2B;--orange2:#FF8C55;--violet:#7C3AED;--violet2:#9D5FF5;
  --green:#22C55E;--red:#EF4444;--yellow:#F59E0B;--blue:#3B82F6;
  /* Typographie */
  --text-xs:9px;--text-sm:10px;--text-base:12px;--text-md:13px;
  --text-lg:15px;--text-xl:clamp(19px,2.9vw,27px);--text-hero:clamp(27px,6.7vw,52px);
  /* Navigation menus — taille dédiée, ajustable sans toucher aux tokens globaux */
  --menu-text-size:13px;
  /* Arrondis */
  --radius-sm:8px;--radius-md:12px;--radius-lg:16px;
}
[data-theme="dark"]{--bg:#0E0E14;--bg2:#13131C;--bg3:#1C1C28;--border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.12);--text:#F0F0FA;--text2:#9898B8;--text3:#5A5A7A;--shadow:0 24px 64px rgba(0,0,0,0.5)}
[data-theme="light"]{--bg:#F5F5FA;--bg2:#FFFFFF;--bg3:#EBEBF5;--border:rgba(0,0,0,0.07);--border2:rgba(0,0,0,0.12);--text:#0E0E14;--text2:#5A5A7A;--text3:#9898B8;--shadow:0 24px 64px rgba(0,0,0,0.08)}

/* 2. RESET */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
/* MOUVEMENTS RÉDUITS — un seul endroit, pour tout le site.
   `scrollTo({behavior:'auto'})` ne veut PAS dire « instantané » : par CSSOM-View,
   'auto' DÉFÈRE au scroll-behavior calculé de l'élément défilant. Tant que la
   ligne ci-dessus valait `smooth` sans exception, TOUS les gardes
   `prefereMoinsDeMouvement() ? 'auto' : …` du dépôt étaient inertes — mesuré :
   sous `reduce`, la courbe de défilement était IDENTIQUE à celle sans préférence.
   Corriger 'auto' → 'instant' appel par appel aurait laissé les ~8 autres retours
   en haut (index.html, et le chrome mobile de shared.js) continuer d'animer :
   une incohérence pire que l'uniformité d'avant. Ici, un seul point de vérité.
   ⚠️ Ne concerne QUE le défilement : les animations restent gérées ailleurs. */
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body{font-family:'Inter',sans-serif;font-size:var(--text-base);line-height:1.6;background:var(--bg);color:var(--text);min-height:100vh;transition:background .35s,color .35s;overflow-x:hidden;-webkit-font-smoothing:antialiased}
/* Héritage font vers éléments de formulaire (ne l'héritent pas nativement) */
button,input,select,textarea{font-family:inherit;font-size:inherit}

/* 3. LOGO LETTERS — les tuiles colorées du mot-symbole QWESTIFY (.anl),
   posées par shared.js:1273. `.logo-letter` a été retiré de ce groupe et sa
   règle propre supprimée (16-08-2026) : la classe n'existait dans AUCUN markup,
   vérifié sur les 18 pages et sur le DOM vivant chrome monté. Attention en
   cherchant : la chaîne « logo-letter » est CONTENUE dans `.ls-logo-letter`,
   une classe bien vivante des lignes de liste — un grep naïf conclut à tort. */
.anl{font-weight:800;display:flex;align-items:center;justify-content:center;border-radius:5px;color:#fff}
.anl{font-size:14px;width:20px;height:20px}
.ll1{background:var(--orange)}.ll2{background:var(--green)}.ll3{background:var(--violet)}.ll4{background:var(--yellow)}.ll5{background:#E11D48}.ll6{background:#0891B2}.ll7{background:var(--green)}.ll8{background:var(--orange)}

/* 4. LANDING NAV */
nav#landingNav{position:fixed;top:0;left:0;right:0;z-index:100;height:60px;display:flex;align-items:center;justify-content:space-between;padding:0 20px;background:var(--bg);border-bottom:1px solid var(--border)}
.nav-logo{display:flex;align-items:center;gap:2px;cursor:pointer;text-decoration:none}
.nav-right{display:flex;align-items:center;gap:10px}
.theme-toggle{width:52px;height:28px;background:var(--bg3);border:1px solid var(--border2);border-radius:14px;cursor:pointer;position:relative;flex-shrink:0}
.theme-toggle::after{content:'';position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:var(--text2);transition:transform .3s}
[data-theme="light"] .theme-toggle::after{transform:translateX(24px);background:var(--orange)}
/* ── Système de boutons unifié (3 tailles) ─────────────────────── */
/* S — contextuels (filtres, pills, nav) */
/* `.btn-sm` a été RETIRÉ d'ici (lot P2). Il était défini deux fois — ici et
   dans le <style> d'admin.html — avec des métriques différentes (6px/10px
   contre 8px/12px), et la version d'admin l'emportait silencieusement.
   Vérifié avant retrait : aucune autre page ni aucun script n'emploie la
   classe ; cette règle-ci ne peignait donc RIEN. La définition d'admin est
   désormais la seule. Les voisines `.btn-md` / `.btn-lg` restent en place. */
/* M — actions importantes (logout, GM sidebar) */
.btn-md{display:inline-flex;align-items:center;justify-content:center;width:100%;padding:11px 20px;font-size:var(--text-sm);font-weight:700;border-radius:var(--radius-md);cursor:pointer;transition:all .2s}
/* L — CTA principaux (landing, hero) */
.btn-lg{display:inline-flex;align-items:center;justify-content:center;padding:14px 28px;font-size:var(--text-base);font-weight:600;border-radius:var(--radius-md);cursor:pointer;transition:all .25s}

.btn-nav{font-size:var(--text-sm);font-weight:600;padding:6px 14px;border-radius:var(--radius-sm);border:1px solid var(--border2);background:transparent;color:var(--text);cursor:pointer;transition:all .2s;white-space:nowrap}
.btn-nav-primary{background:var(--orange);border-color:var(--orange);color:#fff}
.btn-nav-primary:hover{background:var(--orange2)}

/* 5. HERO */
.hero{min-height:100svh;padding:100px 20px 60px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;position:relative;overflow:hidden}
.hero-glow{position:absolute;width:600px;height:600px;border-radius:50%;background:radial-gradient(circle,rgba(255,107,43,0.12) 0%,transparent 70%);top:50%;left:50%;transform:translate(-50%,-60%);pointer-events:none;animation:pulse 6s ease-in-out infinite}
.hero-glow2{position:absolute;width:400px;height:400px;border-radius:50%;background:radial-gradient(circle,rgba(124,58,237,0.10) 0%,transparent 70%);bottom:0;right:-100px;pointer-events:none}
@keyframes pulse{0%,100%{opacity:.6;transform:translate(-50%,-60%) scale(1)}50%{opacity:1;transform:translate(-50%,-60%) scale(1.08)}}
.hero-badge{display:inline-flex;align-items:center;gap:8px;background:var(--bg2);border:1px solid var(--border2);border-radius:20px;padding:6px 14px;font-size:12px;font-weight:600;color:var(--text2);margin-bottom:28px}
.badge-dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px var(--green);animation:blink 2s ease-in-out infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
.hero h1{font-size:var(--text-hero);font-weight:800;line-height:1.05;letter-spacing:-1.5px;margin-bottom:20px}
.hero h1 .accent{background:linear-gradient(135deg,var(--orange),var(--yellow));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.hero p{font-size:clamp(15px,3vw,18px);color:var(--text2);max-width:420px;line-height:1.65;margin-bottom:36px}
.hero-cta{display:flex;flex-direction:column;gap:12px;width:100%;max-width:320px}
.btn-primary{width:100%;padding:14px 28px;background:var(--orange);color:#fff;border:none;border-radius:var(--radius-md);font-size:var(--text-base);font-weight:600;cursor:pointer;transition:all .25s}
.btn-primary:hover{background:var(--orange2);transform:translateY(-2px);box-shadow:0 12px 32px rgba(255,107,43,.4)}
.btn-secondary{width:100%;padding:14px 28px;background:transparent;color:var(--text2);border:1px solid var(--border2);border-radius:var(--radius-md);font-size:var(--text-base);font-weight:500;cursor:pointer;transition:all .2s}
.btn-secondary:hover{background:var(--bg3);color:var(--text)}
/* .hero-stats / .hstat* RETIRÉES (vitrine étape 1) : elles n'habillaient que les
   « 2 400 utilisateurs / 18 partenaires / 340 bons » du hero, des chiffres
   inventés. Vérifié avant suppression — 0 occurrence dans tout le dépôt. */

/* 6. SECTIONS LANDING */
section{padding:72px 20px}
.section-title{font-size:clamp(26px,6vw,42px);font-weight:800;line-height:1.1;letter-spacing:-1px;margin-bottom:16px}
.section-sub{font-size:var(--text-base);color:var(--text2);line-height:1.65;max-width:480px}
/* État vide des filtres d'accueil : pleine largeur dans la grille #onboardingContainer (ne modifie pas .section-sub, partagée) */
.fq-empty{grid-column:1 / -1;text-align:center;padding:32px 16px;margin:0 auto}
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
.reveal.visible{opacity:1;transform:translateY(0)}
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}

.quests-main{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
/* LANDING-3 passe 2 — La famille .qm-* et .qf-btn a été RETIRÉE d'ici.
   Elle stylait la vitrine de quêtes écrite en dur dans le bloc #landing de
   campagne.html : barre de recherche, filtres, carte vedette, grille de
   cartes-maquettes (Uber, Migros). Ce bloc n'existe plus.
   La landing d'index.html ne les a JAMAIS employées : son #landingQuests est
   rempli par renderLandingDynamic dans .lc-grid / #landingDemoFeatured /
   #landingLockedGrid — une autre famille de balisage, intacte.
   Vérifié après retrait : plus aucun fichier suivi ne porte ce balisage.
   (Les .qm-badge / .qm-active / .qm-draft d'admin.html sont une famille
   distincte — badges de statut de quête — stylée dans admin.html.) */

footer#landingFooter{background:var(--bg);border-top:1px solid var(--border);padding:32px 20px;text-align:center}
.footer-logo{display:flex;justify-content:center;gap:2px;margin-bottom:16px;cursor:pointer}
.footer-links{display:flex;justify-content:center;gap:20px;flex-wrap:wrap;margin-bottom:16px}
.footer-links a{font-size:13px;color:var(--text3);text-decoration:none;font-weight:500;transition:color .2s}
.footer-links a:hover{color:var(--text2)}
.footer-copy{font-size:12px;color:var(--text3)}

/* 6.5. TYPES DE QUÊTES (landing) */

/* 6.6. SÉRIES QUOTIDIENNES (landing) */
/* Carte « série » de la page d'accueil — soleil, plus flamme. Le masque est
   défini plus bas (--gm-soleil-masque) ; les propriétés personnalisées ne
   dépendent pas de l'ordre de déclaration. */

/* 7. GM POPUP */
/* ══ FEUILLES GM — esthétique Apple, deux variantes de thème ════════════════
   #gmOverlay (série quotidienne) et #gmRepairOverlay (réparation) partagent
   ces règles : deux feuilles, un seul habillage.

   Les couleurs sont posées en PROPRIÉTÉS PERSONNALISÉES sur .gm-overlay, et
   redéclarées une seule fois pour le thème clair. Le reste du bloc ne parle
   plus qu'en `var(--gm-*)` : ajouter une variante ne demandera pas de rouvrir
   chaque règle. Les valeurs sont celles d'iOS, pas les tokens Qwestify — ces
   feuilles sont volontairement des objets « système », pas des surfaces de
   marque. Seul l'orange reste celui de la marque.

   ⚠️ Le thème CLAIR est livré mais N'EST PAS ATTEIGNABLE par un utilisateur
   aujourd'hui : le CSS clair de l'application répond bien (mesuré), mais le
   bouton #dtThemeToggle n'a pas d'écouteur — index.html le cherche au
   chargement alors que la barre bureau est injectée après (piège §15, avalé
   par l'optionnel `?.`). Corriger ce bouton sort du périmètre de ce lot ; la
   variante claire est donc prête pour le jour où il sera recâblé. */
.gm-overlay{
  position:fixed;inset:0;z-index:300;
  background:rgba(0,0,0,.45);
  -webkit-backdrop-filter:blur(20px) saturate(1.2);
  backdrop-filter:blur(20px) saturate(1.2);
  display:flex;align-items:flex-end;justify-content:center;
  opacity:0;pointer-events:none;transition:opacity .35s var(--ease);
  /* Variante SOMBRE (défaut) */
  --gm-sheet:#1c1c1e;
  --gm-grab:#48484a;
  --gm-x-bg:#2c2c2e;
  --gm-x-fg:#98989e;
  --gm-title:#f2f2f7;
  --gm-sub:#98989e;
  --gm-accent:#FF7A3D;
  --gm-surface:rgba(255,255,255,.06);
  --gm-hairline:rgba(255,255,255,.12);
}
/* Variante CLAIRE */
[data-theme="light"] .gm-overlay{
  background:rgba(0,0,0,.28);
  --gm-sheet:#fff;
  --gm-grab:#d1d1d6;
  --gm-x-bg:#f0f0f3;
  --gm-x-fg:#8a8a8f;
  --gm-title:#1c1c1e;
  --gm-sub:#8a8a8f;
  --gm-accent:#FF6B2B;
  --gm-surface:rgba(0,0,0,.04);
  --gm-hairline:rgba(0,0,0,.10);
}
.gm-overlay.open{opacity:1;pointer-events:all}

/* Police système : sur un appareil Apple c'est SF Pro, ailleurs l'équivalent
   local. On ne la charge pas — c'est tout l'intérêt. */
.gm-sheet{
  width:100%;max-width:480px;
  background:var(--gm-sheet);
  border-radius:22px 22px 0 0;
  padding:10px 22px calc(30px + env(safe-area-inset-bottom, 0px));
  transform:translateY(100%);
  transition:transform .45s var(--ease);
  position:relative;
  font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','SF Pro Display',system-ui,'Segoe UI',sans-serif;
  -webkit-font-smoothing:antialiased;
}
.gm-overlay.open .gm-sheet{transform:translateY(0)}

.gm-handle{width:36px;height:5px;border-radius:2.5px;background:var(--gm-grab);margin:0 auto 18px}

.gm-close{
  position:absolute;top:14px;right:18px;width:30px;height:30px;
  border-radius:50%;background:var(--gm-x-bg);border:none;cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  color:var(--gm-x-fg);padding:0;transition:opacity .2s;
}
.gm-close:hover{opacity:.7}
.gm-close svg{width:13px;height:13px;display:block}

/* Le soleil — même tracé pour le héros et le bouton, la couleur vient de
   currentColor (cf. UI_gmSoleilSvg). */
.gm-icon{
  display:flex;justify-content:center;align-items:center;
  margin:2px 0 14px;color:var(--gm-accent);
  filter:none;                /* l'ancienne lueur orange n'a plus lieu d'être */
}
.gm-soleil{width:60px;height:60px;display:block}

.gm-title{
  font-size:17px;font-weight:600;text-align:center;
  color:var(--gm-title);letter-spacing:-.2px;margin-bottom:2px;
}
.gm-count{
  font-size:66px;font-weight:700;text-align:center;line-height:1.06;
  color:var(--gm-accent);letter-spacing:-2.5px;margin:2px 0 6px;
  font-variant-numeric:tabular-nums;
  /* L'ancien compteur était un dégradé découpé au texte
     (-webkit-text-fill-color:transparent). On revient à une couleur pleine :
     un chiffre de 66 px se lit mieux net que fondu. */
  background:none;-webkit-text-fill-color:currentColor;
}
.gm-subtitle{
  font-size:14px;color:var(--gm-sub);text-align:center;
  margin-bottom:22px;line-height:1.45;letter-spacing:-.1px;
}

.btn-gm-claim{
  display:flex;align-items:center;justify-content:center;gap:8px;
  width:100%;padding:15px 24px;
  background:#FF6B2B;color:#fff;border:none;border-radius:14px;
  font-family:inherit;font-size:17px;font-weight:600;letter-spacing:-.2px;
  cursor:pointer;transition:opacity .2s,transform .12s;
  box-shadow:0 6px 18px rgba(255,107,43,.28);
}
.btn-gm-claim:hover{opacity:.92;background:#FF6B2B;transform:none;box-shadow:0 6px 18px rgba(255,107,43,.28)}
.btn-gm-claim:active{transform:scale(.985)}
.gm-btn-soleil{width:18px;height:18px;display:block;color:#fff;flex-shrink:0}
.btn-gm-claim.claimed{
  background:var(--gm-surface);color:var(--gm-sub);
  box-shadow:none;cursor:default;
}
.btn-gm-claim.claimed .gm-btn-soleil{color:var(--gm-sub)}

/* ── Feuille de RÉPARATION — mêmes tokens, ses éléments propres ── */
.gm-repair-cost{
  display:flex;justify-content:space-between;align-items:center;
  background:var(--gm-surface);border-radius:12px;padding:13px 14px;
  margin-bottom:14px;font-size:14px;color:var(--gm-sub);letter-spacing:-.1px;
}
.gm-repair-cost #gmRepairCostVal{font-weight:600;color:var(--gm-accent)}
.gm-repair-btn{background:#FF6B2B}
.gm-repair-btn:disabled{background:var(--gm-surface);color:var(--gm-sub);cursor:not-allowed;box-shadow:none}
.gm-repair-restart{
  display:block;width:100%;margin-top:8px;padding:14px 24px;
  background:transparent;border:1px solid var(--gm-hairline);border-radius:14px;
  color:var(--gm-title);font-family:inherit;font-weight:500;font-size:17px;
  letter-spacing:-.2px;cursor:pointer;transition:opacity .2s;
}
.gm-repair-restart:hover{opacity:.7}
.gm-repair-restart.is-only{background:#FF6B2B;border:none;color:#fff;font-weight:600}

/* 8. APP — état connecté */
#app{display:none;background:var(--bg);min-height:100vh;padding-bottom:70px}

/* 9. MOBILE NAV */
/* ⚠️ L'EN-TÊTE NE DÉFILE JAMAIS HORIZONTALEMENT. Les gouttières et le
   rembourrage sont resserrés (8→6px, 12→10px) et le mot-symbole est le SEUL
   élément élastique : min-width:0 l'autorise à se réduire sous sa largeur
   naturelle — sans lui, un enfant flex refuse de passer sous son contenu et
   c'est la barre entière qui déborde. overflow:hidden est le garde-fou final :
   même si un jour un élément de trop s'invitait, il serait rogné plutôt que de
   rendre la barre défilante. */
.app-nav{position:sticky;top:0;z-index:100;height:56px;display:flex;align-items:center;padding:0 10px;gap:6px;background:var(--bg);border-bottom:1px solid var(--border);overflow:hidden}
/* Le mot-symbole est un lien vers l'accueil (un <button> sur l'accueil même,
   où il remonte la page) : on neutralise les styles par défaut du bouton et
   on retire le soulignement du lien, pour que les deux rendus soient
   indiscernables. Il se comprime au lieu de pousser (min-width:0). */
.app-nav-logo{display:flex;gap:2px;align-items:center;flex:1 1 auto;min-width:0;cursor:pointer;text-decoration:none;overflow:hidden;background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;text-align:left}
.icon-btn{width:34px;height:34px;border-radius:9px;border:none;background:var(--bg3);display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:background .2s}
.icon-btn:hover{background:var(--border2)}
.icon-btn svg{width:17px;height:17px;stroke:var(--text)}
/* ══ PASTILLE DE LIGUE — un hexagone de cuivre, sans texte ═════════════════
   C'était une pastille « Ligue Bronze » de ~95px de large qui poussait l'avatar
   hors de l'écran : l'en-tête défilait horizontalement sur téléphone. Elle
   devient un hexagone plein de 26px, soit ~70px regagnés.

   ⚠️ LE CUIVRE EST FIXE, IL NE SUIT PAS LE PALIER. C'est un repère de marque,
   pas un indicateur d'état — la même couleur pour tout le monde, toujours.

   ⚠️ LE TEXTE EST NEUTRALISÉ EN CSS, PAS EN JS, ET C'EST DÉLIBÉRÉ. Cette
   pastille existe dans HUIT pages, et deux scripts y écrivent encore
   (profile-sync.js pour le libellé, auth.js pour l'état visiteur). Un
   font-size:0 sur le conteneur, remis à zéro nulle part ailleurs, garantit que
   l'hexagone reste muet quoi qu'on lui injecte — aucune page ne peut retrouver
   son ancien libellé par accident. */
.league-badge{
  display:flex;align-items:center;justify-content:center;
  width:26px;height:26px;flex-shrink:0;
  background:linear-gradient(150deg,#E8A56B 0%,#CD7F32 46%,#8C5420 100%);
  clip-path:polygon(50% 0%, 93.3% 25%, 93.3% 75%, 50% 100%, 6.7% 75%, 6.7% 25%);
  border:none;border-radius:0;padding:0;
  font-size:0;line-height:0;color:transparent;
  cursor:pointer;white-space:nowrap;
}
/* Le point de couleur du libellé d'origine n'a plus lieu d'être. */
.league-badge .league-dot{display:none}
.league-dot{width:7px;height:7px;border-radius:50%;background:var(--violet)}
.avatar-badge{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--orange),var(--violet));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:12px;color:#fff;border:2px solid var(--border2);cursor:pointer;flex-shrink:0;position:relative}
.lvl-badge{position:absolute;bottom:-4px;right:-4px;background:var(--orange);color:#fff;font-size:9px;font-weight:800;border-radius:5px;padding:1px 4px;border:1px solid var(--bg)}

/* 10. MOBILE SIDE MENUS */
.side-overlay{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);opacity:0;pointer-events:none;transition:opacity .3s}
.side-overlay.open{opacity:1;pointer-events:all}
.side-menu{position:fixed;top:0;left:0;bottom:0;width:280px;z-index:201;background:var(--bg);border-right:none;transform:translateX(-100%);transition:transform .35s var(--ease);display:flex;flex-direction:column}
.side-menu.open{transform:translateX(0)}
/* En-tête du burger, mis à l'échelle des entrées de 40px (passe 2).
   ⚠️ Ce n'est PAS l'avatar qui fixait la hauteur : le bloc texte pesait plus
   lourd que lui (25,6 + 2 + 19,2 = 46,8px contre 44). Réduire les 44px seuls
   n'aurait donc rien donné — il faut borner les DEUX interlignes, que ni
   .side-name ni .side-level ne déclaraient (ils héritaient du 1.6 du corps).
   .side-name / .side-level / .side-user / .side-avatar : le markup ne les pose
   qu'ici (shared.js:1949-1951). auth.js et profile-sync.js les lisent en
   sélecteur groupé avec les avatars bureau, mais n'y écrivent que du contenu
   (backgroundImage, color) — jamais de dimension. */
.side-header{padding:14px 14px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.side-user{display:flex;align-items:center;gap:10px}
.side-avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--orange),var(--violet));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:16px;color:#fff}
.side-name{font-size:15px;font-weight:800;line-height:1.3}.side-level{font-size:11px;line-height:1.3;color:var(--text2);margin-top:1px}
/* La liste absorbe l'espace restant ET défile si le contenu le dépasse.
   ⚠️ `min-height:0` n'est pas décoratif, c'est LA condition : un élément flex
   a `min-height:auto` par défaut, donc il refuse de se réduire sous la taille
   de son contenu. Sans lui, `overflow-y:auto` ne sert à rien — la liste
   déborderait quand même et pousserait le pied hors de l'écran, sans barre de
   défilement pour le signaler (mesuré avant : −26,3px sous le bord à 375×320).
   Pas de `max-height` en dur : `flex:1` dans une colonne de hauteur fixe donne
   déjà la bonne borne, et elle se recalcule si l'en-tête ou le pied changent —
   une valeur figée, elle, mentirait au premier ajustement.
   `overscroll-behavior:contain` empêche le défilement de se propager à la page
   derrière une fois la liste en butée. */
.side-links{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:8px 12px;display:flex;flex-direction:column;gap:2px}
.side-link{display:flex;align-items:center;gap:11px;padding:10px 14px;border-radius:10px;font-size:var(--menu-text-size);font-weight:500;line-height:1.5;color:var(--text2);cursor:pointer;transition:color .3s ease,background-color .3s ease;border:none;background:none;text-align:left;width:100%}
.side-link:hover,.side-link.active{background:var(--bg3);color:var(--text)}
.side-link svg{width:17px;height:17px;flex-shrink:0}

/* Libellé d'entrée du burger. C'est CE span qui gouverne la hauteur des lignes
   du menu — pas le `font-size` de .side-link juste au-dessus, qu'il écrase.
   Le chercher ici et nulle part ailleurs : la règle jumelle qui vivait à ~4720
   (`.side-menu .side-link .nav-text`) était un doublon strict des trois mêmes
   déclarations, retiré avec ce lot.

   Il était à 28px/1.8 — soit une ligne de 50,4px et des entrées de 74,4px de
   haut. « Vous êtes un commerce ? » mesure 347,9px à cette taille pour un
   budget de 172px : d'où ses TROIS lignes et une entrée de 175,2px.

   15px/1.35 ramène l'entrée à 40,25px et le libellé à 186,4px dans un budget
   de 200px (280 − 24 de .side-links − 28 de padding − 17 d'icône − 11 de gap).
   Mesuré : le repli sans-serif est plus ÉTROIT qu'Inter (184,8px), la police
   la plus large est donc celle qu'on charge — pas de mauvaise surprise si le
   webfont n'arrive pas.

   `margin-left:15px` retiré : il s'ajoutait au `gap:11px` du parent flex, ce
   qui faisait 27px entre l'icône et le texte tout en amputant le budget du
   libellé d'autant. Un seul mécanisme d'écart, le gap.
   `!important` retiré aussi : rien ne concourait (une règle portée par le span
   bat de toute façon le font-size hérité de .side-link). Il empêchait surtout
   toute reprise ultérieure. */
.side-menu .nav-text {
  font-size: 15px;
  line-height: 1.35;
  font-weight: 600;
}
.side-footer{padding:12px;border-top:1px solid var(--border)}
.btn-logout{width:100%;padding:11px 20px;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:var(--radius-md);color:var(--red);font-size:var(--text-sm);font-weight:600;cursor:pointer}
/* Le bouton de sortie du BURGER seulement. `.btn-logout` tout court ne serait pas
   borné au mobile : shared.js le pose AUSSI dans #dtSidebar (.qw-side-auth), qui
   est la sidebar bureau — et le tiroir burger entre 769 et 1279px. admin.html et
   merchant-dashboard.html en ont par-dessus une définition locale à eux.
   `.side-footer` n'existe, lui, que dans #sideMenu. */
.side-footer .btn-logout{padding:9px 16px}

/* Couple « Connexion / S'inscrire » du pied VISITEUR, même échelle (passe 2).
   Il restait à 14px/11px-20px et pesait 123,8px — plus que les trois entrées
   du visiteur réunies (120,8px).

   ⚠️ TROIS niveaux à ne pas confondre, tous rencontrés ici :
     · `.btn-anon-login` / `.btn-anon-signup` nus (style.css:2082, 2090) —
       INTOUCHABLES : ils habillent aussi le CTA visiteur de la topbar BUREAU
       (.dt-anon-cta, shared.js:1487) relevant du §11 ;
     · `.qw-menu-anon …` (style.css:8939) — PAS un scope burger : le même bloc
       vit dans #dtSidebar (shared.js:1830) ;
     · `.side-footer .qw-menu-anon …` — le seul qui soit vraiment privé.

   La bordure transparente sur « S'inscrire » ne décore rien : sans elle il
   mesure 2px de moins que « Connexion », qui porte un liseré de 1px. L'écart
   existait déjà (46,4 contre 44,4) ; à cette taille il se voit. */
.side-footer .qw-menu-anon{gap:6px}
.side-footer .qw-menu-anon .btn-anon-login,
.side-footer .qw-menu-anon .btn-anon-signup{
  font-size:13px; line-height:1.3; padding:10px 16px;
}
.side-footer .qw-menu-anon .btn-anon-signup{border:1px solid transparent}
.right-panel{position:fixed;top:0;right:0;bottom:0;width:290px;z-index:201;background:var(--bg);border-left:1px solid var(--border);transform:translateX(100%);transition:transform .35s var(--ease);display:flex;flex-direction:column}
.right-panel.open{transform:translateX(0)}
.rp-header{padding:18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.rp-title{font-size:var(--text-md);font-weight:800}
.rp-close{font-size:20px;cursor:pointer;color:var(--text2);background:none;border:none}
.rp-body{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}
.gm-card{background:linear-gradient(135deg,rgba(245,158,11,.1),rgba(255,107,43,.06));border:1px solid rgba(245,158,11,.25);border-radius:16px;padding:18px;text-align:center}
/* ══ LE SOLEIL GM, PARTOUT ═══════════════════════════════════════════════════
   Les surfaces GM portaient des emoji (🔥 sur les cartes de série, ☀️ sur les
   boutons). Elles reprennent le soleil de la feuille GM.

   ⚠️ POURQUOI UN MASQUE CSS ET NON UNE BALISE <svg> SUR LES BOUTONS.
   Six endroits font `bouton.textContent = '… GM — Valider …'` pour changer
   l'état du bouton. textContent REMPLACE tous les enfants : une icône posée en
   élément serait effacée au premier changement d'état, et ne reviendrait
   jamais. Un ::before est du CSS, pas du DOM — il survit. Le masque prend la
   couleur du texte (currentColor), donc blanc sur les boutons orange, sans
   qu'on ait à décliner le tracé. */
:root{
  --gm-soleil-masque:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='19'/%3E%3Cg stroke='%23000' stroke-width='6.5' stroke-linecap='round'%3E%3Cline x1='50' y1='9' x2='50' y2='21'/%3E%3Cline x1='50' y1='79' x2='50' y2='91'/%3E%3Cline x1='9' y1='50' x2='21' y2='50'/%3E%3Cline x1='79' y1='50' x2='91' y2='50'/%3E%3Cline x1='21.7' y1='21.7' x2='30.2' y2='30.2'/%3E%3Cline x1='69.8' y1='69.8' x2='78.3' y2='78.3'/%3E%3Cline x1='78.3' y1='21.7' x2='69.8' y2='30.2'/%3E%3Cline x1='30.2' y1='69.8' x2='21.7' y2='78.3'/%3E%3C/g%3E%3C/svg%3E");
}
/* Marque en ligne, avant le texte. `.dt-dd-item` en est exclu : l'entrée de
   menu a déjà sa colonne d'icônes, elle serait doublée. */
.gm-btn:not(.dt-dd-item)::before,
#actGmBtn::before,
.gptp-gm-anon::before,
.gm-icone-inline{
  content:'';
  display:inline-block;
  width:1.05em;height:1.05em;
  vertical-align:-.18em;
  margin-right:.42em;
  background-color:currentColor;
  -webkit-mask:var(--gm-soleil-masque) center/contain no-repeat;
  mask:var(--gm-soleil-masque) center/contain no-repeat;
  flex-shrink:0;
}
/* Dans la colonne d'icônes du menu déroulant, la marque remplit la case et
   n'a pas besoin de sa marge. */
.dt-dd-item-icon .gm-icone-inline{margin-right:0;width:1.1em;height:1.1em}

/* Carte de série du panneau droit : le soleil remplace la flamme. */
.gm-fire{
  width:38px;height:38px;margin:0 auto 8px;color:var(--orange);
  background-color:currentColor;
  -webkit-mask:var(--gm-soleil-masque) center/contain no-repeat;
  mask:var(--gm-soleil-masque) center/contain no-repeat;
}
.gm-label{font-size:15px;font-weight:800;margin-bottom:4px}.gm-sub{font-size:12px;color:var(--text2);margin-bottom:14px}
.gm-stats{display:flex;gap:16px;justify-content:center;margin-bottom:14px}.gm-stat-num{font-size:22px;font-weight:800;color:var(--yellow)}.gm-stat-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:1px}
.gm-btn{width:100%;padding:11px 20px;background:linear-gradient(135deg,var(--yellow),var(--orange));border:none;border-radius:var(--radius-md);color:#fff;font-size:var(--text-sm);font-weight:700;cursor:pointer}
.streak-row{display:flex;gap:5px;justify-content:center;margin-top:12px}
.s-dot{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700}
.s-dot.ok{background:var(--yellow);color:#000}.s-dot.today-dot{background:var(--orange);color:#fff;box-shadow:0 0 10px rgba(255,107,43,.5)}.s-dot.empty{background:var(--bg3);border:1px solid var(--border);color:var(--text3)}
.rp-info-row{background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:12px 14px;display:flex;align-items:center;justify-content:space-between}
.rp-info-label{font-size:13px;color:var(--text2)}.rp-info-val{font-size:14px;font-weight:700}

/* 11. CARROUSEL — .ac-slide et .cdot servent le carrousel BUREAU (#dtAutoCarousel,
   construit par carousel.js). Le reste de ce bloc a été écrit pour la maquette
   mobile, retirée depuis ; vérifié au runtime : zéro instance hors carrousel. */

.auto-carousel .ac-slide,.dt-carousel .ac-slide{display:none;animation:acFadeIn .45s var(--ease)}
.auto-carousel .ac-slide.active,.dt-carousel .ac-slide.active{display:block}
@keyframes acFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* Points de pagination. Le point actif s'allonge en pastille et SE REMPLIT en
   blanc sur 5 s ; au bout du compte le carrousel avance et le suivant démarre.

   ⚠️ LE REMPLISSAGE PASSE PAR background-size, PAS PAR UN PSEUDO-ÉLÉMENT.
   ::after est déjà pris : le LOT 1 s'en sert pour porter la cible tactile de
   28px, adossée au pas des points (une tentative à 44px a été mesurée comme
   inutilisable, elle chevauchait trois voisins). Un ::before aurait demandé
   overflow:hidden sur .cdot pour épouser l'arrondi — ce qui aurait ROGNÉ cette
   cible tactile et cassé le tap au doigt. Animer le fond n'a aucun de ces
   effets : le remplissage suit l'arrondi tout seul et ne déborde de rien.
   position:relative reste posé ici pour la cible du LOT 1 (qui le déclarait
   déjà de son côté sur mobile). */
.cdot{width:6px;height:6px;border-radius:50%;background:var(--border2);transition:all .3s;cursor:pointer;position:relative}
.cdot.active{
  width:18px;border-radius:3px;
  background-color:color-mix(in srgb, var(--orange) 40%, transparent);  /* la piste */
  background-image:linear-gradient(to right,#fff,#fff);                  /* le remplissage */
  background-repeat:no-repeat;
  background-size:0% 100%;
  animation:cdotFill 5s linear forwards;
}
@keyframes cdotFill{from{background-size:0% 100%}to{background-size:100% 100%}}
/* Survol du carrousel : la rotation s'arrête, le remplissage aussi. La classe
   est posée par carousel.js — #dtDots est un FRÈRE de #dtAutoCarousel, aucun
   sélecteur descendant ne pouvait relier le survol de l'un au style de l'autre. */
#dtDots.l3-pause .cdot.active{animation-play-state:paused}
/* Mouvement réduit : ni remplissage progressif, ni rotation automatique (le
   minuteur se tait de son côté). Le point actif reste plein, donc lisible, et
   tous restent cliquables. */
@media (prefers-reduced-motion: reduce){
  .cdot{transition:none}
  .cdot.active{animation:none;background-size:100% 100%}
}

/* ══ Coquille commune des diapositives du carrousel (.l3cc-*) ═══════════════
   Fond = le dégradé de la campagne, posé au montage via .style.
   Les DEUX diapositives sont en colonne unique depuis les étapes 2 et 3 :
   campagne (.l3cc-campagne) et quête (.l3cc-quete) l'annulent plus bas. La
   seconde colonne qui portait l'image plein cadre a disparu avec elle. */
.l3cc-card{
  position:relative;height:420px;border-radius:20px;overflow:hidden;cursor:pointer;
  display:grid;grid-template-columns:minmax(0,1fr) 42%;grid-template-rows:minmax(0,1fr);
  transition:transform .35s cubic-bezier(.4,0,.2,1),box-shadow .35s;will-change:transform;
}
/* Contour net, comme .ob-card : la diapositive n'avait AUCUNE bordure. Même
   liseré clair doublé d'un anneau sombre extérieur, pour que le contraste ne
   dépende pas du dégradé de campagne, qui change d'une diapositive à l'autre. */
.l3cc-card{
  border:1px solid rgba(255,255,255,.28);
  box-shadow:0 0 0 1px rgba(0,0,0,.55), 0 6px 22px rgba(0,0,0,.42);
}
.l3cc-card:hover{transform:translateY(-4px);
  border-color:rgba(255,255,255,.42);
  box-shadow:0 0 0 1px rgba(0,0,0,.55), 0 28px 72px rgba(0,0,0,.6)}

/* ── Cœur et banderole du CARROUSEL — mêmes composants, autre échelle ────────
   Les deux réutilisent les classes des cartes (.ob-quest-heart, .qc-chip) pour
   hériter du dessin ET, pour le cœur, du gestionnaire d'écriture de shared.js.
   Seule la TAILLE change : les valeurs des cartes sont calées sur 233px de
   large, le héros en fait 766. La corde visible de la banderole vaut
   √2·(cx+cy) — ici ~117px pour un texte qui en demande ~63.
   Le cœur reste seul à droite, la banderole seule à gauche. */
.l3cc-card .l3cc-chip{
  top:30px;left:-36px;width:160px;
  padding:5px 0;font-size:10px;letter-spacing:.12em;z-index:6;
}
.l3cc-card .l3cc-heart{
  top:16px;right:16px;width:30px;height:30px;z-index:6;
}
.l3cc-card .l3cc-heart svg{width:24px;height:24px}

.l3cc-left{display:flex;flex-direction:column;justify-content:center;gap:8px;padding:22px 24px;min-width:0;
  background:linear-gradient(100deg,rgba(0,0,0,.30) 0%,rgba(0,0,0,.08) 60%,rgba(0,0,0,0) 100%)}  /* voile de lisibilité discret — colonne gauche seulement (image droite intacte) */
.l3cc-pax{align-self:flex-start;display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:rgba(255,255,255,.7)}
.l3cc-badge{align-self:flex-start;display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:800;letter-spacing:.4px;text-transform:uppercase;padding:4px 10px;border-radius:999px;background:rgba(255,255,255,.15);color:#fff}
.l3cc-title{font-size:clamp(22px,2.9vw,34px);font-weight:900;color:#fff;letter-spacing:-.6px;line-height:1.1;margin-top:2px}
.l3cc-desc{font-size:13px;color:rgba(255,255,255,.78);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-width:94%}
.l3cc-bottom{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.l3cc-datecard{align-self:flex-start;display:flex;align-items:center;gap:14px;padding:8px 12px;border-radius:12px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.14)}
.l3cc-date{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:rgba(255,255,255,.85)}
.l3cc-live{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;color:#fff}
.l3cc-live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px rgba(34,197,94,.25)}
.l3cc-pax svg,.l3cc-date svg{width:13px;height:13px;opacity:.8}
.l3cc-foot{display:flex;align-items:center;min-height:24px}
.l3cc-logo{height:24px;width:auto;max-width:140px;object-fit:contain}
.l3cc-logo-txt{font-size:12px;font-weight:800;color:#fff}


/* ══ SLIDE CAMPAGNE — l'emblème en tête (étape 2) ═══════════════════════════
   Colonne unique : compteur en haut à gauche, emblème centré sur son socle,
   puis logo carré + libellé, titre, description sur deux lignes, rangée de
   dates. L'image plein cadre a disparu — l'emblème tient ce rôle.

   ⚠️ TOUT EST PORTÉ PAR .l3cc-campagne, jamais par .l3cc-card seule : la
   diapositive QUÊTE partage la même coquille et garde sa mise en page à deux
   colonnes jusqu'à l'étape 3. Rien ici ne touche les classes .qc-* / --qc-* de
   la carte de quête, en production depuis le lot c2.

   ⚠️ La lueur n'utilise pas l'accent brut : UI_buildGradient3 retient le
   premier hex du color_theme, souvent le stop SOMBRE du dégradé. Un halo sombre
   sur fond sombre ne se voit pas — on remonte la luminance vers le blanc, ce
   qui préserve la teinte. Même raisonnement qu'au lot c2. */
/* Jetons et coquille partagés par les DEUX diapositives refaites (campagne à
   l'étape 2, quête à l'étape 3). Ajouter .l3cc-quete à cette liste ne change
   rien au calcul de la campagne : elle continue de matcher à l'identique. */
.l3cc-campagne,
.l3cc-quete{
  --l3cc-accent: var(--orange, #FF6B2B);
  --l3cc-glow-color: color-mix(in srgb, var(--l3cc-accent) 62%, #FFFFFF);
  grid-template-columns:minmax(0,1fr);           /* annule les 2 colonnes de .l3cc-card */
  grid-template-rows:minmax(0,1fr);
  text-decoration:none;color:inherit;            /* ce sont des <a> */
}
/* ⚠️ VOILE SOMBRE EN HAUT, ET C'EST CE QUI REND LA LUEUR VISIBLE. Le fond de la
   carte est le dégradé de la campagne, souvent vif et saturé ; une lueur tirée
   de CE MÊME dégradé s'y noyait — mesuré : halo vert sur fond vert, invisible.
   Au lot c2 l'emblème se détachait sur un fond volontairement sombre. On
   recrée ici cette assise : le tiers haut s'assombrit, la lueur retrouve son
   contraste, et le texte du bas gagne en lisibilité par la même occasion. */
.l3cc-campagne .l3cc-left{
  position:relative;justify-content:flex-start;gap:6px;padding:12px 16px 14px;
  background:linear-gradient(180deg,
    rgba(0,0,0,.52) 0%, rgba(0,0,0,.30) 34%, rgba(0,0,0,.10) 62%, rgba(0,0,0,.28) 100%);
}
/* Compteur — ancré en haut à gauche, hors du flux : l'emblème peut ainsi se
   centrer sur toute la largeur sans être décalé par lui. */
.l3cc-campagne .l3cc-pax{position:absolute;top:12px;left:16px;z-index:2}

/* Socle + lueur + emblème */
/* ⚠️ LE SOCLE EST ÉLASTIQUE, ET C'EST LUI QUI RÈGLE LA HAUTEUR (étape 5).
   Il était en flex:0 0 auto sur 58px : chaque pixel de hauteur ajouté à la
   carte tombait alors dans l'UNIQUE gouttière laissée par le margin-top:auto du
   pied. Mesuré avant correctif : 45px de mou à 220px de carte, mais 165px à
   340px — la carte grandissait en se vidant. En flex:1 1 auto, le socle absorbe
   le mou AUTOUR de l'emblème : la hauteur gagnée devient de la respiration là
   où on la veut, et non un trou au-dessus du pied. */
.l3cc-plinth{
  position:relative;align-self:center;flex:1 1 auto;
  display:flex;align-items:center;justify-content:center;
  width:100%;min-height:58px;margin-bottom:2px;
}
/* ══ LE SPOT ════════════════════════════════════════════════════════════════
   Une lumière chaude tombant d'en haut sur le médaillon. Distincte du halo
   d'accent (.l3cc-glow), qu'elle ne remplace pas : celui-ci teinte le fond aux
   couleurs de la campagne, celle-là éclaire.

   ⚠️ COULEUR LITTÉRALE, PAS D'ACCENT. Deux raisons, et la première est
   esthétique : une lumière qui prend la couleur de l'objet qu'elle éclaire ne
   se lit pas comme une lumière, seulement comme « plus de campagne ». C'est le
   contraste chaud/teinté qui la fait exister, et qui donne au liseré blanc
   quelque chose à accrocher. La seconde est mécanique : une valeur littérale
   n'a aucune variable à résoudre, donc aucune chaîne de secours à maintenir —
   `color-mix()` avec une variable non résolue devient invalide au calcul et
   retombe à la valeur INITIALE (transparent), le piège documenté plus bas.

   ⚠️ SŒUR DE L'EMBLÈME, POSÉE AVANT LUI. Elle ne doit surtout pas l'englober :
   `.l3cc-coin` est penchée en 3D et vit à côté du socle précisément pour ne pas
   être rognée — un ancêtre supplémentaire porteur d'un filtre ou d'un mélange
   l'aplatirait. Ici la couche est un frère, elle ne peut rien rogner.

   Le foyer est à 30 % de sa hauteur, pas au centre : c'est ce décentrage vers
   le haut qui lit « cône venu d'en haut » plutôt que « halo posé autour ».
   `screen` éclaircit sans délaver — `plus-lighter`, essayé, sature la face
   supérieure pâle du socle et lui mange son arête. */
.l3cc-spot{
  position:absolute;top:50%;left:50%;width:262px;height:236px;
  transform:translate(-50%,-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse 52% 46% at 50% 30%,
    rgba(255,248,238,.36) 0%,
    rgba(255,248,238,.15) 44%,
    rgba(255,248,238,0) 72%);
  mix-blend-mode:screen;
  filter:blur(9px);
}
.l3cc-glow{
  position:absolute;top:50%;left:50%;width:320px;height:320px;
  transform:translate(-50%,-50%);border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,
    color-mix(in srgb, var(--l3cc-glow-color) 55%, transparent) 0%,
    color-mix(in srgb, var(--l3cc-glow-color) 20%, transparent) 42%,
    transparent 70%);
  filter:blur(13px);
}
/* ══ LE SOCLE OCTOGONAL ═════════════════════════════════════════════════════
   Huit côtés, teinté par l'accent de la diapositive : il suit donc le dégradé
   de la carte au lieu de s'y poser comme un corps étranger. Relief obtenu par
   un dégradé qui éclaircit le haut et assombrit les flancs, plus un liseré
   intérieur clair sur l'arête supérieure. L'emblème y est légèrement incliné.

   ⚠️ DEUX ÉLÉMENTS, ET C'EST OBLIGATOIRE. .l3cc-emblem ne porte QUE l'ombre
   portée, .l3cc-octo porte la découpe. Les réunir ne marcherait pas : le CSS
   applique `filter` AVANT `clip-path`, si bien qu'une ombre déclarée sur
   l'élément découpé serait calculée sur le carré puis rognée avec lui — donc
   invisible. Un box-shadow ne convient pas davantage : il suit la boîte, pas
   la découpe, et dessinerait l'ombre d'un carré sous un octogone. D'où
   filter:drop-shadow() sur le parent, qui épouse la forme réellement peinte. */
/* ⚠️ LA PIÈCE EST SŒUR DE L'OCTOGONE, PAS SON ENFANT. C'est ce qui permet à
   l'emblème de FLOTTER : placé dedans, il était rogné par le clip-path et ne
   pouvait pas déborder l'arête haute du socle. Il vit donc à côté, en position
   absolue, remonté au-dessus du centre de l'octogone qu'il chevauche.
   Le socle est descendu d'autant (margin-top), pour que l'ensemble reste
   centré dans le plinthe sans en changer la hauteur. */
.l3cc-emblem{
  position:relative;z-index:2;display:flex;justify-content:center;
  /* Allégée : trois ombres se disputaient la zone (celle-ci, le box-shadow au
     sol de la pièce, et .l3cc-cast), d'où un halo mou. Celle-ci reste — le socle
     est une forme découpée, un box-shadow y dessinerait l'ombre d'un carré —
     mais cède le premier rôle au contact sous la pièce. */
  filter:drop-shadow(0 6px 11px rgba(0,0,0,.34));
}
/* ⚠️ LE SOCLE N'EST PLUS UNE DÉCOUPE, C'EST UN DESSIN. Le clip-path et son
   dégradé ont laissé place à un SVG isométrique (carousel.js, _SVG_SOCLE) qui
   trace la face supérieure, les flancs et les arêtes — un vrai piédestal, pas
   une silhouette plate qu'un dégradé essaie de faire passer pour du relief.
   Le conteneur ne garde donc que la boîte ; toute la forme est dans le SVG.

   Les teintes restent dérivées de --l3cc-accent, exactement comme l'ancien
   dégradé à 168° : dessus éclairci, flanc gauche à mi-chemin, face plus sombre,
   flanc droit le plus sombre — la lumière vient toujours du haut à gauche.
   ⚠️ Chaque color-mix porte sa chaîne de secours (var(--l3cc-accent, …)) : une
   variable non résolue rend color-mix INVALIDE AU CALCUL, et la propriété
   retombe alors à sa valeur initiale — un socle transparent, pas la couleur
   précédente. Même piège que celui déjà documenté pour .l3cc-emblem-initial. */
/* ⚠️ LA BOÎTE GRANDIT (88 → 116), ET LA MARGE COMPENSE (26 → 12). Le dessin
   n'occupe que ~60 % de la hauteur de sa viewBox carrée — un piédestal est plus
   large que haut. À 88px le plateau ne mesurait plus que 33px de haut sous une
   pièce de 66 : la pièce l'avalait. À 116 le plateau dépasse d'une quinzaine de
   pixels de chaque côté et le socle se lit enfin. Hauteur totale de l'emblème
   inchangée à 2px près (26+88=114 → 12+116=128, pour 156px de plinthe). */
.l3cc-octo{
  position:relative;margin-top:19px;
  width:186px;height:186px;
  --octo-acc:var(--l3cc-accent, var(--orange, #FF6B2B));
  --octo-dessus:color-mix(in srgb, var(--octo-acc) 40%, #FFFFFF);
  --octo-cote-g:color-mix(in srgb, var(--octo-acc) 58%, #14141C);
  --octo-face:color-mix(in srgb, var(--octo-acc) 46%, #0C0C12);
  --octo-cote-d:color-mix(in srgb, var(--octo-acc) 34%, #07070B);
  --octo-arete:rgba(255,255,255,.34);
  --octo-arete-vive:rgba(255,255,255,.95);   /* les arêtes hautes, face à la lumière */
  /* Lueur de l'arête : large et très peu opaque, passée SOUS le trait vif.
     Un halo peint, pas un filtre — voir carousel.js pour le pourquoi (les ids). */
  --octo-arete-halo:rgba(255,255,255,.14);
  /* `--octo-arete-bas` a été RETIRÉE avec l'arête de pied qu'elle colorait :
     un pied qui se dissout ne porte pas de trait. Voir carousel.js. */
}
.l3cc-octo-svg{
  position:absolute;inset:0;width:100%;height:100%;display:block;
  overflow:visible;   /* le liseré de 1,5 est centré sur le tracé : il déborde d'un demi-trait */
}
/* Les flancs s'éteignent vers le bas : le socle s'enfonce dans la carte au lieu
   de s'arrêter sur une arête franche. Le masque agit sur le GROUPE, donc d'un
   seul geste sur les trois faces et le trait de pied. */
/* ⚠️ LE MASQUE S'ÉTEINT AU-DESSUS DES PIEDS, ET C'EST TOUT LE CORRECTIF.
   La passe précédente gardait 0,18 d'opacité à 84 % et n'atteignait zéro qu'à
   92 %. Or les coins extérieurs du fût tombaient à 76,4 % : ils étaient donc
   peints à 0,41 — d'où la silhouette du pied encore lisible, et le petit trait
   horizontal qui la soulignait.
   Ici le noir complet est atteint à 74 %, tandis que le prisme a été allongé
   (36 → 40) pour repousser ses coins à 78,3 %. Il n'y a plus AUCUNE géométrie
   du côté visible du fondu : le fût s'évanouit dans le fond au lieu de s'y
   arrêter sur une forme.
   Le sommet reste plein jusqu'à 42 % — la descente que Jonathan a validée est
   conservée : le pilier disparaît vers y≈84 dans la viewBox, contre 79,9 avant
   le lot précédent. On perd la queue fantôme, pas la profondeur.
   Quatre paliers rapprochés plutôt qu'une rampe droite : une rampe linéaire
   laisse une bande perceptible au moment où elle quitte le noir. */
/* ⚠️ LE MASQUE SE RÉSOUT SUR LA BBOX DU GROUPE, pas sur la viewBox : mesuré,
   y 46,8 → 97,9. Repères du prisme, en % de cette bbox :
       0 %    jointure avec le plateau (G/D)
      78,3 %  coins EXTÉRIEURS du pied (Gb/Db)
     100 %    pointe avant, le point le plus bas (Fb/Eb)
   Deux faits commandent la recette : sous 78,3 % il ne reste QUE la face avant
   (les flancs latéraux sont finis, on peut y descendre sans rien réveiller) ;
   et les coins du pied — ce qui trahissait une semelle posée — vivent aux x
   EXTRÊMES, là où le flanc est réduit à un fil.
   D'où DEUX passes linéaires : la VERTICALE règle la profondeur et laisse la
   face avant descendre jusqu'à sa pointe ; l'HORIZONTALE éteint les deux
   liserés extrêmes, donc les coins du pied, sans toucher ni la face avant
   (29 → 71 % de la largeur) ni la jointure F→E. Un radial ne savait pas faire
   les deux : pincer assez les côtés l'obligeait à mordre sur la jointure. */
.l3cc-flancs{
  -webkit-mask-image:
    linear-gradient(to bottom, #000 34%, rgba(0,0,0,.94) 52%, rgba(0,0,0,.82) 66%, rgba(0,0,0,.66) 78%, rgba(0,0,0,.44) 88%, rgba(0,0,0,.20) 95%, transparent 100%),
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.55) 8%, #000 22%, #000 78%, rgba(0,0,0,.55) 92%, transparent 100%);
  mask-image:
    linear-gradient(to bottom, #000 34%, rgba(0,0,0,.94) 52%, rgba(0,0,0,.82) 66%, rgba(0,0,0,.66) 78%, rgba(0,0,0,.44) 88%, rgba(0,0,0,.20) 95%, transparent 100%),
    linear-gradient(to right, transparent 0%, rgba(0,0,0,.55) 8%, #000 22%, #000 78%, rgba(0,0,0,.55) 92%, transparent 100%);
  -webkit-mask-composite:source-in;   /* syntaxe héritée WebKit */
  mask-composite:intersect;           /* syntaxe standard */
}

/* Halo de sol. Les flancs s'éteignent sur un fond PLUS CLAIR qu'eux : même
   parfaitement fondus, la transition tonale se voyait. Cette flaque d'ombre
   leur donne une destination sombre vers laquelle aller.
   z-index négatif : le halo passe DERRIÈRE le SVG, il ne voile pas le socle. */
.l3cc-octo::after{
  content:'';
  position:absolute;
  left:50%;bottom:4%;
  transform:translateX(-50%);
  width:104%;height:40%;
  z-index:-1;
  pointer-events:none;
  background:radial-gradient(ellipse at center,
    rgba(0,0,0,.62) 0%, rgba(0,0,0,.34) 42%, rgba(0,0,0,.12) 66%, transparent 80%);
  filter:blur(9px);
}
/* L'ombre PORTÉE sur la face supérieure — c'est elle qui vend l'élévation de
   la pièce au-dessus du socle.
   ⚠️ ELLE N'EST PLUS ROGNÉE. Le clip-path qui la contenait a disparu avec la
   découpe : elle est donc RESSERRÉE et remontée pour rester sur la face du
   dessus (centrée vers 45 % de la boîte, là où le SVG dessine le plateau).
   Trop large ou trop basse, elle baverait sur les flancs, où une ombre projetée
   n'a rien à faire. */
.l3cc-cast{
  /* Recalée à 53 % : c'est là que se trouve LA BASE DU DISQUE une fois la pièce
     remontée sur le plateau. À 47 % l'ombre traînait 7px derrière lui — une
     ombre décrochée de son objet, qui défait l'illusion au lieu de la servir. */
  /* LA seule ombre de contact. Resserrée à 25 % parce que la pièce tournée perd
     23 % de largeur (cos 40° = 0,766) : à 33 % elle débordait de part et
     d'autre. Assombrie pour porter seule ce que trois ombres faisaient mal. */
  position:absolute;left:50%;top:48%;transform:translate(-50%,-50%);
  width:25%;height:8.5%;border-radius:50%;pointer-events:none;
  background:radial-gradient(ellipse at center,
    rgba(0,0,0,.74) 0%, rgba(0,0,0,.42) 44%, rgba(0,0,0,.14) 66%, transparent 80%);
  filter:blur(4px);
}
/* La pièce — remontée, penchée, avec son ombre propre, plus resserrée que
   celle du socle : c'est ce contraste qui la fait lire « au-dessus ». */
/* ⚠️ UNE PIÈCE RONDE, PAS UNE PLAQUE. La passe précédente lui avait donné un
   panneau carré à coins arrondis en verre dépoli : ce n'est pas la référence.
   Layer3 pose une PIÈCE — un disque net, cerclé de blanc, légèrement penché,
   qui flotte au-dessus du socle et y projette son ombre.

   Le blanc du fond joue DEUX rôles à la fois : il est l'anneau (par le
   padding) et le disque plein sur lequel le logo repose. C'est ce disque qui
   rend la pièce universelle — un logo transparent, carré ou rond y devient
   rond de la même façon, et la silhouette circulaire donne enfin à l'ombre un
   contour auquel se raccrocher. */
/* ⚠️ LA PIÈCE SE POSE SUR LE SOCLE, ELLE NE LE RECOUVRE PLUS. Mesuré avant :
   66px de disque sur un plateau de 97 — 68 % de sa largeur — et un bas qui
   descendait 15px SOUS l'arête avant, donc par-dessus les flancs. Le piédestal
   dessiné en 305a183 disparaissait derrière son propre logo.
   Désormais 54px (56 % du plateau) et remontée : le bas du disque s'arrête au
   ras de l'arête avant, les trois flancs restent entièrement visibles dessous,
   et il reste une vingtaine de pixels de plateau de chaque côté. Le trophée sur
   son socle, au lieu du couvercle sur sa boîte.
   ⚠️ Ne pas se fier à getBoundingClientRect pour juger la taille : rotate(-12deg)
   gonfle la boîte englobante à 78px alors que le disque, lui, reste à 54 —
   une rotation ne change pas la silhouette d'un cercle. */
/* ── LA PIÈCE : un MÉDAILLON INCLINÉ, plus un disque plat ────────────────────
   Trois choses la font lire en volume :
     · `perspective() rotateX()` la bascule — le cercle devient une ellipse, vue
       de trois quarts comme le socle sous elle ;
     · une PILE DE box-shadow décalées vers le bas dessine sa TRANCHE. C'est le
       point qui compte : box-shadow épouse le border-radius ET suit la
       transformation, donc la tranche reste correcte une fois la pièce
       inclinée. Un drop-shadow n'aurait donné qu'une ombre, pas une épaisseur ;
     · la dernière ombre, large et floue, est portée sur le plateau.

   ⚠️ POURQUOI CETTE TRANSFORMATION NE CASSE PAS L'EMPILEMENT. Le parent
   .l3cc-emblem porte un `filter: drop-shadow`, qui crée un contexte
   d'empilement — c'est ce qui avait fait écarter le rotateX pour le SOCLE
   (305a183). Ici la bascule est CONFINÉE à la pièce : pas de
   `transform-style: preserve-3d`, pas de scène 3D partagée, chaque élément est
   aplati indépendamment. La pièce reste la SŒUR suivante du socle dans le flux,
   donc peinte après lui — elle le recouvre sans qu'aucun z-index soit requis.
   L'ordre du DOM suffit, et c'est le plus robuste des empilements.

   ⚠️ L'ORDRE DES FONCTIONS EST SIGNIFIANT : `perspective()` doit précéder les
   rotations qu'elle gouverne, sinon elle ne s'applique à rien. Et translateX
   reste en tête pour que le centrage ne subisse pas la perspective. */
.l3cc-coin{
  position:absolute;top:0;left:50%;
  /* ⚠️ AUCUNE `perspective()` — c'est le correctif de fond. Le socle est une
     projection PARALLÈLE (octogone régulier aplati de 0,45, cf. _SVG_SOCLE) ;
     `perspective(304px)` sur un disque de 96px rendait son bas 15 à 21 % plus
     grand que son haut, soit un œuf posé sur un socle sans convergence. Deux
     systèmes de projection dans la même image : aucun réglage d'angle ne
     pouvait les réconcilier. rotateX seul, sans perspective, EST une projection
     parallèle — le système du socle.
     Les angles se DÉDUISENT, ils ne se cherchent pas :
       · dessus du socle = plan horizontal en raccourci 0,45 ; en projection
         parallèle ce raccourci vaut sin(élévation) ⇒ caméra à arcsin(0,45)
         = 26,74°, d'où rotateX ;
       · rotateY(40°) est le TOUR autour de l'axe vertical : c'est lui qui met
         la pièce de trois quarts et découvre sa tranche à gauche ;
       · rotateZ(-12°) n'a aucun effet sur la silhouette d'un cercle — il incline
         le LOGO. */
  transform:translateX(-50%) rotateX(26.74deg) rotateY(40deg) rotateZ(-12deg);
  display:flex;align-items:center;justify-content:center;
  width:96px;height:96px;
  border-radius:50%;
  background:#FFFFFF;
  padding:6px;box-sizing:border-box;      /* l'épaisseur de l'anneau blanc */
  /* ⚠️ LA TRANCHE SUIT LE FLANC, PLUS LE BAS. Les décalages d'un box-shadow
     vivent dans l'espace LOCAL et subissent la transformation : « vers le bas »
     convenait à une pièce de face, pas à une pièce tournée, dont l'épaisseur
     part vers l'arrière-gauche. La direction est RÉSOLUE, pas choisie : l'axe
     local Z se projette en (sin p, −sin e cos p) ; en inversant la matrice de
     projection puis en défaisant rotateZ, il vient la direction unitaire locale
     (−0.898, +0.438). Quatre paliers plus sombres et plus serrés que les six
     gris clair d'avant — qui faisaient une jupe pâle, pas un chant — et un pied,
     à 2, 4, 6, 8 et 9 px le long d'elle.
     L'ancienne ombre AU SOL est retirée : .l3cc-cast la remplace, seule. */
  box-shadow:
    -1.8px 0.9px 0 #C4C4CE, -3.6px 1.8px 0 #9E9EAB,
    -5.4px 2.6px 0 #7C7C89, -7.2px 3.5px 0 #5E5E6B,
    -8.1px 3.9px 1px rgba(0,0,0,.55);
}
/* Le logo est DÉCOUPÉ EN ROND, mais AJUSTÉ (contain), jamais recadré.
   ⚠️ object-fit:cover a été essayé et rejeté sur preuve : il remplit bien le
   disque pour un logo carré, mais il DÉTRUIT un logo panoramique. Le mot-symbole
   livré dans le dépôt (logo-qwestify.svg, viewBox 206×24, soit 8,6:1) s'y
   réduisait à deux taches de couleur illisibles — comparé côte à côte.
   `contain` ne coûte rien au logo carré (il remplit le disque de la même façon)
   et sauve le panoramique. Le disque blanc du dessous fait le reste du travail :
   c'est lui, pas le recadrage, qui donne à toute image sa lecture ronde. */
.l3cc-coin img{
  width:100%;height:100%;display:block;
  border-radius:50%;object-fit:contain;
}
/* Sans logo : la pièce devient un jeton teinté par l'accent, initiale au centre. */
/* ⚠️ LA VARIABLE PORTE SA PROPRE VALEUR DE SECOURS, ET C'EST INDISPENSABLE.
   Si --l3cc-accent n'est pas résolue, `color-mix()` devient invalide au calcul
   et le `background` retombe à sa valeur INITIALE — transparent — et non à une
   déclaration précédente : c'est la règle des valeurs invalides au calcul, que
   var() ne rattrape pas. On obtiendrait alors du texte blanc sur le disque
   blanc de la pièce, soit une initiale illisible au moment précis où elle sert
   de dernier recours. La chaîne de secours garantit un fond sombre en toute
   circonstance. */
.l3cc-emblem-initial{
  width:100%;height:100%;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:color-mix(in srgb, var(--l3cc-accent, var(--orange, #FF6B2B)) 68%, #12121A);
  color:#fff;font-size:30px;font-weight:800;line-height:1;
}

/* Rangée enseigne — logo carré + libellé « Campagne » */
/* Enseigne EMPILÉE (façon Layer3) : un grand logo carré, le libellé dessous,
   puis le titre. Le logo et le libellé étaient côte à côte sur une seule ligne ;
   l'ordre du gabarit était déjà le bon, seule la direction du flex change. */
.l3cc-brand{display:flex;flex-direction:column;align-items:flex-start;gap:7px;min-width:0}
.l3cc-sqlogo{width:52px;height:52px;border-radius:14px;object-fit:contain;
  background:rgba(255,255,255,.10);flex-shrink:0;
  box-shadow:0 4px 12px rgba(0,0,0,.28),inset 0 0 0 1px rgba(255,255,255,.12)}
.l3cc-campagne .l3cc-badge{padding:3px 9px;font-size:9px;letter-spacing:.5px}

/* Titre + description resserrés pour la colonne unique */
.l3cc-campagne .l3cc-title{font-size:17px;line-height:1.15;max-height:2.3em;overflow:hidden;margin-top:0}
.l3cc-campagne .l3cc-desc{font-size:11.5px;line-height:1.35;max-width:100%}

/* Rangée de dates — passe à la ligne plutôt que de déborder */
.l3cc-campagne .l3cc-datecard{align-self:stretch;flex-wrap:wrap;gap:8px;
  padding:6px 10px;border-radius:10px;margin-top:auto}
.l3cc-campagne .l3cc-date{font-size:11px}
.l3cc-campagne .l3cc-live{font-size:10.5px}

/* ══ SLIDE QUÊTE — l'emblème en tête (étape 3) ══════════════════════════════
   Même architecture que la diapositive campagne : colonne unique, emblème
   centré sur son socle, logo carré + libellé, titre, barre segmentée, pied.
   L'image plein cadre disparaît — mesurée à 375px, elle recouvrait la ligne
   « +50 XP », qui devenait illisible.
   Rien ici ne touche les classes .qc-* / --qc-* de la carte de quête (lot c2),
   ni la diapositive campagne, dont le rendu reste inchangé. */
.l3cc-quete .l3cc-left{
  position:relative;justify-content:flex-start;gap:6px;padding:12px 16px 14px;
  /* Même voile sombre qu'en campagne : sans lui la lueur, tirée du dégradé de
     la campagne parente, se noie dans ce même dégradé. */
  background:linear-gradient(180deg,
    rgba(0,0,0,.52) 0%, rgba(0,0,0,.30) 34%, rgba(0,0,0,.10) 62%, rgba(0,0,0,.28) 100%);
}
.l3cc-quete .l3cc-title{font-size:17px;line-height:1.15;max-height:2.3em;overflow:hidden;margin-top:0}

.l3cq-label{font-size:9px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;
  color:rgba(255,255,255,.72);padding:3px 9px;border-radius:999px;background:rgba(255,255,255,.15)}

/* Barre segmentée — affinée pour s'accorder à la carte de quête du lot c2
   (3px, gouttière serrée) sans emprunter ses classes. */
.l3cq-steps{display:flex;gap:4px;align-items:center;width:100%;margin-top:2px}
.l3cq-seg{flex:1;height:3px;border-radius:3px;background:rgba(255,255,255,.18)}
.l3cq-seg.is-done{background:linear-gradient(90deg,var(--orange),var(--yellow))}

/* Pied — participants à gauche, XP et cadeau/Q à droite. */
.l3cq-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;margin-top:auto}
.l3cq-pax{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;
  color:rgba(255,255,255,.7);min-width:0}
.l3cq-pax strong{font-size:13px;font-weight:800;color:#fff}
.l3cq-pax svg{width:13px;height:13px;opacity:.85;flex-shrink:0}
.l3cq-badges{display:inline-flex;align-items:center;gap:6px;flex-shrink:0}
/* La pilule XP du carrousel garde son CHIFFRE — c'est la diapositive héros, et
   « +50 » y est l'argument, pas la décoration — mais gagne le logo en tête et
   perd sa teinte d'accent, pour le même motif que les badges des cartes : une
   couleur de campagne derrière un logo coloré, cela fait deux couleurs pour un
   seul objet. 13px = l'échelle des icônes du carrousel (`.l3cq-badge svg`). */
.l3cq-xp{display:inline-flex;align-items:center;gap:5px;padding:3px 9px 3px 7px;border-radius:999px;
  font-size:10.5px;font-weight:800;line-height:1;
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.16);
  color:#fff}
.l3cq-xp::before{content:"";width:13px;height:13px;flex-shrink:0;
  background:url('assets/images/badges/xp.webp') center/contain no-repeat}
.l3cq-badge{display:inline-flex;align-items:center;justify-content:center;
  width:24px;height:24px;border-radius:50%;flex-shrink:0;
  background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);color:#fff}
.l3cq-badge svg{width:13px;height:13px}
/* Même geste que .qc-badge-gift : le logo livré sur le disque neutre, le tracé
   masqué. `.l3cq-q` n'existe plus — sans bon, la diapositive n'a pas de badge. */
.l3cq-gift{background-color:rgba(255,255,255,.06);
  background-image:url('assets/images/badges/cadeau.webp');
  background-position:center;background-repeat:no-repeat;background-size:13px;
  border-color:rgba(255,255,255,.16)}
.l3cq-gift svg{display:none}

/* Petit écran — le carrousel de l'accueil EST visible au doigt depuis le LOT 1
   (.dt-main est révélée ≤768px) : ces règles ne sont plus un filet de sécurité,
   elles servent le rendu réel. La hauteur de 220px appartient à l'étape 5. */
/* ⚠️ HAUTEUR CHOISIE SUR MESURE (étape 5), pas au jugé. À 220px la carte
   paraissait courte et creuse : 45px de mou sur une campagne sans description,
   tous réunis dans la gouttière au-dessus du pied. 320px donne un portrait de
   1:0,92 sur les 347px utiles d'un téléphone — la proportion des cartes de
   Layer3 — et le socle devenu élastique répartit ce gain AUTOUR de l'emblème
   au lieu de l'empiler en un trou. Relevé à 320px : mou résiduel de 2 à 4px,
   socle de 127 à 149px selon la garniture, aucun débordement.
   L'octogone monte à 96px et la gouttière entre blocs de 6 à 9px, pour que le
   socle et le texte respirent tous les deux. */
@media(max-width:768px){
  .l3cc-card{height:404px;grid-template-columns:1fr}
  .l3cc-left{padding:14px 16px 14px;gap:9px}
  .l3cc-title{font-size:16px}
  /* L'octogone grandit avec la carte : à 320px le socle élastique occupe 150 à
     175px, un octogone de 76px y rouvrirait le vide qu'on vient de fermer.
     ⚠️ Même arbitrage qu'en bureau depuis le piédestal dessiné : la boîte
     grandit (96 → 112) et la marge se resserre d'autant (28 → 10). Mesuré à
     320px, la largeur la plus contrainte : la plinthe fait 127px et l'emblème
     122, soit 5px de mou — plus qu'avant (3px). On gagne donc en lisibilité ET
     en marge, au lieu de choisir. */
  .l3cc-octo{width:179px;height:179px;margin-top:16px}
  /* Même rapport qu'en bureau : 64 sur un plateau de 94, soit 68 %. */
  .l3cc-coin{width:93px;height:93px;top:0}
  .l3cc-emblem-initial{font-size:32px}
  .l3cc-glow{width:298px;height:298px}
  /* Le spot suit le halo : même rapport, sa propre valeur (il est en px).
     ⚠️ HAUTEUR BORNÉE À 202 ET PAS 220. Mesuré à 320px, la largeur la plus
     contrainte : le plinthe est centré à 120,5px du haut de la carte, qui
     commence à 14 — au-delà de 213px de haut, le spot dépasse par le HAUT et
     la carte (overflow:hidden) le tranche. 202 laisse 5px de marge.
     Le halo d'accent, lui, déborde volontairement des trois côtés depuis
     toujours : c'est une lueur de fond, son rognage par la carte fait partie
     de son rendu. Le spot, qui doit se lire comme un faisceau net, non. */
  .l3cc-spot{width:244px;height:202px}
  .l3cc-sqlogo{width:46px;height:46px;border-radius:13px}
}

/* Skeleton de chargement — affiché pendant fetchCarouselData() */
.l3c-skeleton{
  height:336px;border-radius:20px;
  background:linear-gradient(90deg,var(--bg2) 25%,var(--bg3) 50%,var(--bg2) 75%);
  background-size:200% 100%;
  animation:l3cShimmer 1.6s ease-in-out infinite;
}
@keyframes l3cShimmer{
  0%  {background-position:200% 0}
  100%{background-position:-200% 0}
}
/* ⚠️ CETTE SURCHARGE DOIT RESTER APRÈS LA RÈGLE DE BASE, et non dans le bloc
   mobile plus haut. Les deux sélecteurs pèsent 0-1-0 — une @media n'ajoute
   aucune spécificité — donc c'est l'ORDRE SOURCE qui tranche. Placée avant, la
   surcharge était muette : le squelette se peignait à 336px puis les cartes le
   remplaçaient par 320px, soit 16px de saut à chaque chargement au doigt —
   exactement le défaut que ce commentaire prétendait empêcher. */
@media(max-width:768px){.l3c-skeleton{height:320px}}

.c-uber{background:linear-gradient(135deg,#000,#1c1c1c)}.c-migros{background:linear-gradient(135deg,#EA580C,#C2410C)}.c-shiroshi{background:linear-gradient(135deg,#4C1D95,#6D28D9)}.c-volta{background:linear-gradient(135deg,#065F46,#047857)}.c-kebab{background:linear-gradient(135deg,#92400E,#B45309)}.c-hybride{background:linear-gradient(135deg,#065F46,#0D9488)}.c-vtc{background:linear-gradient(135deg,#1E3A5F,#2563EB)}

.ls-list{display:flex;flex-direction:column;gap:2px}
/* Séparateur de ligne en `--border2` (0,12 d'alpha en sombre) et non `--border`
   (0,07) : à 0,07 sur #13131C, le trait est sous le seuil de perception et les
   quêtes se lisent comme un bloc continu. Les deux valeurs sont des jetons du
   :root — on monte d'un cran, on n'invente pas une couleur. */
.ls-item{display:flex;align-items:center;gap:14px;padding:14px 0;border-bottom:1px solid var(--border2);cursor:pointer;transition:opacity .2s}
.ls-item:last-child{border-bottom:none}
.ls-item:hover{opacity:.75}
.ls-logo{width:52px;height:52px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
/* Lot B · B1 — logo d'item liste rempli dynamiquement (quest-lists.js).
   L'URL arrive par la propriété personnalisée --ls-logo, posée en JS : seule la
   donnée est en ligne, la mise en forme reste ici (cf. le motif --qt-* du §9). */
.ls-logo-img{background-image:var(--ls-logo);background-size:cover;background-position:center;background-color:var(--bg3)}
.ls-logo-fallback{background:linear-gradient(135deg,var(--orange),#0F172A)}
.ls-logo-letter{color:#fff;font-weight:900;font-size:18px}
.ls-info{flex:1;min-width:0}
.ls-partner{font-size:11px;color:var(--text3);font-weight:600;margin-bottom:3px}
.ls-name{font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px}
/* Ligne du bas d'une ligne de liste : bande d'avatars PUIS compteur (Layer3).
   `flex-wrap` est un filet, pas une mise en page : à 360 px la ligne du
   compteur n'offre qu'une soixantaine de pixels libres une fois « N
   Participants » posé. Plutôt que de rogner les avatars jusqu'à l'illisible
   pour tenir coûte que coûte, on laisse la bande passer dessous quand elle ne
   rentre pas — jamais de débordement, jamais de texte tronqué. */
.ls-participants{font-size:12px;color:var(--text3);display:flex;align-items:center;gap:8px;flex-wrap:wrap;row-gap:4px}
.ls-pax-count{white-space:nowrap}

/* Avatars GÉNÉRIQUES empilés — posés par quest-lists.js (_ql_buildAvatars).
   Ce ne sont PAS des joueurs : une silhouette neutre, déclinée en cinq teintes,
   qui illustre « ils sont N ». Les vrais avatars ne peuvent pas être montrés,
   leur URL contenant l'uid du joueur (cf. le commentaire de _ql_buildAvatars).

   Entièrement dessinées : aucun fichier, aucune requête. La silhouette est une
   seule data-URI SVG déclarée ici et réutilisée par les cinq variantes ; seul
   le dégradé change. Un `<span>` vide suffit donc côté markup.

   L'anneau `border: 2px solid var(--bg)` est ce qui détache chaque pastille de
   la précédente malgré le chevauchement : sans lui, les rondes se fondent en
   une tache. box-sizing:border-box pour que la taille annoncée soit la taille
   occupée. */
.qw-avatars{display:inline-flex;align-items:center;flex-shrink:0;
  --qw-av-glyphe:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='9' r='3.7' fill='%23fff' fill-opacity='.66'/%3E%3Cpath d='M4.6 21.6c0-3.9 3.3-6.7 7.4-6.7s7.4 2.8 7.4 6.7z' fill='%23fff' fill-opacity='.66'/%3E%3C/svg%3E")}
.qw-av{width:22px;height:22px;border-radius:50%;display:block;box-sizing:border-box;
  border:2px solid var(--bg);background-repeat:no-repeat;
  background-size:100% 100%, cover;background-position:center}
.qw-av + .qw-av{margin-left:-7px}
/* Cinq teintes sourdes : la grappe doit vivre sans concurrencer les badges
   XP/cadeau à droite de la ligne, ni se faire prendre pour un accent de marque. */
.qw-av--1{background-image:var(--qw-av-glyphe),linear-gradient(150deg,#6E66B8,#3E3A6B)}
.qw-av--2{background-image:var(--qw-av-glyphe),linear-gradient(150deg,#C06A45,#78392A)}
.qw-av--3{background-image:var(--qw-av-glyphe),linear-gradient(150deg,#3F8E7E,#245049)}
.qw-av--4{background-image:var(--qw-av-glyphe),linear-gradient(150deg,#5B7FB0,#2F4468)}
.qw-av--5{background-image:var(--qw-av-glyphe),linear-gradient(150deg,#B08A46,#6B522A)}
.qw-av-more{margin-left:-7px;min-width:22px;height:22px;padding:0 4px;
  border-radius:11px;border:2px solid var(--bg);box-sizing:border-box;
  background:var(--bg3);color:var(--text2);font-size:9px;font-weight:800;
  line-height:1;display:inline-flex;align-items:center;justify-content:center}

@media (max-width: 430px) {
  .ls-participants{gap:6px}
  .qw-av,.qw-av-more{width:18px;height:18px;min-width:18px;border-width:1.5px}
  .qw-av-more{padding:0 3px;border-radius:9px;font-size:8px}
  .qw-av + .qw-av,.qw-av-more{margin-left:-6px}
}
/* P3 — barre de progression SEGMENTÉE des cartes de liste (1 segment = 1 étape) */
/* ── Groupe de badges à DROITE des lignes « Nouveau » / « Tendances » ────────
   Le markup est celui de UI_buildQuestCard, repris tel quel (.quest-rewards-icons
   + .qc-badge…), et son habillage vient donc déjà de style.css:4721+. Deux
   ajustements sont néanmoins nécessaires ICI, et seulement ici :

   ⚠️ 1. --qc-accent n'est déclarée que sur .ob-quest-card (style.css:4575). Hors
      d'une carte, les trois `color-mix(in srgb, var(--qc-accent) …)` de
      .qc-badge-xp deviennent INVALIDES et sont abandonnés : le badge XP
      retombait sur le gris neutre de .qc-badge, sans son orange. On redéclare
      donc la variable sur la ligne — on ne touche pas au composant partagé.

   2. La ligne est un flex à 3 zones (logo · infos · badges). .ls-info porte
      déjà flex:1 min-width:0, et .quest-rewards-icons son margin-left:auto :
      le groupe se cale à droite sans une règle de plus. On lui laisse juste de
      l'air à gauche pour qu'il ne colle pas au titre ellipsé. */
.ls-item{--qc-accent:var(--orange,#FF6B2B)}
.ls-item .quest-rewards-icons{margin-left:12px}

/* ⚠️ CES TROIS CLASSES NE SONT PLUS RENDUES DANS .ls-item (la barre « ✓ Terminé »
   a quitté ces lignes), mais elles RESTENT : quete.html:1121 les réutilise pour
   la carte « Prochaine quête » (.qt-next-bar), alimentée par
   questionnaire.js:1372. Les supprimer casserait cet écran. */
.ls-progress{display:flex;align-items:center;gap:4px;margin-top:6px}
.ls-seg{flex:1;height:5px;border-radius:3px;background:var(--bg3)}
.ls-seg-done{background:var(--orange)}
.ls-seg-done-completed{background:var(--green)}
.ls-progress-label{font-size:10px;font-weight:800;color:var(--green);white-space:nowrap;flex-shrink:0;margin-left:2px}
/* B4a-1 — carte « Next up » : la barre P3 (.ls-progress) respire sous les participants.
   Scopée .qt-next-card pour n'affecter aucune autre carte de flow. Réutilise tokens/classes. */
.qt-next-card .ls-progress{margin-top:12px}

/* ── B4a-2 — enrichissement visuel « Next up » (cosmétique, HORS §9). Réutilise .qt-flow-card ;
     ne touche AUCUN token --qt partagé (couleur cadeau = variable LOCALE --qt-next-gift). ── */
.qt-next-card{padding:0;--qt-next-gift:var(--yellow)}   /* .qt-flow-card a déjà overflow:hidden */
.qt-next-cover{aspect-ratio:16/10;background-size:cover;background-position:center;background-color:var(--qt-surface-soft);display:flex;align-items:center;justify-content:center}
.qt-next-cover-fallback{background:linear-gradient(135deg,var(--qt-accent),var(--qt-surface))}
.qt-next-card .qt-next-cover-logo{color:var(--qt-text);font-weight:900;font-size:1.6rem;text-transform:none;letter-spacing:0}
.qt-next-cover-logo-img{width:48px;height:48px;object-fit:contain;border-radius:10px}
.qt-next-body{padding:clamp(0.8rem,1.8vw,1rem) clamp(0.9rem,2vw,1.15rem)}
.qt-next-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}
.qt-next-card .qt-next-pax{display:inline-flex;align-items:center;color:var(--qt-text-muted);font-size:0.82rem;font-weight:700;text-transform:none;letter-spacing:0}
.qt-next-card .qt-next-pax.qt-next-pax-new{color:var(--qt-accent);background:color-mix(in srgb,var(--qt-accent) 16%,transparent);padding:2px 9px;border-radius:var(--qt-radius-pill);font-size:0.72rem;font-weight:800;text-transform:uppercase;letter-spacing:0.04em}
.qt-next-rewards{display:flex;align-items:center;gap:8px;flex-shrink:0}
.qt-next-card .qt-next-reward{display:inline-flex;align-items:center;gap:5px;text-transform:none;letter-spacing:0}
.qt-next-reward-ico{width:22px;height:22px;object-fit:contain;flex-shrink:0}
.qt-next-card .qt-next-reward-xp span{color:var(--qt-accent);font-weight:900;font-size:0.9rem}
.qt-next-card .qt-next-reward-gift{max-width:170px;padding:3px 10px 3px 4px;border-radius:var(--qt-radius-pill);background:color-mix(in srgb,var(--qt-next-gift) 15%,transparent);border:1px solid color-mix(in srgb,var(--qt-next-gift) 42%,transparent)}
.qt-next-card .qt-next-reward-gift .qt-next-reward-txt{max-width:120px;color:var(--qt-next-gift);font-weight:800;font-size:0.78rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* 14. MOBILE BOTTOM NAV */
.bottom-nav{position:fixed;bottom:0;left:0;right:0;z-index:50;height:58px;background:var(--bg);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-around;padding:0 4px}
.bnav-btn{display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px;border-radius:8px;border:none;background:none;cursor:pointer;color:var(--text3);transition:all .2s;flex:1}
.bnav-btn.active{color:var(--orange)}
.bnav-btn svg{width:19px;height:19px;stroke:currentColor}
.bnav-label{font-size:9px;font-weight:700;letter-spacing:.5px}

/* 15. DOTS (commun) */
.dt-dots-row{display:flex;justify-content:center;gap:6px;margin-top:12px}

/* ============================================================
   16. DESKTOP LAYOUT ≥1024px
   Sidebar + Main UNIQUEMENT si #app a la classe .logged-in-layout
   La landing (non connectée) garde sa structure centrée max-width:1200px
   ============================================================ */
.dt-sidebar{display:none}
.dt-main{display:none}

/* Icône de recherche : taille FIXE à toutes les largeurs (sinon le <svg> sans width/height
   intrinsèque explose sous 1024px, où le stylage topbar est gated ≥1024). */
.dt-search svg{width:15px;height:15px;flex-shrink:0;stroke:var(--text3)}

@media(min-width:1024px){

  /* ────────────────────────────────────────────────
     LANDING DESKTOP — centrée, sans sidebar
     ──────────────────────────────────────────────── */
  nav#landingNav{padding:0 32px}
  /* Les cinq règles #landingHero qui vivaient ici ont été RETIRÉES (vitrine
     étape 1). Elles habillaient l'ancien hero, et deux d'entre elles battaient
     silencieusement le nouveau : un sélecteur d'ID (1,0,0) l'emporte sur une
     classe (0,1,0) quel que soit l'ordre du fichier.
       · #landingHero{padding:120px 48px 80px} écrasait .vt-hero — mesuré :
         padding calculé « 120px 48px 80px » au lieu du « 168px 40px 0 » écrit ;
       · #landingHero p{max-width:560px} écrasait le max-width:33ch de .vt-lede.
     Les trois autres (h1, .hero-cta, .btn-primary/.btn-secondary) visaient un
     markup qui n'existe plus. #landingHero est porté par index.html SEULEMENT
     (vérifié : aucune autre page) — contrairement à nav#landingNav juste
     au-dessus, que partenaires.html et inscription-commerce.html partagent.
     La ligne 2311 (masquage du membre connecté) garde l'id et reste en place. */

  /* Toutes les sections landing centrées avec max-width */
  section{padding:96px 48px;max-width:1200px;margin:0 auto}.quests-main {padding-left:48px;padding-right:48px}

  /* Types de quêtes — 4 cartes par ligne + 1 carte large dessous */

  /* Streak card plus large sur desktop */

  footer#landingFooter{padding:48px}

  /* ────────────────────────────────────────────────
     APP CONNECTÉ — Sidebar + Main 2 colonnes
     S'active UNIQUEMENT quand #app a .logged-in-layout
     ──────────────────────────────────────────────── */

  /* Masquer mobile uniquement dans l'app connecté */
  .logged-in-layout .app-nav,
  .logged-in-layout .bottom-nav,
  .logged-in-layout .side-overlay,
  .logged-in-layout .side-menu,
  .logged-in-layout .right-panel{display:none!important}

  /* Le conteneur app perd ses contraintes */
  .logged-in-layout{display:block!important;padding-bottom:0;max-width:none;margin:0}

  /* Sidebar visible uniquement dans le mode connecté */
  .logged-in-layout .dt-sidebar{
    display:flex!important;flex-direction:column;
    position:fixed;top:0;left:0;bottom:0;
    width:240px;max-width:240px;
    box-sizing:border-box;overflow:hidden;
    background:var(--bg);border-right:none !important;z-index:200
  }

  .dt-logo-area{
    padding:15px 14px 11px;border-bottom:1px solid var(--border);
    flex-shrink:0;width:100%;box-sizing:border-box
  }
  .dt-logo-link{display:inline-flex;gap:2px;align-items:center;text-decoration:none;cursor:pointer}
  .dt-logo-link .anl{width:24px;height:24px;font-size:14px;border-radius:5px}

  .dt-nav{
    flex:1;padding:10px 10px;
    display:flex;flex-direction:column;gap:2px;
    overflow:hidden;width:100%;box-sizing:border-box
  }
  .dt-nav-sep{font-size:10px;font-weight:700;letter-spacing:1.5px;color:var(--text3);text-transform:uppercase;padding:14px 10px 6px;white-space:nowrap;overflow:hidden}
  .dt-nav-item{
    display:flex;align-items:center;gap:10px;
    padding:9px 12px;border-radius:10px;
    font-size:var(--menu-text-size);font-weight:500;line-height:1.5;color:var(--text2);
    text-decoration:none;cursor:pointer;
    transition:color .3s ease,background-color .3s ease;
    width:100%;box-sizing:border-box;
    overflow:hidden;white-space:nowrap;text-overflow:ellipsis
  }
  .dt-nav-item svg{width:17px;height:17px;flex-shrink:0;min-width:17px;stroke:currentColor}
  .dt-nav-item span{overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1}
  .dt-nav-item:hover{background:var(--bg3);color:var(--text)}
  /* État ACTIF — page courante (classe posée par renderSidebar via window.location).
     Dark mode / glassmorphism : fond translucide, texte pur, fin accent gauche. */
  .dt-nav-item.active{background:rgba(255,255,255,.08);color:#fff;font-weight:600;box-shadow:inset 3px 0 0 0 var(--orange)}
  /* Thème CLAIR : la règle ci-dessus écrit du blanc en dur sur un fond blanc translucide,
     sur le fond clair du rail (--bg) → ~1,1:1, texte ET icône (stroke:currentColor)
     invisibles. On repasse aux jetons du thème, comme le survol ; la barre orange reste.
     Le thème sombre n'est pas concerné : ce sélecteur ne s'y applique jamais. */
  [data-theme="light"] .dt-nav-item.active{background:var(--bg3);color:var(--text)}
  .dt-nav-badge{margin-left:auto;flex-shrink:0;background:var(--orange);color:#fff;font-size:10px;font-weight:800;font-style:normal;padding:1px 7px;border-radius:10px}
  /* Badge discret « Bientôt » — zone en construction (Painted Door) */
  .dt-nav-badge-soon{margin-left:auto;flex-shrink:0;background:rgba(124,58,237,.18);border:1px solid rgba(124,58,237,.35);color:#c4b5fd;font-size:9px;font-weight:700;font-style:normal;letter-spacing:.4px;text-transform:uppercase;padding:1px 7px;border-radius:999px}
  /* Thème CLAIR : #c4b5fd sur la pastille violette → ~1,3:1. Même fond, même bordure ;
     texte violet assombri par le jeton --text (--violet seul plafonne à ~4:1). */
  [data-theme="light"] .dt-nav-badge-soon{color:color-mix(in srgb, var(--violet) 75%, var(--text))}

  .dt-user-row{padding:12px 16px;border-top:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0;width:100%;box-sizing:border-box;overflow:hidden}
  .dt-user-av{width:34px;height:34px;border-radius:50%;flex-shrink:0;background:linear-gradient(135deg,var(--orange),var(--violet));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px;color:#fff}
  .dt-user-info{flex:1;min-width:0;overflow:hidden}
  .dt-user-name{font-size:13px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .dt-user-lvl{font-size:11px;color:var(--text2);white-space:nowrap}
  .dt-logout-btn{margin-left:auto;flex-shrink:0;background:none;border:none;color:var(--text3);cursor:pointer;padding:4px;transition:color .2s}
  .dt-logout-btn:hover{color:var(--red)}
  .dt-logout-btn svg{stroke:currentColor}

  .dt-sidebar-foot{padding:10px 20px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px;flex-shrink:0;width:100%;box-sizing:border-box;overflow:hidden}
  .dt-foot-link{font-size:11px;color:var(--text3);text-decoration:none;white-space:nowrap;transition:color .2s}
  .dt-foot-link:hover{color:var(--text2)}

  /* ── MAIN (uniquement si connecté) ── */
  .logged-in-layout .dt-main{display:flex!important;flex-direction:column;margin-left:240px;min-height:100vh}

  .dt-topbar{display:flex;align-items:center;height:60px;gap:16px;padding:0 24px;background:var(--bg);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;box-sizing:border-box;width:100%}
  .dt-search{display:flex;align-items:center;gap:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:10px;padding:8px 14px;flex:1;max-width:420px;min-width:0;transition:border-color .2s;box-sizing:border-box}
  .dt-search:focus-within{border-color:var(--orange)}
  .dt-search input{flex:1;min-width:0;background:none;border:none;outline:none;color:var(--text);font-size:14px}
  .dt-search input::placeholder{color:var(--text3)}
  .dt-topbar-right{display:flex;align-items:center;gap:12px;flex-shrink:0}
  .dt-league-pill{display:flex;align-items:center;gap:6px;background:linear-gradient(135deg,rgba(124,58,237,.15),rgba(124,58,237,.08));border:1px solid rgba(124,58,237,.25);border-radius:20px;padding:5px 12px;font-size:12px;font-weight:700;color:var(--violet2)}
  .dt-league-dot{width:7px;height:7px;border-radius:50%;background:var(--violet);flex-shrink:0}
  .dt-avatar-pill{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--orange),var(--violet));display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px;color:#fff;cursor:pointer;border:2px solid var(--border2);position:relative;flex-shrink:0}
  .dt-theme-btn{width:44px;height:24px;background:var(--bg3);border:1px solid var(--border2);border-radius:12px;cursor:pointer;position:relative;flex-shrink:0}
  .dt-theme-btn::after{content:'';position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--text2);transition:transform .3s}
  [data-theme="light"] .dt-theme-btn::after{transform:translateX(20px);background:var(--orange)}

  /* ── BODY — colonne droite fixe 280px = dt-topbar-right, carousel prend le reste ── */
  /* !important : surcharge les inline styles "3fr 1fr" présents dans le HTML       */
  /* max-width:2400px : illimité en pratique, garde l'animation smooth vers 1100px   */
  /* margin:0 : bord gauche aligné sur le padding 24px de la topbar                 */
  .dt-body{display:grid;grid-template-columns:1fr 0fr !important;gap:0px;padding:14px 16px;align-items:start;max-width:2400px;margin:0;width:100%;box-sizing:border-box}
  .dt-right{display:none;overflow:hidden}

  .dt-center{display:flex;flex-direction:column;gap:12px;min-width:0;--content-gutter:32px}

  /* Carrousel dans dt-center */
  .dt-label{font-size:var(--text-xs);font-weight:700;letter-spacing:2px;color:var(--text3);text-transform:uppercase;margin-bottom:12px}
  .dt-carousel{
    position:relative;border-radius:20px;overflow:visible;
    border:1px solid var(--bg3);
    box-shadow:0 0 0 1px rgba(255,255,255,.04), 0 4px 24px rgba(0,0,0,.35);
  }
  .dt-carousel .ac-slide{display:none}
  .dt-carousel .ac-slide.active{display:block;animation:acFadeIn .4s var(--ease)}
  .dt-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:10;width:40px;height:40px;border-radius:50%;background:rgba(14,14,20,.7);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.15);display:flex;align-items:center;justify-content:center;cursor:pointer;color:#fff;font-size:20px;opacity:0;transition:opacity .25s,background .2s;line-height:1}
  .dt-arrow-l{left:14px}.dt-arrow-r{right:14px}
  .dt-arrow:hover{background:rgba(255,255,255,.18)}
  .dt-carousel:hover .dt-arrow{opacity:1}

  /* Filtres */
  .dt-filters{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
  .dt-fb{padding:7px 15px;border-radius:20px;border:1px solid var(--border2);background:transparent;color:var(--text2);font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
  .dt-fb:hover{background:var(--bg3);color:var(--text)}
  .dt-fb.active{background:var(--orange);border-color:var(--orange);color:#fff}

  /* Bloc (titre + contenu) */
  .dt-block{display:flex;flex-direction:column;gap:8px}
  .dt-block-hd{display:flex;align-items:center;justify-content:space-between}
  .dt-block-title{font-size:var(--text-md);font-weight:800;letter-spacing:-.3px}
  .dt-seeall{font-size:13px;font-weight:600;color:var(--text3);background:none;border:none;cursor:pointer;transition:color .2s}
  .dt-seeall:hover{color:var(--orange)}

  /* Grille 3 colonnes */
  .dt-grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
  .dt-qcard{background:var(--bg2);border:1px solid var(--border);border-radius:16px;overflow:hidden;cursor:pointer;transition:all .22s}
  .dt-qcard:hover{border-color:var(--border2);transform:translateY(-3px);box-shadow:0 12px 32px rgba(0,0,0,.15)}
  .dt-qcard-top{height:120px;display:flex;align-items:center;justify-content:center;font-size:28px;position:relative}
  .dt-qcard-spon{position:absolute;top:8px;left:8px;background:var(--yellow);color:#000;font-size:9px;font-weight:800;letter-spacing:1px;padding:2px 7px;border-radius:4px;text-transform:uppercase}
  .dt-qcard-body{padding:12px 14px 14px;display:flex;flex-direction:column;gap:4px}
  .dt-qcard-partner{font-size:10px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.5px}
  .dt-qcard-name{font-size:14px;font-weight:800;letter-spacing:-.2px}
  .dt-qcard-foot{display:flex;align-items:center;gap:8px;margin-top:8px;padding-top:10px;border-top:1px solid var(--border)}
  .dt-avs{display:flex}
  .dt-av{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:800;color:#fff;border:2px solid var(--bg2);margin-left:-6px;flex-shrink:0}
  .dt-av:first-child{margin-left:0}
  .dt-qcard-pts{font-size:11px;font-weight:600;color:var(--text2);flex:1;min-width:0}
  .dt-qcard-rew{flex-shrink:0;display:inline-block;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);color:var(--green);font-size:11px;font-weight:700;padding:2px 8px;border-radius:5px}

  /* Deux listes côte à côte */
  .dt-two-lists{display:grid;grid-template-columns:1fr 1fr;gap:20px}
  .dt-list-col{background:var(--bg2);border:1px solid var(--border);border-radius:16px;padding:18px;display:flex;flex-direction:column;gap:12px}
  .dt-list-col .ls-item{padding:8px 0;border-bottom:1px solid var(--border2)}
  .dt-list-col .ls-item:last-child{border-bottom:none}

  /* ── BARRE DROITE 25% ── */
  .dt-right{position:sticky;top:80px}
  .dt-activity-card{background:var(--bg2);border:1px solid var(--border);border-radius:18px;padding:20px;display:flex;flex-direction:column;gap:12px}
  .dt-act-title{font-size:14px;font-weight:800;margin-bottom:4px}
  .dt-act-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border);font-size:13px}
  .dt-act-row:last-of-type{border-bottom:none}
  .dt-act-label{color:var(--text2)}.dt-act-val{font-weight:700}
  .dt-gm-btn{width:100%;padding:11px 20px;background:linear-gradient(135deg,var(--yellow),var(--orange));border:none;border-radius:var(--radius-md);color:#fff;font-size:var(--text-sm);font-weight:800;cursor:pointer;transition:all .2s;margin-top:4px}
  .dt-gm-btn:hover{filter:brightness(1.1);transform:scale(1.02)}
  .dt-gm-btn.claimed{background:var(--bg3);color:var(--text3);cursor:default;transform:none;filter:none}
}

/* ============================================================
   17. CLASSES .cmp-* — utilisées par le tunnel de quête (quete.html, questionnaire.js)
   Écrites à l'origine pour campagne-uber.html, page supprimée à l'étape F.
   Le bloc RESTE : les classes servent toujours.
   ============================================================ */

/* Bouton retour */
.cmp-back{
  display:inline-flex;align-items:center;gap:6px;
  font-size:13px;font-weight:600;color:var(--text2);
  text-decoration:none;
  padding:6px 12px;border-radius:8px;
  border:1px solid var(--border2);background:var(--bg2);
  transition:all .2s;align-self:flex-start;

}
.cmp-back:hover{color:var(--text);border-color:var(--text3)}

/* Grand bandeau titre */
.cmp-hero{
  position:relative;
  background:linear-gradient(135deg,#000 0%,#1a1a1a 50%,#2a2a2a 100%);
  border-radius:24px;
  overflow:hidden;
  padding:40px 32px 36px;
  min-height:200px;
  display:flex;align-items:flex-end;
}
.cmp-hero-overlay{
  position:absolute;inset:0;
  background:radial-gradient(ellipse at top right,rgba(255,107,43,0.15) 0%,transparent 60%);
  pointer-events:none;
}
.cmp-sponsored{
  position:absolute;top:18px;left:24px;
  background:var(--yellow);color:#000;
  font-size:10px;font-weight:800;letter-spacing:1px;
  padding:4px 10px;border-radius:5px;
  text-transform:uppercase;z-index:2;
}
.cmp-hero-content{position:relative;z-index:1;width:100%}
.cmp-partner-name{
  font-size:12px;font-weight:700;letter-spacing:2px;
  color:rgba(255,255,255,0.6);text-transform:uppercase;
  margin-bottom:10px;
}
.cmp-title{
  font-size:var(--text-xl);font-weight:900;
  letter-spacing:-1px;line-height:1.1;color:#fff;
  margin-bottom:10px;max-width:640px;
}
.cmp-subtitle{
  font-size:var(--text-base);color:rgba(255,255,255,0.7);
  line-height:1.5;max-width:520px;
}

/* Deux blocs infos côte à côte */
.cmp-info-row{
  display:grid;grid-template-columns:1fr 1fr;gap:14px;
}
@media(max-width:640px){.cmp-info-row{grid-template-columns:1fr}}
.cmp-info-card{
  background:var(--bg2);border:1px solid var(--border);
  border-radius:16px;padding:18px;
  display:flex;align-items:center;gap:14px;
  position:relative;
}
.cmp-info-icon{
  width:42px;height:42px;border-radius:11px;
  display:flex;align-items:center;justify-content:center;
  flex-shrink:0;
}
.cmp-info-content{flex:1;min-width:0}
.cmp-info-label{
  font-size:var(--text-xs);font-weight:700;letter-spacing:1.5px;
  color:var(--text3);text-transform:uppercase;margin-bottom:4px;
}
.cmp-info-main{
  font-size:15px;font-weight:800;letter-spacing:-.2px;
  margin-bottom:2px;
}
.cmp-info-sub{font-size:12px;color:var(--text2)}

/* Badge Live brillant */
.cmp-live-badge{
  display:flex;align-items:center;gap:5px;
  background:rgba(34,197,94,0.12);
  border:1px solid rgba(34,197,94,0.3);
  border-radius:20px;padding:4px 10px;
  font-size:11px;font-weight:800;color:var(--green);
  white-space:nowrap;flex-shrink:0;
  box-shadow:0 0 12px rgba(34,197,94,0.25);
}
.cmp-live-dot{
  width:7px;height:7px;border-radius:50%;
  background:var(--green);
  box-shadow:0 0 8px var(--green);
  animation:blink 1.5s infinite;
}

/* Avatars participants */
.cmp-avatars{display:flex;flex-shrink:0}
.cmp-av{
  width:26px;height:26px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:10px;font-weight:800;color:#fff;
  border:2px solid var(--bg2);
  margin-left:-7px;flex-shrink:0;
}
.cmp-av:first-child{margin-left:0}

/* Carte de progression globale */
.cmp-progress-card{
  background:linear-gradient(135deg,rgba(255,107,43,0.06),rgba(245,158,11,0.04));
  border:1px solid rgba(255,107,43,0.15);
  border-radius:16px;padding:18px 20px;
}
.cmp-progress-top{
  display:flex;align-items:center;justify-content:space-between;
  margin-bottom:12px;
}
.cmp-progress-label{
  font-size:11px;font-weight:700;letter-spacing:1.5px;
  color:var(--text3);text-transform:uppercase;
}
.cmp-progress-text{
  font-size:15px;font-weight:800;margin-top:3px;
}
.cmp-progress-pct{
  font-size:28px;font-weight:900;
  background:linear-gradient(135deg,var(--orange),var(--yellow));
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;
  background-clip:text;letter-spacing:-1px;
}
.cmp-progress-bar{
  height:8px;background:rgba(255,255,255,0.06);
  border-radius:4px;overflow:hidden;
}
[data-theme="light"] .cmp-progress-bar{background:var(--bg3)}
.cmp-progress-fill{
  height:100%;border-radius:4px;
  background:linear-gradient(90deg,var(--orange),var(--yellow));
  transition:width .8s var(--ease);
}

/* Grille des quêtes */
.cmp-quests{display:flex;flex-direction:column;gap:10px}
.cmp-quest{
  background:var(--bg2);border:1px solid var(--border);
  border-radius:14px;padding:16px;
  display:flex;align-items:center;gap:14px;
  transition:all .2s;
}
.cmp-quest-done{opacity:0.7}
.cmp-quest-active{
  border-color:rgba(255,107,43,0.3);
  background:linear-gradient(135deg,rgba(255,107,43,0.04),rgba(255,107,43,0.01));
  cursor:pointer;
}
.cmp-quest-active:hover{
  border-color:var(--orange);
  transform:translateX(4px);
  box-shadow:0 8px 24px rgba(255,107,43,0.12);
}
.cmp-quest-locked{opacity:0.5}
.cmp-quest-icon{
  width:44px;height:44px;border-radius:11px;
  display:flex;align-items:center;justify-content:center;
  flex-shrink:0;
}
.cmp-quest-body{flex:1;min-width:0}
.cmp-quest-num{
  font-size:var(--text-xs);font-weight:700;letter-spacing:1px;
  color:var(--text3);text-transform:uppercase;margin-bottom:4px;
}
.cmp-quest-name{
  font-size:15px;font-weight:800;letter-spacing:-.2px;
  margin-bottom:4px;
}
.cmp-quest-desc{
  font-size:12px;color:var(--text2);line-height:1.5;
}
.cmp-quest-pts{
  font-size:12px;font-weight:700;color:var(--green);
  flex-shrink:0;
  background:rgba(34,197,94,0.08);
  border:1px solid rgba(34,197,94,0.2);
  border-radius:6px;padding:4px 10px;
}
.cmp-quest-pts-active{
  color:var(--orange);
  background:rgba(255,107,43,0.08);
  border-color:rgba(255,107,43,0.25);
}
.cmp-quest-pts-locked{
  font-size:18px;background:transparent;border:none;color:var(--text3);
}

/* ============================================================
   18. HEADER & SIDEBAR — Layer3 style update
   ============================================================ */

/* ── Topbar séparateurs verticaux ── */
.dt-topbar-sep {
  width: 1px; height: 22px;
  background: var(--border2);
  flex-shrink: 0;
}

/* ── Avatar avec dropdown ── */
.dt-avatar-wrap {
  position: relative;
  flex-shrink: 0;
}
.dt-avatar-pill {
  width: 34px; height: 34px; border-radius: 50%;
  background: linear-gradient(135deg, var(--orange), var(--violet));
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 13px; color: #fff;
  cursor: pointer; border: 2px solid var(--border2);
  position: relative; transition: border-color .2s;
  user-select: none;
}
.dt-avatar-pill:hover { border-color: var(--orange); }
.dt-avatar-pill .lvl-badge {
  position: absolute; bottom: -4px; right: -4px;
  background: var(--orange); color: #fff;
  font-size: 9px; font-weight: 800;
  border-radius: 5px; padding: 1px 4px;
  border: 1px solid var(--bg2);
}

/* ── Dropdown menu ── */
.dt-dropdown {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  /* 200 → 216px. « Mon tableau de bord » demandait 139px de texte pour 138
     disponibles : il se repliait sur DEUX lignes (61px de haut contre 39 pour
     ses voisines) — mesuré, à un pixel près. Élargir plutôt que rogner l'espace
     icône/texte : un pixel de marge se reperd au premier changement de fonte.
     Le déroulant est en `position:absolute`, l'élargir ne déplace rien ; et la
     variante `--large` est déjà à 220px, les deux se rejoignent presque. */
  min-width: 216px;
  background: var(--bg2);
  border: 1px solid var(--border2);
  border-radius: 14px;
  box-shadow: 0 16px 48px rgba(0,0,0,0.35);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-8px) scale(0.97);
  transition: opacity .2s var(--ease), transform .2s var(--ease);
  z-index: 999;
}
/* Variante des pages riches (index, campagne). Ces deux écarts — 20 px de
   large en plus, ombre un peu plus dense — vivaient dans un style="" du
   markup ; les voici en classe, au pixel près, conformément à §2. */
.dt-dropdown--large { min-width: 220px; box-shadow: 0 16px 48px rgba(0,0,0,0.4); }
[data-theme="light"] .dt-dropdown { box-shadow: 0 16px 48px rgba(0,0,0,0.12); }
.dt-dropdown.open {
  opacity: 1;
  pointer-events: all;
  transform: translateY(0) scale(1);
}

/* Dropdown user info header */
.dt-dd-header {
  padding: 14px 16px 10px;
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px;
}
.dt-dd-av {
  width: 36px; height: 36px; border-radius: 50%;
  background: linear-gradient(135deg, var(--orange), var(--violet));
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 14px; color: #fff; flex-shrink: 0;
}
.dt-dd-name { font-size: 14px; font-weight: 700; }
.dt-dd-lvl  { font-size: 11px; color: var(--text2); margin-top: 1px; }

/* Dropdown items */
.dt-dd-items { padding: 6px; }
.dt-dd-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: 9px;
  font-size: 14px; font-weight: 600; color: var(--text2);
  cursor: pointer; transition: background .15s, color .15s;
  text-decoration: none; font-family: 'Inter', sans-serif;
  border: none; background: none; width: 100%; text-align: left;
}
.dt-dd-item:hover { background: var(--bg3); color: var(--text); }
.dt-dd-item-icon { font-size: 16px; flex-shrink: 0; line-height: 1; }
.dt-dd-sep {
  height: 1px; background: var(--border);
  margin: 4px 6px;
}
.dt-dd-item-danger { color: var(--red) !important; }
.dt-dd-item-danger:hover { background: rgba(239,68,68,0.08) !important; }

/* ── Badge ligue Layer3 ── */
.dt-league-pill {
  display: flex; align-items: center; gap: 6px;
  background: linear-gradient(135deg, rgba(245,158,11,.12), rgba(245,158,11,.06));
  border: 1px solid rgba(245,158,11,.25);
  border-radius: 20px; padding: 5px 12px;
  font-size: 12px; font-weight: 700; color: var(--yellow);
  cursor: default; white-space: nowrap;
}
.dt-league-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--yellow); flex-shrink: 0; }
/* Le bronze des pages riches, jusqu'ici écrit en style="" dans le markup. */
.dt-league-dot--bronze { background: #CD7F32; }
/* Reprend au pixel près le style="" que portait le mot-symbole de la topbar. */
.dt-header-logo img { height: 24px; width: auto; object-fit: contain; }

/* ── Sidebar nouveaux liens ── */
.dt-nav-item-new::after {
  content: 'NEW';
  font-size: 9px; font-weight: 800; letter-spacing: .5px;
  background: var(--orange); color: #fff;
  border-radius: 4px; padding: 1px 5px;
  margin-left: auto;
}

/* ============================================================
   19. PAGE PROFIL — profil.html
   ============================================================ */

/* ── 1. En-tête profil ── */
.prf-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 28px;
}

.prf-avatar-zone {
  display: flex;
  align-items: center;
  gap: 20px;
  flex: 1;
  min-width: 0;
}

.prf-avatar-wrap { position: relative; flex-shrink: 0; }
.prf-avatar {
  width: 80px; height: 80px; border-radius: 50%;
  background: linear-gradient(135deg, var(--orange), var(--violet));
  display: flex; align-items: center; justify-content: center;
  font-weight: 900; font-size: 30px; color: #fff;
  border: 3px solid var(--border2);
  box-shadow: 0 0 0 4px rgba(255,107,43,0.15);
}
.prf-lvl-badge {
  position: absolute; bottom: -2px; right: -6px;
  background: var(--orange); color: #fff;
  font-size: 11px; font-weight: 800;
  border-radius: 8px; padding: 2px 8px;
  border: 2px solid var(--bg2);
  box-shadow: 0 2px 8px rgba(255,107,43,0.4);
}

.prf-info { flex: 1; min-width: 0; }
.prf-name {
  font-size: clamp(20px, 3vw, 28px); font-weight: 900;
  letter-spacing: -0.5px; margin-bottom: 6px;
}
.prf-meta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }
.prf-league {
  display: inline-flex; align-items: center; gap: 5px;
  background: linear-gradient(135deg, rgba(245,158,11,.12), rgba(245,158,11,.06));
  border: 1px solid rgba(245,158,11,.25);
  border-radius: 20px; padding: 4px 12px;
  font-size: 12px; font-weight: 700; color: var(--yellow);
}
.prf-since { font-size: 12px; color: var(--text3); }

/* XP bar */
.prf-xp-wrap { max-width: 380px; }
.prf-xp-top {
  display: flex; justify-content: space-between;
  font-size: 12px; font-weight: 600; margin-bottom: 7px;
}
.prf-xp-label { color: var(--text3); text-transform: uppercase; letter-spacing: 1px; }
.prf-xp-val   { color: var(--orange); }
.prf-xp-bar { height: 8px; background: var(--bg3); border-radius: 4px; overflow: hidden; }
.prf-xp-fill {
  height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, var(--orange), var(--yellow));
  transition: width 1s var(--ease);
  position: relative;
}
.prf-xp-fill::after {
  content: '';
  position: absolute; right: 0; top: 0; bottom: 0;
  width: 3px; background: rgba(255,255,255,0.6);
  border-radius: 2px;
}

/* Edit button */
.prf-edit-btn {
  display: flex; align-items: center; gap: 7px;
  padding: 9px 18px; border-radius: 10px;
  border: 1px solid var(--border2); background: var(--bg3);
  color: var(--text); font-family: 'Inter', sans-serif;
  font-size: 13px; font-weight: 600; cursor: pointer;
  transition: all 0.2s; white-space: nowrap; flex-shrink: 0;
}
.prf-edit-btn:hover { border-color: var(--orange); color: var(--orange); background: rgba(255,107,43,0.04); }

/* ── 2. Blocs statistiques (2 cartes) ── */
/* Barre d'XP du profil — les deux morceaux qui vivaient en style inline.
   `.prf-xp-fill` part de 0 : le JS pose la largeur réelle juste après, et sans
   cette valeur la barre serait pleine le temps d'un battement de cil. */
.prf-xp-next  { color: var(--text3); font-weight: 500; }
.prf-xp-fill  { width: 0; }
.prf-xp-reste { margin-top: 6px; font-size: 12px; color: var(--text3); }

/* ── Bloc « Ma saison » (profil) ──────────────────────────────────────────
   Masqué tant que get_etat_saison() n'a pas répondu, et définitivement si elle
   ne répond pas : `hidden` doit donc l'emporter sur le `display:flex`. */
.prf-saison[hidden] { display: none !important; }
.prf-saison {
  display: flex; flex-direction: column; gap: 8px;
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 18px 20px; margin-bottom: 16px;
}
.prf-saison-head   { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.prf-saison-titre  { margin: 0; font-size: 15px; font-weight: 800; letter-spacing: -.2px; }
.prf-saison-meta   { font-size: 12px; color: var(--text3); }
.prf-saison-ligue  { font-size: 14px; font-weight: 700; color: var(--yellow); }
.prf-saison-xp     { font-size: 14px; font-weight: 700; color: var(--violet2); }
.prf-saison-note   { margin: 0; font-size: 12px; line-height: 1.45; color: var(--text2); }
.prf-saison-lien   { align-self: flex-start; margin-top: 2px; font-size: 12px; font-weight: 700;
                     color: var(--orange); text-decoration: none; }
.prf-saison-lien:hover { text-decoration: underline; }
.prf-saison-ligue[hidden], .prf-saison-xp[hidden], .prf-saison-note[hidden] { display: none; }

/* ── Palmarès — « Saisons passées » (Compétitions) et « Mon palmarès » (profil) ──
   UN bloc pour les deux pages, peint par palmares.js — jamais de copie par page.
   ⚠️ Aucun élément n'y est basculé par `hidden` : le socle est REMPLACÉ et le
   groupe se déplie par un <details> natif. Il n'y a donc pas de règle [hidden]
   à poser ici (voir l'en-tête de palmares.js) ; si ça change, elle vient avec.
   Seul le placement dans la grille des Compétitions vit dans classement.html,
   parce que cette grille y est définie. */
.pal {
  display: flex; flex-direction: column; gap: 12px; min-width: 0; box-sizing: border-box;
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 18px 20px;
  /* ⚠️ C'EST UNE <section>, ET LA VITRINE EN A FAIT UN PIÈGE. Les règles globales
     `section { padding … ; max-width: 1200px; margin: 0 auto }` (haut de ce
     fichier) visent la page d'accueil. Dans la colonne flex du profil, la marge
     auto CENTRE l'élément et le RÉDUIT à son contenu — mesuré en Chromium à
     1280 px : 301 px de large dans une colonne de 704, quand les blocs voisins
     en font 640. Même piège que #campaignProgress (claude.md §16). On neutralise
     ici marge et largeur maximale ; le padding, lui, est déjà repris ci-dessus. */
  margin: 0; max-width: none;
  scroll-margin-top: 72px;   /* arrivée par l'ancre : l'en-tête collant ne couvre pas le titre */
}
.pal--compact     { margin-bottom: 16px; }   /* même rythme que .prf-saison, juste au-dessus */
.pal-titre        { margin: 0; font-size: 15px; font-weight: 800; letter-spacing: -.2px; }
.pal-corps        { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.pal-message      { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text2); }

/* Une saison (variante complète) : un <details>, le résumé est la zone à toucher. */
.pal-saison       { min-width: 0; background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius-md); }
.pal-resume       { display: block; list-style: none; cursor: pointer; padding: 14px 16px; border-radius: var(--radius-md); }
.pal-resume::-webkit-details-marker { display: none; }
.pal-resume:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.pal-tete         { display: flex; align-items: baseline; justify-content: space-between; gap: 4px 12px; flex-wrap: wrap; }
.pal-nom          { font-size: 14px; font-weight: 800; color: var(--text); }
.pal-dates        { font-size: 12px; color: var(--text3); }
.pal-faits        { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.pal-fait         { font-size: 12px; font-weight: 700; color: var(--text2); background: var(--bg2);
                    border: 1px solid var(--border2); border-radius: 20px; padding: 3px 10px; }
.pal-fait--ligue  { color: var(--yellow); }
.pal-sens         { display: block; margin-top: 8px; font-size: 13px; font-weight: 700; line-height: 1.4; }
.pal-sens--montee     { color: var(--green); }
.pal-sens--descente   { color: var(--red); }
.pal-sens--maintien   { color: var(--text); }
.pal-sens--repetition { color: var(--text2); font-weight: 600; }
.pal-deplier      { display: inline-flex; align-items: center; gap: 8px; margin-top: 10px;
                    font-size: 12px; font-weight: 700; color: var(--orange); }
.pal-deplier::after { content: ''; width: 6px; height: 6px; margin-top: -3px;
                    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
                    transform: rotate(45deg); transition: transform .2s var(--ease); }
.pal-saison[open] .pal-deplier::after { margin-top: 3px; transform: rotate(-135deg); }

/* Le classement final du groupe — la ligne du joueur porte la même mise en
   avant que sa ligne au classement en cours (.rank-item.me). */
.pal-groupe       { border-top: 1px solid var(--border); padding: 12px 16px 14px; }
.pal-groupe-titre { margin: 0 0 8px; font-size: 11px; font-weight: 800; letter-spacing: .6px;
                    text-transform: uppercase; color: var(--text3); }
.pal-liste        { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.pal-ligne        { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 8px 10px; border-radius: var(--radius-sm); }
.pal-ligne--moi   { background: rgba(255,107,43,.12); box-shadow: inset 3px 0 0 0 var(--orange); }
.pal-ligne-rang   { flex-shrink: 0; width: 24px; text-align: center; font-size: 13px; font-weight: 800; color: var(--text2); }
.pal-ligne--moi .pal-ligne-rang { color: var(--orange); }
.pal-ligne-nom    { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; }
.pal-ligne-pseudo { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                    font-size: 14px; font-weight: 600; color: var(--text); }
.pal-toi          { flex-shrink: 0; font-size: 10px; font-weight: 800; letter-spacing: .4px; text-transform: uppercase;
                    color: var(--orange); border: 1px solid rgba(255,107,43,.45); border-radius: 10px; padding: 1px 6px; }
.pal-ligne-xp     { flex-shrink: 0; font-size: 13px; font-weight: 800; color: var(--orange); }
.pal-partis       { margin: 8px 0 0; font-size: 12px; color: var(--text3); }

/* Variante compacte (profil) : une ligne par saison, son propre résultat. */
.pal-lignes       { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.pal-court        { display: flex; flex-direction: column; gap: 6px; min-width: 0; padding: 12px 14px;
                    background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius-md); }
.pal-court .pal-faits, .pal-court .pal-sens { margin-top: 0; }
.pal-lien         { align-self: flex-start; font-size: 12px; font-weight: 700; color: var(--orange); text-decoration: none; }
.pal-lien:hover   { text-decoration: underline; }

/* Téléphone : la variante complète se resserre comme les cartes voisines des
   Compétitions ; la compacte garde le padding de « Ma saison », juste au-dessus,
   pour que les deux titres du profil restent alignés. */
@media (max-width: 480px) {
  .pal:not(.pal--compact) { padding: 16px 14px; }
  .pal-resume  { padding: 12px; }
  .pal-groupe  { padding: 10px 12px 12px; }
}

.prf-stats-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
@media(max-width: 900px) { .prf-stats-grid { grid-template-columns: repeat(2, 1fr); } }
@media(max-width: 640px) { .prf-stats-grid { grid-template-columns: repeat(2, 1fr); } }

.prf-stat-card {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 20px 16px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
  transition: all 0.2s;
}
.prf-stat-card:hover { border-color: var(--border2); transform: translateY(-2px); }
.prf-stat-icon {
  width: 42px; height: 42px; border-radius: 11px;
  display: flex; align-items: center; justify-content: center;
}
.prf-stat-val {
  font-size: clamp(22px, 3vw, 28px); font-weight: 900; letter-spacing: -1px;
  color: var(--orange); line-height: 1;
}
.prf-stat-label {
  font-size: var(--text-xs); font-weight: 600; color: var(--text2);
  text-transform: uppercase; letter-spacing: 0.5px;
}

/* ── 3. Annonceurs complétés ── */
.prf-brands {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
@media(max-width: 640px) { .prf-brands { grid-template-columns: 1fr; } }

.prf-brand {
  display: flex; align-items: center; gap: 12px;
  padding: 14px; border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--bg2);
  transition: all 0.2s;
}
.prf-brand-done {
  border-color: rgba(34,197,94,0.2);
  background: linear-gradient(135deg, rgba(34,197,94,0.04), transparent);
}
.prf-brand-locked { opacity: 0.45; }
.prf-brand-logo {
  width: 44px; height: 44px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.prf-brand-info { flex: 1; min-width: 0; }
.prf-brand-name { font-size: 14px; font-weight: 700; margin-bottom: 3px; }
.prf-brand-reward { font-size: 12px; color: var(--green); font-weight: 600; }
.prf-brand-check {
  width: 28px; height: 28px; border-radius: 50%;
  background: rgba(34,197,94,0.12);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}

/* ── 4. Historique activité ── */
.prf-activity {
  display: flex; flex-direction: column;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;
}
.prf-act-item {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
  transition: background 0.15s;
}
.prf-act-item:last-child { border-bottom: none; }
.prf-act-item:hover { background: var(--bg3); }

.prf-act-icon {
  width: 36px; height: 36px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.prf-act-quest { background: rgba(255,107,43,0.1); color: var(--orange); }

.prf-act-body { flex: 1; min-width: 0; }
.prf-act-title {
  font-size: 14px; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  margin-bottom: 3px;
}
.prf-act-sub { font-size: 12px; color: var(--text2); }

.prf-act-right { text-align: right; flex-shrink: 0; }
.prf-act-xp {
  font-size: 13px; font-weight: 800; color: var(--orange);
  margin-bottom: 3px;
}
.prf-act-time { font-size: 11px; color: var(--text3); }

/* Mobile adaptations profil */
@media(max-width: 640px) {
  .prf-header { flex-direction: column; padding: 20px; }
  .prf-edit-btn { width: 100%; justify-content: center; }
  .prf-name { font-size: 20px; }
  .prf-avatar-zone { flex-direction: column; text-align: center; align-items: center; }
  .prf-xp-wrap { max-width: 100%; }
}

/* ============================================================
   20. SIDEBAR — B2B Block "Grandir avec Qwestify"
   ============================================================ */
.dt-b2b-block {
  padding: 14px 16px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
  width: 100%; box-sizing: border-box;
}
.dt-b2b-title {
  font-size: 11px; font-weight: 700;
  color: var(--text2); margin-bottom: 3px;
  letter-spacing: .2px;
}
.dt-b2b-sub {
  font-size: 10px; color: var(--text3);
  line-height: 1.4; margin-bottom: 10px;
}
.dt-b2b-link {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 12px; border-radius: 9px;
  background: rgba(255,107,43,.06);
  border: 1px solid rgba(255,107,43,.18);
  color: var(--orange); font-family: 'Inter', sans-serif;
  font-size: 12px; font-weight: 700;
  text-decoration: none; cursor: pointer;
  transition: all .2s;
}
.dt-b2b-link:hover { background: rgba(255,107,43,.12); border-color: rgba(255,107,43,.35); }

/* ── Liens légaux en bas de sidebar (2 colonnes) ── */
.sidebar-footer-links {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px 12px;
  margin-top: 16px;
}

.sidebar-footer-links a {
  font-size: 11px;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.35);
  text-decoration: none;
  transition: color .2s ease;
  white-space: nowrap;
}

.sidebar-footer-links a:hover {
  color: rgba(255, 255, 255, 1);
}

/* ── Emoji devant les nav items ── */
.dt-nav-emoji {
  font-size: 16px;
  line-height: 1;
  flex-shrink: 0;
  width: 20px;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Masquer le SVG icon sur desktop quand emoji présent */
@media(min-width:1024px) {
  .dt-nav-item .dt-nav-emoji + svg { display: none; }
}

/* ============================================================
   21. SIDEBAR & HEADER FIXES — no emoji, full text, right-align
   ============================================================ */
@media(min-width:1024px){

  /* Fix 1 — Sidebar wider to show "Découvre et gagne" fully */
  .logged-in-layout .dt-sidebar{
    width:240px !important;
    max-width:240px !important;
  }
  .logged-in-layout .dt-main{
    margin-left:240px !important;
  }

  /* Fix 2 — Nav items: never truncate, text always visible */
  .dt-nav{
    overflow-x:hidden !important;
    overflow-y:auto !important;
  }
  .dt-nav-item{
    white-space:nowrap !important;
    overflow:visible !important;
    text-overflow:unset !important;
  }
  .dt-nav-item span{
    white-space:nowrap !important;
    overflow:visible !important;
    text-overflow:unset !important;
    flex-shrink:0 !important;
    min-width:0;
  }

  /* Fix 3 — Topbar right block pushed to far right */
  .dt-topbar{
    gap:0 !important;
  }
  .dt-topbar-right{
    margin-left:auto !important;
    gap:10px !important;
  }

  /* Emoji span hidden — use SVG only */
  .dt-nav-emoji{
    display:none !important;
  }
}

/* Paramètres · « Réinitialiser » la photo (phase 2d) — sorti du style en ligne, avec une
   cible tactile de 44 px (claude.md §18 r. 3 ; il en faisait 37). Caché par défaut :
   _updateResetBtnVisibility() l'affiche quand il y a une photo perso. */
.set-reset-photo {
  display: none; min-height: 44px; padding: 8px 16px; border-radius: 10px;
  border: 1px solid rgba(239,68,68,.4); background: transparent; color: var(--red, #EF4444);
  font-weight: 600; cursor: pointer; transition: all .2s; white-space: nowrap;
}
.set-reset-photo:hover { background: rgba(239,68,68,.08); }
.set-reset-photo:disabled { cursor: default; opacity: .7; }

/* ============================================================
   22. MOBILE RESPONSIVE — @media (max-width: 768px)
   Couvre tous les écrans smartphone (iPhone, Android standard)
   ============================================================ */
@media(max-width: 768px) {

  /* ── Reset body padding ── */
  body { overflow-x: hidden; }
  #app { padding-bottom: 64px; } /* bottom nav height */

  /* ── Sidebar desktop : masquée sur mobile ──
     La sidebar .dt-sidebar est déjà display:none par défaut.
     On s'assure qu'elle ne réapparaît jamais sur mobile. */
  .dt-sidebar,
  .dt-main,
  .dt-topbar { display: none !important; }

  /* ── App nav mobile (burger) : visible ── */
  .app-nav  { display: flex !important; }
  .bottom-nav { display: flex !important; }

  /* ════════════════════════════════════════════
     LANDING PAGE
     ════════════════════════════════════════════ */

  /* Hero */
  .hero { padding: 88px 20px 48px; min-height: auto; }
  .hero h1 { font-size: clamp(28px, 8vw, 44px); letter-spacing: -1px; }
  .hero p  { font-size: var(--text-base); }
  .hero-cta { flex-direction: column; max-width: 100%; }
  .btn-primary, .btn-secondary { width: 100%; font-size: var(--text-base); padding: 14px 20px; }

  /* Sections landing */
  section { padding: 52px 16px; }
  .section-title { font-size: clamp(22px, 6vw, 32px); }
  .section-sub { font-size: var(--text-base); }

  /* Steps : 1 colonne */

  /* Quest types : 1 colonne */

  /* Streak days */

  /* Footer */
  footer#landingFooter { padding: 28px 20px; }
  .footer-links { gap: 12px; }

  /* ════════════════════════════════════════════
     APP MOBILE — contenu general
     ════════════════════════════════════════════ */

  /* Carrousel */

  /* ════════════════════════════════════════════
     PROFIL — prf-stats-grid : 2 colonnes → 2 col
     (déjà géré à 640px, on force ici aussi)
     ════════════════════════════════════════════ */
  .prf-stats-grid {
    grid-template-columns: 1fr 1fr !important;
    gap: 10px;
  }
  .prf-stat-val { font-size: 22px; }
  .prf-header { flex-direction: column; padding: 20px 16px; gap: 16px; }
  .prf-avatar-zone { flex-direction: column; align-items: center; text-align: center; }
  .prf-edit-btn { width: 100%; justify-content: center; }
  .prf-name { font-size: 20px; }
  .prf-xp-wrap { max-width: 100%; }
  .prf-brands { grid-template-columns: 1fr !important; }
  .prf-act-title { font-size: 13px; }

  /* ════════════════════════════════════════════
     PARAMÈTRES — inputs empilés verticalement
     ════════════════════════════════════════════ */
  .set-hero { flex-wrap: wrap; padding: 20px 16px; gap: 14px; }
  .set-change-photo { width: 100%; justify-content: center; }
  .set-hero-name { font-size: 18px; }

  /* Champs : label au-dessus, input en dessous, bouton full width */
  .set-field {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 14px 16px;
  }
  .set-field-label {
    width: 100%;
    font-size: 12px;
    color: var(--text3);
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }
  .set-field-input {
    width: 100%;
    font-size: 15px;
    padding: 11px 14px;
  }
  .set-save-btn {
    width: 100%;
    padding: 10px;
    font-size: 13px;
  }
  .set-danger-zone { flex-direction: column; padding: 16px; }
  .set-delete-btn { width: 100%; text-align: center; }

  /* ════════════════════════════════════════════
     BIENVENUE — déjà bien, on affine
     ════════════════════════════════════════════ */
  .bvn-card { padding: 32px 20px 36px; border-radius: 0; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; }
  .bvn-title { font-size: 26px; }
  .bvn-emoji { font-size: 60px; }
  .bvn-stats { flex-direction: row !important; gap: 8px; }
  .bvn-stat { padding: 12px 8px; }
  .bvn-stat-val { font-size: 20px; }
  .bvn-cta { font-size: 15px; padding: 14px 20px; }

  /* ════════════════════════════════════════════
     CAMPAGNES — zone centrale adaptée
     ════════════════════════════════════════════ */
  .cmp-hero { padding: 24px 20px; min-height: 160px; }
  .cmp-title { font-size: clamp(20px, 5vw, 28px); }
  .cmp-info-row { grid-template-columns: 1fr !important; gap: 10px; }
  .cmp-info-card { padding: 14px; }
  .cmp-progress-card { padding: 16px; }
  .cmp-quest { padding: 14px; gap: 12px; }
  .cmp-quest-name { font-size: 14px; }
  .cmp-quest-desc { font-size: 12px; }

  /* ════════════════════════════════════════════
     CLASSEMENT — podium et liste
     ════════════════════════════════════════════ */
  .podium-section { padding: 16px 12px 0; }
  .my-pos { margin: 0 12px 12px; }
  .rank-section { padding: 8px 12px; }
  .ri-name { font-size: 13px; }

  /* ════════════════════════════════════════════
     FORMULAIRES AUTH (popup connexion)
     ════════════════════════════════════════════ */
  #authModal { padding: 24px 16px 32px; }

  /* ════════════════════════════════════════════
     GM POPUP
     ════════════════════════════════════════════ */
  /* ⚠️ Ce bloc RÉDUISAIT le titre à var(--text-base) — soit 12 px sur mobile,
     les tokens typographiques étant resserrés plus bas dans cette même requête.
     Le titre de la feuille tombait donc à 12 px alors que la maquette en
     demande 17. Les tailles Apple sont pensées POUR le téléphone : on les
     laisse telles quelles et on ne garde ici que le rembourrage. */
  .gm-sheet { padding: 10px 20px calc(26px + env(safe-area-inset-bottom, 0px)); }

  /* ════════════════════════════════════════════
     TYPOGRAPHIE GLOBALE — confort sur mobile
     ════════════════════════════════════════════ */
  .ls-name  { font-size: var(--text-sm); }
  .dt-block-title { font-size: var(--text-base); }

  /* Bottom nav : texte lisible */
  .bnav-label { font-size: 10px; letter-spacing: 0; }
  .bnav-btn svg { width: 20px; height: 20px; }
}

/* ── Très petits écrans (iPhone SE, 375px) ── */
@media(max-width: 400px) {
  .hero h1 { font-size: 26px; }
  .prf-stats-grid { grid-template-columns: 1fr 1fr !important; }
  .bvn-stats { flex-direction: column !important; }
  .bvn-stat { max-width: 100%; }
  section { padding: 44px 14px; }
}

/* ═══════════════════════════════════════════════════════════════════
   IDENTICON OVERRIDE — Suppression radicale du fond violet sur avatars
   Ces règles écrasent les gradients orange→violet partout.
   L'identicon DiceBear (background-image inline) prend le dessus.
   ═══════════════════════════════════════════════════════════════════ */

/* Tous les conteneurs avatar : fond transparent + texte invisible */
/* Éléments avatar non-pill : reset texte/couleur uniquement */
.side-avatar,
.dt-user-av,
.dt-dd-av,
.prf-avatar,
.set-avatar {
  background-color: transparent !important;
  background-image: none;
  color: transparent !important;
  font-size: 0 !important;
}
/* Pill avatar : on garde le gradient de fond comme fallback visible */
.dt-avatar-pill,
.avatar-badge {
  color: transparent !important;
  font-size: 0 !important;
}

/* Préserver le badge de niveau (lvl-badge) à l'intérieur */
.dt-avatar-pill .lvl-badge,
.avatar-badge .lvl-badge {
  color: #fff !important;
  font-size: 10px !important;
  background: rgba(0, 0, 0, .45) !important;
}

/* Quand l'identicon est chargé, on s'assure que rien ne passe */
.is-avatar-loaded {
  color: transparent !important;
  font-size: 0 !important;
}
.is-avatar-loaded .lvl-badge {
  color: #fff !important;
  font-size: 10px !important;
}

/* ═══════════════════════════════════════════════════════════════════
   BLOC "MON ACTIVITÉ" v2 — Architecture Layer3
   ═══════════════════════════════════════════════════════════════════ */
.act-card-v2{background:var(--bg2);border:1px solid var(--border);border-radius:16px;height:auto;overflow:visible;min-width:320px;width:320px}
.acv2-header{display:flex;align-items:center;justify-content:space-between;padding:13px 16px 10px;border-bottom:1px solid #333}
.acv2-title{font-size:var(--text-sm);font-weight:800;color:var(--text)}
.acv2-learn{font-size:11px;color:var(--orange);text-decoration:none;font-weight:600;opacity:.85;transition:opacity .15s}
.acv2-learn:hover{opacity:1}
.acv2-xp-section{padding:12px 16px;border-bottom:1px solid #333}
.acv2-xp-label{font-size:var(--text-xs);font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--text3);margin-bottom:4px}
.acv2-xp-value{font-size:19px;font-weight:800;color:var(--orange);margin-bottom:6px;line-height:1}
.acv2-xp-next{font-size:11px;font-weight:500;color:var(--text3)}
.acv2-bar-track{height:4px;background:var(--bg3);border-radius:2px;overflow:hidden}
.acv2-bar-fill{height:4px;background:var(--orange);border-radius:2px;transition:width .4s cubic-bezier(.4,0,.2,1)}
.acv2-section{padding:10px 16px;border-bottom:1px solid #333}
.acv2-section-title{font-size:var(--text-xs);font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--text3);margin-bottom:8px}
.acv2-row{display:flex;justify-content:space-between;align-items:center;padding:3px 0}
/* La ligne de rang se retire quand le joueur n'en a pas. `.acv2-row` est en
   display:flex, qui battrait l'attribut hidden — d'où cette règle. */
.acv2-row[hidden]{display:none}

/* ── Modale « Choisis ton pseudo » (shared.js) ────────────────────────────────
   Aucun style inline : la modale est construite en createElement + textContent,
   tout son habillage vit ici. */
.qw-pseudo-fond {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(0, 0, 0, .62);
}
.qw-pseudo-boite {
  width: 100%; max-width: 380px;
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 24px 22px;
  box-shadow: 0 18px 50px -12px rgba(0, 0, 0, .6);
}
.qw-pseudo-titre { margin: 0 0 6px; font-size: 17px; font-weight: 800; letter-spacing: -.2px; }
.qw-pseudo-intro { margin: 0 0 16px; font-size: 13px; line-height: 1.45; color: var(--text3); }
.qw-pseudo-champ {
  width: 100%; box-sizing: border-box; padding: 12px 14px;
  background: var(--bg3); border: 1px solid var(--border);
  border-radius: 10px; color: var(--text); font-size: 15px; font-family: inherit;
}
.qw-pseudo-champ:focus { outline: none; border-color: var(--orange); }
.qw-pseudo-erreur { margin: 8px 0 0; font-size: 12px; line-height: 1.4; color: #E5484D; }
.qw-pseudo-erreur[hidden] { display: none; }
.qw-pseudo-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
.qw-pseudo-plus-tard, .qw-pseudo-valider {
  padding: 10px 16px; border-radius: 9px; font-size: 14px; font-weight: 700;
  font-family: inherit; cursor: pointer; border: 1px solid transparent;
}
.qw-pseudo-plus-tard { background: transparent; border-color: var(--border); color: var(--text3); }
.qw-pseudo-plus-tard:hover { color: var(--text); border-color: var(--border2); }
.qw-pseudo-valider { background: var(--orange); color: #fff; }
.qw-pseudo-valider:disabled { opacity: .6; cursor: default; }
/* Message d'erreur sous un champ de réglages (parametres.html). */
.set-field-erreur { margin: 6px 0 0; font-size: 12px; line-height: 1.4; color: #E5484D; }
.set-field-erreur[hidden] { display: none; }
.acv2-row-val--rang{color:var(--orange)}
.acv2-row-label{font-size:12px;color:var(--text2)}
.acv2-row-val{font-size:12px;font-weight:700;color:var(--text)}
.acv2-bottom{display:flex;align-items:center;border-bottom:1px solid #333}
.acv2-bottom-item{flex:1;text-align:center;padding:10px 8px}
.acv2-bottom-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:3px}
.acv2-bottom-val{font-size:14px;font-weight:800;color:var(--text)}
.acv2-gm-hint{font-size:12px;line-height:1.4;color:var(--text3);margin-top:9px;display:flex;align-items:center;gap:6px}
.acv2-gm-hint.is-warn{color:#D8862F}
/* Compteur SÉRIE GM — couleur selon l'état (défaut jaune, remplace l'ancien inline) */
.gm-streak-val { color: var(--yellow); }
.gm-streak-val.is-gap  { color: #6a6a76; }   /* série en sursis : chiffre terne */
.gm-streak-val.is-lost { color: #555561; }   /* série perdue : chiffre éteint */
/* Badge "EN SURSIS" à côté du compteur (état gap uniquement) */
.gm-streak-badge {
  display: inline-block;
  margin-top: 4px;
  background: #3a2e14;
  color: #d8862f;
  font-size: 10px;
  font-weight: 600;
  padding: 3px 8px;
  border-radius: 6px;
  text-transform: uppercase;
  letter-spacing: .04em;
  white-space: nowrap;
}
.acv2-sep{width:1px;height:36px;background:#333;flex-shrink:0}
/* La tuile « XP de saison » et son séparateur se masquent hors saison via
   l'attribut hidden. Ni l'un ni l'autre ne pose `display` aujourd'hui, donc
   la règle du navigateur suffirait — on l'écrit quand même : le jour où
   quelqu'un mettra `display:flex` sur .acv2-bottom-item, la tuile
   réapparaîtrait hors saison sans que rien ne le signale. */
.acv2-bottom-item[hidden], .acv2-sep[hidden] { display: none; }
.acv2-activity{padding:8px 16px 12px}
.acv2-act-item{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.04)}
.acv2-act-item:last-child{border-bottom:none}
.acv2-act-dot{width:6px;height:6px;border-radius:50%;background:var(--orange);flex-shrink:0}
.acv2-act-text{font-size:11px;color:var(--text2);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.acv2-act-xp{font-size:10px;font-weight:700;color:var(--green);flex-shrink:0}

/* ── Section Fonctionnement v2 — 3 colonnes ─────────────────────────── */

/* ── Carousel : affiche le 1er slide par défaut avant l'init JS ─────────
   Évite le carrousel "vide" quand initDtCarousel tourne trop tôt. */
.dt-carousel .ac-slide:first-child,
.auto-carousel .ac-slide:first-child {
  display: block;
}

/* ── Fallback : forcer la visibilité des sections si JS n'a pas déclenché ─
   Évite le contenu invisible si IntersectionObserver ne s'est pas exécuté. */
@media (prefers-reduced-motion: no-preference) {
  .reveal { animation: revealFallback 0.8s ease forwards; }
}
@keyframes revealFallback {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}
.reveal.visible { opacity: 1 !important; transform: translateY(0) !important; }

/* ── #authOverlay — fenêtre d'authentification centrée ──────────────────
   Ces règles écrasent tout conflit. Ne pas supprimer. */
#authOverlay {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  z-index: 9999 !important;
  background: rgba(0, 0, 0, 0.6) !important;
  backdrop-filter: blur(6px);
  align-items: center !important;
  justify-content: center !important;
  overflow-y: auto;
}
#authOverlay[style*="flex"] {
  display: flex !important;
}
#authModal {
  position: relative;
  margin: auto;
  max-height: 90vh;
  overflow-y: auto;
}

/* ── Auth modal centré — animation et contenu toujours visible ─────────── */
@keyframes fadeIn { from { opacity:0; transform:scale(.97); } to { opacity:1; transform:scale(1); } }

#authModal {
  position: relative !important;
  border-radius: 20px !important;
  width: 90% !important;
  max-width: 440px !important;
  box-sizing: border-box;
  margin: auto;
  flex-shrink: 0;
}

#authContent {
  display: block !important;
  visibility: visible !important;
  width: 100%;
  min-height: 100px;
}

#authContent input,
#authContent button {
  display: block !important;
  visibility: visible !important;
  width: 100%;
  box-sizing: border-box;
}

/* ── CSS d'urgence authContent — force tout visible ────────────────────── */
#authContent,
#authContent *,
#authContent div,
#authContent input,
#authContent button,
#authContent span,
#authContent label {
  visibility: visible !important;
  opacity: 1 !important;
}

#authContent {
  display: block !important;
  height: auto !important;
  width: 100% !important;
  min-height: 120px !important;
  overflow: visible !important;
}

#authModal {
  opacity: 1 !important;
  visibility: visible !important;
  overflow: visible !important;
}

/* Animation de la modal — doit terminer en visible */
#authModal,
#authModal * {
  animation-fill-mode: forwards !important;
}

/* ── Masquage instantané du landing quand l'utilisateur est connecté ──────
   UNIQUEMENT des sélecteurs d'ID (uniques) pour ne pas toucher le contenu
   de #app. Le wrapper #landing cache tout le reste après DOMContentLoaded. */
html.user-logged-in #landingNav,
html.user-logged-in #landingHero,
html.user-logged-in #fonctionnement,
html.user-logged-in #landingQuests,
html.user-logged-in #landingFooter,
html.user-logged-in #landing {
  display: none !important;
}

/* L'app s'affiche immédiatement, sans affecter son contenu interne */
html.user-logged-in #app {
  display: block !important;
}

/* ── Sidebar et Main : actifs dès que logged-in-layout est présent, SANS dépendre du media query ── */
.logged-in-layout .dt-sidebar {
  display: flex !important;
  flex-direction: column;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: 240px; max-width: 240px;
  box-sizing: border-box; overflow: hidden;
  background: var(--bg);
  border-right: none !important;
  z-index: 200;
}
.logged-in-layout .dt-main {
  display: flex !important;
  flex-direction: column;
  margin-left: 240px;
  min-height: 100vh;
}

/* ── Lot A · A1 — Shell déconnecté riche (#app.anon-rich) ──────────────────
   Crochet CSS DÉDIÉ au visiteur. On N'UTILISE PAS body.is-logged-out, qui
   déclenche le bloc legacy « sidebar MASQUÉE DÉFINITIVEMENT » (plus bas dans
   ce fichier) et masquait la sidebar du shell. #app.anon-rich porte aussi
   .logged-in-layout : tout le responsive (mobile, app-nav, barre du bas)
   reste donc piloté par .logged-in-layout. Ces règles ne font que GARANTIR,
   sur desktop, la sidebar visible et le décalage du contenu — à l'identique
   du connecté. Placé AVANT l'override mobile ci-dessous pour que ≤768px
   continue de masquer le shell desktop. */
#app.anon-rich .dt-sidebar {
  display: flex !important;
}
#app.anon-rich .dt-main {
  margin-left: 240px;
}

/* ── Lot A · A3 — Topbar visiteur : avatar/ligue → boutons Connexion/S'inscrire ──
   Piloté par #app.anon-rich (jamais en connecté). Composants en classes (pas d'inline).
   Masque aussi les séparateurs voisins pour éviter un double-trait dans la topbar. */
.dt-anon-cta { display: none; }                         /* masqué par défaut (mode connecté) */
#app.anon-rich #dtAvatarWrap,
#app.anon-rich #dtLeaguePill,
#app.anon-rich .dt-topbar-right .dt-topbar-sep { display: none !important; }
#app.anon-rich .dt-anon-cta { display: flex; align-items: center; gap: 10px; }
#app.anon-rich .acv2-row-ranking { display: none !important; }   /* pas de classement pour un visiteur */

/* ── A3 (suite) — MÊME contrat, appliqué à la barre haute MOBILE (.app-nav) ──
   Les règles ci-dessus ne visent que la topbar desktop (#dtAvatarWrap /
   #dtLeaguePill). La barre mobile porte d'autres éléments (.avatar-badge,
   .league-badge) et n'avait donc aucune conscience de l'authentification : le
   visiteur y voyait un avatar, un niveau « — » et une ligue « — », et le clic
   sur l'avatar ouvrait « Mon espace » avec le bouton « GM — Valider
   aujourd'hui », un bonus quotidien réservé aux membres. Personne ne l'avait
   vu parce qu'avant l'ouverture de l'accueil réel sur téléphone, aucun
   visiteur n'atteignait cette barre.

   Les deux chemins de connexion qui existaient sur téléphone — le tiroir
   burger et l'onglet « Profil » de la barre du bas — demandent tous deux une
   ouverture préalable. La barre elle-même n'en offrait AUCUN : elle se
   contentait de retirer ce que le visiteur ne doit pas voir. Le bouton « Se
   connecter » ci-dessous comble ce vide (troisième chemin, direct).

   Portée : #app.anon-rich (donc jamais un membre) + .app-nav (masquée
   au-dessus de 768px, donc jamais le bureau). .lvl-badge est un enfant de
   .avatar-badge ; on le nomme quand même, pour que la règle tienne s'il
   déménage. */
#app.anon-rich .app-nav .avatar-badge,
#app.anon-rich .app-nav .lvl-badge,
#app.anon-rich .app-nav .league-badge { display: none !important; }

/* Le pendant EXACT des trois lignes ci-dessus : ce que le membre voit ici
   (ligue + avatar) disparaît pour le visiteur, et ce bouton prend la place
   libérée — même sélecteur, même bloc, donc un seul endroit à lire pour savoir
   qui voit quoi. Masqué par défaut : sur les six autres pages, où rien ne pose
   .anon-rich, il n'existe pas à l'écran. Sur bureau, .app-nav est masquée et
   c'est .dt-anon-cta (Connexion / S'inscrire) qui tient ce rôle.
   Hauteur 34px = celle de .icon-btn, pour un alignement optique exact avec le
   burger et la loupe qui l'encadrent. */
.app-nav-login { display: none; }
#app.anon-rich .app-nav .app-nav-login {
  display: inline-flex; align-items: center; flex-shrink: 0;
  height: 34px; padding: 0 12px;
  background: transparent; color: var(--text);
  border: 1px solid var(--border2); border-radius: 10px;
  font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 600;
  text-decoration: none; white-space: nowrap;
}
#app.anon-rich .app-nav .app-nav-login:hover { border-color: var(--orange); color: var(--orange); }

/* ≤360px — le mot-symbole rétrécit CARRÉ au lieu de s'écraser.
   .app-nav-logo est en flex:1 1 auto et ses huit tuiles gardent le
   flex-shrink par défaut : la largeur cède, la hauteur non. Mesuré à 320px
   avec ce bouton en place, sans la règle ci-dessous : des tuiles de 14,1px de
   large pour 20px de haut — des lettres en rectangles. On leur donne donc une
   taille qui TIENT sans être comprimée (8 × 14 + 7 × 1 = 119px pour 127px
   disponibles), ce qui les laisse carrées.
   Visiteur uniquement : le membre n'a pas ce bouton, ses tuiles restent à
   20px. Et aucun débordement horizontal dans les deux cas — .app-nav est en
   overflow:hidden, mais c'est ici la place réelle qui est vérifiée, pas le
   masquage du dépassement. */
@media (max-width: 360px) {
  #app.anon-rich .app-nav .app-nav-logo { gap: 1px; }
  #app.anon-rich .app-nav .anl { width: 14px; height: 14px; font-size: 10px; border-radius: 3px; }
  #app.anon-rich .app-nav .app-nav-login { padding: 0 10px; font-size: 12.5px; }
}

/* Bloc "Revenez chaque jour" : bouton GM pour les connectés, texte pour les visiteurs */
#app.anon-rich .gptp-gm-cta { display: none; }
.gptp-gm-anon { display: none; }
#app.anon-rich .gptp-gm-anon { display: inline-flex; align-items: center; gap: 6px; cursor: default; margin-top: 4px; }

.btn-anon-login {
  background: transparent;
  border: 1px solid var(--border2);
  color: var(--text);
  padding: 8px 16px; border-radius: 10px; cursor: pointer;
  font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 500;
}
.btn-anon-login:hover { border-color: var(--orange); color: var(--orange); }
.btn-anon-signup {
  background: var(--orange);
  border: none; color: #fff;
  padding: 8px 16px; border-radius: 10px; cursor: pointer;
  font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 600;
}
.btn-anon-signup:hover { filter: brightness(1.08); }

/* ── Lot A · A4 — Cartes campagne verrouillées pour le visiteur ──────────────
   ⚠️ CE BLOC A ÉTÉ FUSIONNÉ dans les règles partagées du gate, plus bas
   (« CARTES VERROUILLÉES »). Il décrivait exactement le même dessin que
   #app.intro-pending — image grisée, pastilles en couleur, cadenas au titre —
   et deux copies d'une même intention finissent par diverger.

   Ce qui a disparu au passage, à dessein : `pointer-events:none` et
   `cursor:default`. Le commentaire d'origine annonçait lui-même cette
   évolution (« clic → inscription : retirer cette ligne et laisser le handler
   JS délégué gérer le clic »). Une carte morte n'explique rien ; désormais le
   clic remonte au handler, qui ouvre la feuille d'inscription. */

/* ══════════════════════════════════════════════════════════════════════════
   GATE D'ONBOARDING — #app.intro-pending
   ─────────────────────────────────────────────────────────────────────────
   Tant qu'un joueur CONNECTÉ n'a pas terminé la quête d'intro (is_demo=true),
   la home lui montre cette quête à la place du carrousel et verrouille le
   reste. Une fois l'intro faite, le marqueur tombe et tout revient.

   ⚠️ MARQUEUR PARALLÈLE, PAS UN REMPLAÇANT. `#app.anon-rich` (§11) décrit un
   VISITEUR ; `#app.intro-pending` décrit un MEMBRE pas encore onboardé. Les
   deux ne coexistent jamais (_showApp retire anon-rich avant de poser
   intro-pending) mais ils ont des règles distinctes, car ils ne verrouillent
   pas la même chose : le visiteur garde ses quêtes cliquables (il doit pouvoir
   jouer la démo sans compte), le membre non onboardé ne les garde pas.

   ⚠️ PAS DE `pointer-events: none` ICI — contrairement à anon-rich. Une carte
   morte n'explique rien ; ici le clic DOIT arriver jusqu'au handler délégué
   pour déclencher le nudge vers l'intro. C'est exactement l'évolution que le
   commentaire d'anon-rich annonçait (« retirer cette ligne et laisser le
   handler JS gérer le clic »). Le curseur reste donc un pointeur : la carte
   répond, elle ne se contente pas d'être grise.
   ══════════════════════════════════════════════════════════════════════════ */

/* 1 — Le carrousel cède la place à la quête d'intro. Mêmes cibles qu'anon-rich. */
#app.intro-pending #dtAutoCarousel,
#app.intro-pending #dtDots { display: none !important; }
#app.intro-pending .dt-demo-featured { display: block; }

/* ══ CARTES VERROUILLÉES — RÈGLES PARTAGÉES PAR LES DEUX ÉTATS ══════════════
   #app.anon-rich (visiteur) ET #app.intro-pending (membre pas encore onboardé)
   verrouillent désormais la MÊME chose, avec le MÊME dessin. Les sélecteurs
   sont doublés plutôt que les règles : c'est le même vocabulaire de « pas
   encore pour toi » dans toute l'application, il ne doit exister qu'une fois.
   Les deux classes ne coexistent jamais (_showApp retire anon-rich avant de
   poser intro-pending) — ce doublement ne peut donc rien additionner.
   Seule la RÉPONSE AU CLIC diffère, et elle est en JS : le visiteur reçoit la
   feuille d'inscription, le membre non onboardé son nudge vers l'intro. */

/* 2 — CAMPAGNES verrouillées. */
#app.anon-rich .ob-card-link[href*="campagne.html"] .card-banner-container,
#app.intro-pending .ob-card-link[href*="campagne.html"] .card-banner-container {
  filter: grayscale(85%) brightness(0.55);
}
#app.anon-rich .ob-card-link[href*="campagne.html"] .ob-card-title::after,
#app.intro-pending .ob-card-link[href*="campagne.html"] .ob-card-title::after {
  content: " 🔒";
  font-size: 0.85em;
  opacity: 0.9;
  margin-left: 4px;
}

/* 3 — QUÊTES verrouillées — ce qu'anon-rich ne fait PAS (le visiteur doit
   pouvoir jouer la démo). Le socle `.qc-plinth` porte le visuel de la carte :
   on le grise, comme la bannière d'une campagne, et les pastilles de
   récompense restent en couleur (elles sont ses SŒURS, pas ses filles).

   ⚠️ `:not([data-qw-intro])` — LA SORTIE DU GATE. La carte de la quête d'intro
   est elle-même une carte quête ; sans cette exclusion on verrouillerait la
   seule porte qui permet d'en sortir. L'attribut est posé par le JS, qui
   connaît l'id de la démo ; le handler du nudge vérifie EN PLUS le href au
   moment du clic, si bien qu'un marquage tardif ne peut jamais enfermer le
   joueur. */
#app.anon-rich .ob-card-link[href*="quete.html"]:not([data-qw-intro]) .qc-plinth,
#app.intro-pending .ob-card-link[href*="quete.html"]:not([data-qw-intro]) .qc-plinth {
  filter: grayscale(85%) brightness(0.55);
}
#app.anon-rich .ob-card-link[href*="quete.html"]:not([data-qw-intro]) .ob-card-title::after,
#app.intro-pending .ob-card-link[href*="quete.html"]:not([data-qw-intro]) .ob-card-title::after {
  content: " 🔒";
  font-size: 0.85em;
  opacity: 0.9;
  margin-left: 4px;
}
/* Les rangées « Nouveau » / « Tendances » (.ls-item) sont des <div> à écouteur
   JS, pas des <a> — mais le handler en phase de CAPTURE les intercepte de la
   même façon. Elles excluent déjà la démo à la source (quest-lists.js filtre
   `neq('is_demo', true)`), il n'y a donc aucune exception à y prévoir. */
#app.anon-rich .ls-item .ls-logo,
#app.intro-pending .ls-item .ls-logo { filter: grayscale(85%) brightness(0.6); }
#app.anon-rich .ls-item .ls-name::after,
#app.intro-pending .ls-item .ls-name::after {
  content: " 🔒";
  font-size: 0.85em;
  opacity: 0.9;
  margin-left: 4px;
}

/* ══ FEUILLE D'INSCRIPTION DU VISITEUR — #qwAnonGate ════════════════════════
   Ce que voit un visiteur qui touche une carte verrouillée. Ce n'est PAS un
   toast : un toast s'efface pendant qu'on le lit et n'offre aucune action ; il
   faut ici un bouton, donc une surface qui attend.

   ⚠️ AUCUN NOUVEAU SYSTÈME DE MODALE. On réutilise celui des feuilles GM —
   .gm-overlay / .gm-sheet / .gm-handle / .gm-close / .gm-title / .gm-subtitle
   / .btn-gm-claim — qui est déjà une feuille par le BAS, pensée pour le
   téléphone : pleine largeur jusqu'à 480px, coins hauts arrondis, glissement
   depuis le bas, et un rembourrage qui tient compte de env(safe-area-inset-
   bottom) — la barre gestuelle des iPhone récents. Elle porte aussi ses
   variantes claire et sombre. Tout cela existe ; le redessiner aurait produit
   une deuxième vérité.

   Ne restent ici que les trois écarts réels. */

/* 1 — La croix passe de 30 à 44px : c'est la cible tactile minimale, et cette
   feuille s'ouvre d'abord au doigt. Portée à .qw-gate-sheet uniquement — les
   feuilles GM gardent la leur. */
.qw-gate-sheet .gm-close { width: 44px; height: 44px; }

/* 2 — Le cadenas, repris du dessin des cartes : c'est le même objet qu'on
   vient de toucher, il doit se reconnaître. */
.qw-gate-ic { font-size: 34px; line-height: 1; text-align: center; margin: 4px 0 12px; }

/* 3 — Le refus, discret mais pas caché : 44px de haut lui aussi, sans quoi il
   serait une cible qu'on rate et une porte qu'on ne peut pas prendre. */
.qw-gate-later {
  display: block; width: 100%; min-height: 44px; margin-top: 6px;
  background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--gm-sub); transition: color .2s;
}
.qw-gate-later:hover { color: var(--gm-title); }

/* 4 — Le message d'incitation. Une CLASSE, pas un style écrit en JS : le
   cliquet ne compte que `style="` dans le HTML, mais y échapper en poussant le
   style dans le JS reviendrait à le soustraire au compteur sans le supprimer
   (§15). Il vit donc ici. */
.qw-intro-toast {
  position: fixed;
  bottom: 88px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 99999;
  max-width: min(92vw, 420px);
  padding: 12px 18px;
  border-radius: 999px;
  background: rgba(12, 12, 18, .94);
  border: 1px solid rgba(255, 255, 255, .14);
  box-shadow: 0 10px 34px rgba(0, 0, 0, .45);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  text-align: center;
  pointer-events: none;
  opacity: 1;
  transition: opacity .35s ease;
}
.qw-intro-toast.is-fading { opacity: 0; }
/* Sur bureau la barre basse n'existe pas : le message peut descendre. */
@media (min-width: 769px) {
  .qw-intro-toast { bottom: 32px; }
}

/* ── Lot C — Grand bloc vedette « quête démo » (visiteur) ───────────────────
   En #app.anon-rich : le carrousel est masqué et remplacé par le bloc démo
   (style « Get Started »). En connecté : carrousel normal, bloc démo masqué. */
.dt-demo-featured { display: none; }
#app.anon-rich #dtAutoCarousel,
#app.anon-rich #dtDots { display: none !important; }
#app.anon-rich .dt-demo-featured { display: block; }

.demo-feat-card {
  border-radius: 18px;
  overflow: hidden;
  background: var(--bg2);
  border: 1px solid var(--border);
}
/* ⚠️ CE CADRE ÉTAIT EN height:220px + background-size:cover, ET IL DÉCAPITAIT
   L'IMAGE. Un cadre de hauteur fixe et de largeur fluide n'a pas de ratio : à
   375px il valait 345×220 (1,57:1), sur un bureau 1440 il valait ~845×220
   (3,84:1). Pour une image 4:3, cover cale sur la largeur et jette le débord
   vertical — mesuré ~15 % à 375px et ~65 % sur bureau. On ne voyait plus qu'un
   ruban horizontal de lettres coupées, et la première image du produit vue par
   un visiteur non connecté ne montrait pas ce qu'elle représente.

   Trois pièces, et chacune répond à une contrainte précise :

   · aspect-ratio 16/9 + max-height — le cadre a enfin un RATIO, donc il ne
     dépend plus de la largeur de l'écran. Le plafond évite qu'un bureau large
     ne fabrique une bannière de 475px de haut.
   · background-size:contain sur ::after — l'image ENTIÈRE, jamais rognée, quel
     que soit son ratio. C'est la seule promesse qui tienne pour des visuels
     téléversés par des marchands, dont on ne maîtrise pas le cadrage.
   · ::before — la même image en cover, floutée, DERRIÈRE. Contain laisse par
     construction des bandes dès que l'image et le cadre n'ont pas le même
     ratio ; un aplat les signalerait, ce flou les comble avec les couleurs de
     l'image elle-même.

   ⚠️ POURQUOI DEUX PSEUDO-ÉLÉMENTS, ET PAS LE FOND DE .demo-feat-banner.
   Le fond d'un élément se peint SOUS ses pseudo-éléments : porté par le
   .demo-feat-banner lui-même, la couche nette serait recouverte par le flou.
   Et `filter` sur l'élément flouterait aussi le badge, qui est un enfant. D'où
   deux couches empilées à z-index explicite, et le badge remonté au-dessus.

   L'URL vient de --demo-img, écrite en JS par renderDemoFeatured (index.html).
   Le repli `none` évite un url() invalide tant que la donnée n'est pas là. */
.demo-feat-banner {
  /* ⚠️ width:100% N'EST PAS REDONDANT, malgré un bloc de niveau bloc.
     Dès que max-height BORNE une boîte qui porte un aspect-ratio, le navigateur
     re-dérive la LARGEUR depuis la hauteur bornée. Mesuré sur un bureau 1440 :
     sans cette ligne, la bannière tombait à 533px (= 300 × 16/9) dans une carte
     de 1128px, et se retrouvait plantée au milieu du vide. Avec : 1128×300. */
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 300px;
  background-color: var(--bg3);
  position: relative;
  overflow: hidden;             /* borne le flou débordant de ::before */
  display: flex;
  align-items: flex-start;
  padding: 16px;
}
/* Couche 1 — le fond flou qui comble les bandes de contain. Le scale(1.12)
   masque les bords délavés que tout flou gaussien produit en périphérie. */
.demo-feat-banner::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--demo-img, none);
  background-size: cover;
  background-position: center;
  /* brightness fortement abaissée : à 0,75 le flou restait plus LUMINEUX que les
     bords sombres de l'image et se lisait comme deux panneaux rivaux de part et
     d'autre — mesuré à l'œil sur un bureau 1440. À 0,34 il redevient ce qu'il
     doit être : une lueur d'ambiance qui prolonge l'image, pas un décor. */
  filter: blur(40px) saturate(1.1) brightness(0.34);
  transform: scale(1.2);
}
/* Couche 2 — l'image nette, entière.
   La boîte prend le RATIO de l'image (--demo-ratio, mesuré en JS sur la source),
   pleine hauteur, largeur déduite, centrée par inset:0 + margin:auto. Elle épouse
   donc l'image au lieu de la contenir : la couture entre le net et le flou suit
   le bord de l'image, et l'ombre portée la pose franchement sur le fond — sans
   ça, on lisait trois panneaux juxtaposés, mesuré sur un bureau 1440.
   background-size:contain et non cover, à dessein : si --demo-ratio manque ou
   ment, contain montre l'image entière quand même. Le repli ne rogne jamais. */
.demo-feat-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: 1;
  height: 100%;
  width: auto;
  max-width: 100%;
  aspect-ratio: var(--demo-ratio, 16 / 9);
  background-image: var(--demo-img, none);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: 0 0 60px 10px rgba(0, 0, 0, 0.7);
}
.demo-feat-badge {
  position: relative;
  z-index: 2;                   /* au-dessus des deux couches d'image */
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
}
.demo-feat-title {
  font-size: 1.6rem;
  font-weight: 800;
  margin: 18px 22px 4px;
  color: var(--text);
}
.demo-feat-sub {
  font-size: 0.95rem;
  color: var(--text2);
  margin: 0 22px 18px;
  line-height: 1.45;
}
.demo-feat-btn {
  margin: 0 22px 22px;
  background: var(--orange);
  color: #fff;
  border: none;
  padding: 12px 28px;
  border-radius: 12px;
  font-family: 'Inter', sans-serif;
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
}
.demo-feat-btn:hover { filter: brightness(1.08); }

/* ── Override mobile strict — doit venir APRÈS toutes les règles globales
   logged-in-layout pour que la cascade CSS respecte le layout mobile.
   - sidebar + dt-main masqués
   - app-nav et bottom-nav restaurés pour les connectés ── */
@media (max-width: 768px) {
  /* Masquer le layout desktop */
  .logged-in-layout .dt-sidebar,
  #app.logged-in-layout .dt-sidebar {
    display: none !important;
    width: 0 !important;
    max-width: 0 !important;
    overflow: hidden !important;
  }
  .logged-in-layout .dt-main,
  #app.logged-in-layout .dt-main {
    display: none !important;
  }
}
/* Masquer le mobile dans le layout connecté hors media query */
.logged-in-layout .app-nav,
.logged-in-layout .bottom-nav { display: none !important; }
/* ── Réactivation mobile connecté — APRÈS la règle globale ci-dessus ──────
   Même sélecteur + !important, mais placé plus bas dans la cascade :
   sur ≤768px les deux éléments sont restaurés pour l'interface mobile. ── */
@media (max-width: 768px) {
  .logged-in-layout .app-nav       { display: flex  !important; }
  .logged-in-layout .bottom-nav    { display: flex  !important; }
}
/* dt-right : min-width:0 pour les grilles (ne pas écraser le display natif) */
.logged-in-layout .dt-right { min-width: 0; }

/* ── Footer connecté (bas de dt-center) ──────────────────────────────────── */
.dt-inner-footer {
  margin-top: 32px;
  padding: 20px 0 12px;
  border-top: 1px solid var(--border);
}
.dt-inner-footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-bottom: 8px;
}
.dt-inner-footer-links a {
  font-size: 12px;
  color: var(--text3);
  text-decoration: none;
  transition: color .2s;
}
.dt-inner-footer-links a:hover { color: var(--orange); }
.dt-inner-footer-copy {
  font-size: 11px;
  color: var(--text3);
  opacity: .6;
}

/* ── Mode déconnecté (Landing Page) ────────────────────────────────────── */
/* Logo dans le header — visible seulement en mode déconnecté */
.dt-header-logo {
  display: none;
  align-items: center;
  gap: 8px;
  font-size: 24px;
  font-weight: 700;
  text-decoration: none;
  color: var(--text);
  transition: color .2s;
}
.dt-header-logo:hover { color: var(--orange); }
.dt-header-logo .anl {
  display: inline-block;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  border-radius: 6px;
  color: #fff;
}
.dt-header-logo .ll1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.dt-header-logo .ll2 { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.dt-header-logo .ll3 { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.dt-header-logo .ll4 { background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
.dt-header-logo .ll5 { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
.dt-header-logo .ll6 { background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); }
.dt-header-logo .ll7 { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); }
.dt-header-logo .ll8 { background: linear-gradient(135deg, #ff9a56 0%, #ff6a88 100%); }

/* ── Mode déconnecté : grille stricte 100% largeur, sidebar MASQUÉE DÉFINITIVEMENT ── */
body.is-logged-out {
  --sidebar-width: 0;
}
/* RÈGLE ULTRA-STRICTE : masquer la sidebar en mode déconnecté, peu importe les autres classes */
body.is-logged-out .dt-sidebar,
body.is-logged-out.logged-in-layout .dt-sidebar {
  display: none !important;
  position: fixed !important;
  left: -9999px !important;
  width: 0 !important;
  max-width: 0 !important;
  visibility: hidden !important;
  overflow: hidden !important;
  opacity: 0 !important;
}
body.is-logged-out .dt-main,
body.is-logged-out.logged-in-layout .dt-main {
  display: flex !important;
  flex-direction: column !important;
  margin-left: 0 !important;
  width: 100% !important;
  max-width: 100% !important;
  flex: 1 !important;
}
body.is-logged-out .dt-header-logo {
  display: flex !important;
}
/* Masquer les éléments connectés */
body.is-logged-out .app-nav { display: none !important; }

/* Dropdown avatar en mode déconnecté — masquer tout sauf Classement et avatar */
body.is-logged-out #dtDropdown {
  min-width: 160px !important;
}
/* Masquer le pseudo et le niveau du dropdown header */
body.is-logged-out #dtDdName,
body.is-logged-out #dtDdLvl {
  display: none !important;
}
/* Masquer la div contenant pseudo/niveau (garder juste l'avatar) */
body.is-logged-out #dtDropdown > div:first-child > div:nth-child(2) {
  display: none !important;
}
/* Masquer "Mon Profil" (premier lien a) */
body.is-logged-out #dtDropdown > div:nth-child(2) > a:nth-child(1) {
  display: none !important;
}
/* Masquer "Succès" (button dans les items) */
body.is-logged-out #dtDropdown > div:nth-child(2) > button {
  display: none !important;
}
/* Masquer le séparateur (div avec juste une hauteur) */
body.is-logged-out #dtDropdown > div:nth-child(2) > div[style*="height:1px"] {
  display: none !important;
}
/* Masquer "Déconnexion" — tous les autres buttons */
body.is-logged-out #dtDropdown button[onclick*="exitApp"],
body.is-logged-out #dtDropdown button[onclick*="doLogout"] {
  display: none !important;
}

/* ═══ RÈGLE FINALE ULTRA-STRICTE : MASQUER SIDEBAR EN MODE DÉCONNECTÉ ═══ */
body.is-logged-out .dt-sidebar {
  display: none !important;
  visibility: hidden !important;
  width: 0 !important;
  height: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
  border: none !important;
  overflow: hidden !important;
  position: absolute !important;
  left: -9999px !important;
  z-index: -999 !important;
}

/* Override même si logged-in-layout est présent */
body.is-logged-out .logged-in-layout .dt-sidebar,
body.is-logged-out .dt-main.logged-in-layout .dt-sidebar,
body.is-logged-out #app.logged-in-layout .dt-sidebar {
  display: none !important;
  visibility: hidden !important;
  width: 0 !important;
  height: 0 !important;
}

/* S'assurer que le main prend 100% en mode déconnecté */
body.is-logged-out .dt-main {
  margin-left: 0 !important;
  width: 100% !important;
  flex: 1 !important;
}

/* ── Activité sidebar — positionnement fixe ── */
.dt-activity-sidebar {
  position: fixed;
  top: 80px;
  right: 24px;
  width: 300px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
  z-index: 10;
  display: none;
}

.logged-in-layout .dt-activity-sidebar {
  display: block;
}

/* ── Sidebar Rétractable — Animation Layer3 ── */
.dt-body {
  transition: grid-template-columns 0.35s cubic-bezier(0.25, 1, 0.5, 1), gap 0.35s cubic-bezier(0.25, 1, 0.5, 1) !important;
}

body.sidebar-open .dt-body {
  grid-template-columns: 1fr 320px !important;
  gap: 0 !important;      /* gouttière droite déplacée sur le padding symétrique de .dt-center (voir bloc « Gouttières symétriques ») — évite l'empilement gap+padding */
  max-width: 2400px !important;
}

body.sidebar-open .dt-right {
  display: block !important;
  overflow: hidden !important;
}

/* ── M2 — Backdrop du panneau overlay (masqué hors bande 769-1279) ── */
.dtr-scrim{ display:none }

/* ── M2 — Panneau « Mon activité » en OVERLAY (bande 769-1279) ──
   Sous 1280px le panneau ne pousse plus le contenu : il FLOTTE à droite (position:fixed),
   GLISSE depuis la droite (translateX 100%->0, ~0.2s) au-dessus d'un backdrop assombri
   (fondu). Le contenu reste PLEINE LARGEUR (grille forcée 1fr 0fr — neutralise l'inline
   3fr 1fr ; le panneau est hors grille). ≤768 = shell mobile (.dt-main masqué → non concerné).
   Desktop ≥1024 (M1) intact. Anon toujours supprimé (garde #app.anon-rich plus bas). */
@media (min-width:769px) and (max-width:1279px){
  .dt-body{ grid-template-columns:1fr 0fr !important; gap:0 !important }
  body.sidebar-open .dt-body{ grid-template-columns:1fr 0fr !important; }   /* bat la globale 2046 (1fr 320px) : ouvert dans la bande = piste droite à 0 → carrousel entier, panneau flotte par-dessus */
  .dt-right{
    display:block !important;
    position:fixed !important; top:72px; right:16px; bottom:auto;
    width:min(320px, 90vw); z-index:270;   /* au-dessus du backdrop gauche (260) → reste net quand les 2 sont ouverts */
    max-height:calc(100vh - 88px);         /* carte contenue : hauteur = contenu, bornée à la fenêtre (72 haut + 16 bas) */
    background:var(--bg); border:1px solid var(--border); border-radius:16px;
    box-shadow:-10px 0 34px rgba(0,0,0,.5);
    overflow-y:auto;
    transform:translateX(100%);                    /* fermé = glissé hors écran à droite */
    transition:transform .2s var(--ease, ease);
  }
  body.sidebar-open .dt-right{ transform:translateX(0); }   /* ouvert = glisse en place */

  .dtr-scrim{
    display:block; position:fixed; inset:0; z-index:250;
    background:transparent;
    opacity:0; pointer-events:none; transition:opacity .2s;   /* backdrop en fondu */
  }
  body.sidebar-open .dtr-scrim{ opacity:1; pointer-events:auto; }
}

/* Garde visiteur déconnecté : le panneau d'activité (placeholder) ne s'ouvre jamais dans
   le shell #app.anon-rich, quel que soit l'état sidebar-open (défensif, indépendant du timing). */
#app.anon-rich .dt-body{ grid-template-columns:1fr 0fr !important }
#app.anon-rich .dt-right{ display:none !important }

/* ── Morceau B — Menu gauche en BURGER / overlay sous 1280px (bande 769-1279) ──
   La sidebar (déjà position:fixed) devient un tiroir off-canvas qui GLISSE depuis la GAUCHE
   (translateX -100%->0 sur body.nav-open, ~0.2s) au-dessus d'un backdrop assombri ; .dt-main
   repasse margin-left:0 (contenu ENTIER pleine largeur). Un burger ☰ (injecté dans .dt-topbar)
   ouvre/ferme ; fermeture backdrop / ✕ / Échap. ≥1280 = sidebar colonne inchangée, burger masqué.
   ≤768 = shell mobile (sidebar déjà display:none). Coexiste avec l'overlay droit M2 : z-index en
   tiers séparés (gauche scrim 270 / tiroir 280 / ✕ 290 ; droite scrim 250 / panneau 260) +
   exclusion mutuelle côté JS. Burger / scrim / ✕ masqués hors bande. */
.dtnav-burger{ display:none; align-items:center; justify-content:center; width:38px; height:38px;
  border:none; background:transparent; color:var(--text); font-size:20px; cursor:pointer }
.dtnav-scrim{ display:none }
.dtnav-close{ display:none; position:fixed; top:84px; left:calc(16px + min(280px,85vw) - 46px); z-index:290;
  width:34px; height:34px; align-items:center; justify-content:center; border:none; border-radius:8px;
  background:var(--bg3); color:var(--text2); font-size:16px; cursor:pointer }

@media (min-width:769px) and (max-width:1279px){
  /* Contenu pleine largeur : plus de colonne sidebar qui pousse */
  .logged-in-layout .dt-main, #app.anon-rich .dt-main{ margin-left:0 !important; }
  /* Sidebar off-canvas (glisse depuis la gauche) */
  .logged-in-layout .dt-sidebar, #app.anon-rich .dt-sidebar{
    width:min(280px,85vw) !important; max-width:min(280px,85vw) !important;
    z-index:280 !important;
    top:72px; left:16px; bottom:auto;            /* carte : sous la topbar, marge gauche, plus d'étirement pleine hauteur */
    max-height:calc(100vh - 88px);               /* bornée à la fenêtre (72 haut + 16 bas) */
    overflow-y:auto;                             /* scroll INTERNE si le contenu dépasse (override overflow:hidden global) */
    border:1px solid var(--border); border-radius:16px;   /* look carte, symétrique à .dt-right */
    transform:translateX(-100%); transition:transform .2s var(--ease, ease);
    box-shadow:10px 0 34px rgba(0,0,0,.5);
  }
  /* Ouverture du tiroir. Le second sélecteur n'est pas décoratif : la règle FERMÉE
     ci-dessus vise `#app.anon-rich .dt-sidebar`, soit (1,2,0) en spécificité, quand
     `body.nav-open .dt-sidebar` ne pèse que (0,2,1). Sur la coquille visiteur, le
     translateX(-100%) gagnait donc TOUJOURS : le burger s'affichait, se cliquait,
     posait bien nav-open, et le tiroir restait hors écran. Les autres pages y
     échappaient par accident — leur sélecteur fermé est `.logged-in-layout
     .dt-sidebar`, (0,2,0), qui perd bien face à nav-open.
     `body.nav-open #app.anon-rich .dt-sidebar` = (1,3,1), qui reprend la main. */
  body.nav-open .dt-sidebar,
  body.nav-open #app.anon-rich .dt-sidebar{ transform:translateX(0); }
  .dt-nav{ flex:none; }   /* carte compacte : nav = hauteur naturelle → bloc B2B + footer suivent le contenu (plus poussés en bas), scroll via la carte */
  /* Backdrop en fondu */
  .dtnav-scrim{ display:block; position:fixed; inset:0; z-index:260;   /* au-dessus du backdrop droit (250) → clic-milieu ferme le menu gauche ; sous les 2 panneaux */
    background:transparent;
    opacity:0; pointer-events:none; transition:opacity .2s; }
  body.nav-open .dtnav-scrim{ opacity:1; pointer-events:auto; }
  /* Burger visible ; ✕ visible quand ouvert */
  .dtnav-burger{ display:flex; }
  body.nav-open .dtnav-close{ display:flex; }
}

/* ── Correction coexistence — garder les 2 déclencheurs cliquables menu ouvert (bande 769-1279) ──
   Les scrims (inset:0, z 250/260) recouvrent la .dt-topbar (z 100, contexte d'empilement sticky)
   qui contient les 2 boutons → clic sur le bouton de l'autre menu intercepté par le scrim. Fix :
   quand un overlay est ouvert, on remonte la topbar AU-DESSUS des scrims (265, sous les drawers
   270/280) MAIS on la rend traversante (pointer-events:none) → cliquer le fond (y compris le fond
   de la topbar) ferme le menu ; seuls les 2 déclencheurs restent cliquables (pointer-events:auto).
   Empilement : scrims 250/260 < topbar 265 < panneau droit 270 < drawer gauche 280. */
@media (min-width:769px) and (max-width:1279px){
  /* Un bouton qui n'ouvre rien est pire que pas de bouton. quete.html pose
     délibérément body.is-logged-out pour le visiteur (quete.html:471), ce qui
     masque .dt-sidebar — le tiroir que ce burger est censé faire glisser. Le
     masquage de .app-nav qui l'accompagne ne vaut qu'en dessous de 769px, si
     bien qu'entre 769 et 1279 le burger survivait à sa propre cible : visible,
     cliquable, sans rien à ouvrir. On retire le contrôle, on ne ressuscite pas
     la sidebar — l'écran de quête masque sa navigation pour le visiteur, et
     c'est une décision de la page, pas un oubli. Le menu mobile (#sideMenu),
     lui, reste disponible sous 769px. */
  body.is-logged-out .dtnav-burger { display: none; }

  body.nav-open .dt-topbar,
  body.sidebar-open .dt-topbar{ z-index:265; pointer-events:none; }

  body.nav-open .dtnav-burger,  body.sidebar-open .dtnav-burger,
  body.nav-open #sidebarToggle, body.sidebar-open #sidebarToggle{ pointer-events:auto; }
}

/* ── Band 769-1023 — habillage topbar + onglets (Approche A) ───────────────────
   Le stylage topbar/onglets est gated ≥1024 ; sous 1024 (mais >768, hors shell
   mobile) il manquait → recherche pleine largeur + boutons natifs (cadres blancs).
   Ce bloc RECOPIE les 6 règles conteneur/pastille du bloc ≥1024, borné à 1023 pour
   ne JAMAIS recouvrir le desktop. Pastilles Ligue/avatar/séparateurs = déjà GLOBALES
   → rien à ajouter (elles se rangent dès que les flex existent). Burger + overlay
   (769-1279) non touchés (on habille autour). */
@media (min-width:769px) and (max-width:1023px){
  /* 1. Conteneur topbar (recopie de :525, padding 24->16) */
  .dt-topbar{display:flex;align-items:center;height:60px;gap:16px;padding:0 16px;background:var(--bg);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;box-sizing:border-box;width:100%}
  /* 2. Cluster droit poussé à droite (recopie de :530 + :1229) */
  .dt-topbar-right{display:flex;align-items:center;gap:10px;margin-left:auto;flex-shrink:0}
  /* 3. Barre de recherche (recopie de :526-529, max-width 420->320 ; loupe déjà fixée 1d222bb) */
  .dt-search{display:flex;align-items:center;gap:10px;background:var(--bg3);border:1px solid var(--border2);border-radius:10px;padding:8px 14px;flex:1;max-width:320px;min-width:0;transition:border-color .2s;box-sizing:border-box}
  .dt-search:focus-within{border-color:var(--orange)}
  .dt-search input{flex:1;min-width:0;background:none;border:none;outline:none;color:var(--text);font-size:14px}
  .dt-search input::placeholder{color:var(--text3)}
  /* 4. Toggle thème (recopie de :534-536 — sinon bouton sans contenu -> collapse) */
  .dt-theme-btn{width:44px;height:24px;background:var(--bg3);border:1px solid var(--border2);border-radius:12px;cursor:pointer;position:relative;flex-shrink:0}
  .dt-theme-btn::after{content:'';position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--text2);transition:transform .3s}
  [data-theme="light"] .dt-theme-btn::after{transform:translateX(20px);background:var(--orange)}
  /* 5. Conteneur onglets (recopie de :562) */
  .dt-filters{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
  /* 6. Onglets pastille (recopie de :563-565 — supprime le chrome natif) */
  .dt-fb{padding:7px 15px;border-radius:20px;border:1px solid var(--border2);background:transparent;color:var(--text2);font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
  .dt-fb:hover{background:var(--bg3);color:var(--text)}
  .dt-fb.active{background:var(--orange);border-color:var(--orange);color:#fff}

  /* ── 7. Contenu du MENU GAUCHE (carte flottante) — recopie de l'intérieur sidebar ≥1024
     (bloc @418), pour la bande 769-1023 où il n'était pas stylé : icônes géantes (svg sans
     taille, comme la loupe) + texte en <a> brut violet souligné. Le CONTENEUR carte (:2094)
     et .dt-nav{flex:none} (:2105) restent gérés par le bloc 769-1279 → ici on ajoute UNIQUEMENT
     le contenu, sans flex sur .dt-nav (cohabitation). Bloc B2B .dt-b2b-* = déjà GLOBAL. Version
     lean : seules les classes réellement rendues (zéro code mort, §10). ── */
  /* Logo (recopie :477-481 ; .anl non recopié = logo <img> inline, classe non utilisée) */
  .dt-logo-area{padding:15px 14px 11px;border-bottom:1px solid var(--border);flex-shrink:0;width:100%;box-sizing:border-box}
  .dt-logo-link{display:inline-flex;gap:2px;align-items:center;text-decoration:none;cursor:pointer}
  /* Conteneur nav — recopie :484-488 SANS flex:1 (garde le flex:none du band :2105) */
  .dt-nav{padding:10px 10px;display:flex;flex-direction:column;gap:2px;overflow:hidden;width:100%;box-sizing:border-box}
  /* Items + texte (recopie :490-498) — corrige liens violets bruts + layout flex/padding */
  .dt-nav-item{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:10px;font-size:var(--menu-text-size);font-weight:500;line-height:1.5;color:var(--text2);text-decoration:none;cursor:pointer;transition:color .3s ease,background-color .3s ease;width:100%;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
  /* Icônes (recopie :499) — corrige les icônes GÉANTES (svg viewBox sans width/height) */
  .dt-nav-item svg{width:17px;height:17px;flex-shrink:0;min-width:17px;stroke:currentColor}
  .dt-nav-item span{overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1}
  .dt-nav-item:hover{background:var(--bg3);color:var(--text)}
  .dt-nav-item.active{background:rgba(255,255,255,.08);color:#fff;font-weight:600;box-shadow:inset 3px 0 0 0 var(--orange)}
  /* Thème CLAIR : la règle ci-dessus écrit du blanc en dur sur un fond blanc translucide,
     sur le fond clair du rail (--bg) → ~1,1:1, texte ET icône (stroke:currentColor)
     invisibles. On repasse aux jetons du thème, comme le survol ; la barre orange reste.
     Le thème sombre n'est pas concerné : ce sélecteur ne s'y applique jamais. */
  [data-theme="light"] .dt-nav-item.active{background:var(--bg3);color:var(--text)}
  /* Badge « Bientôt » de l'item Arcade (recopie :507) */
  .dt-nav-badge-soon{margin-left:auto;flex-shrink:0;background:rgba(124,58,237,.18);border:1px solid rgba(124,58,237,.35);color:#c4b5fd;font-size:9px;font-weight:700;font-style:normal;letter-spacing:.4px;text-transform:uppercase;padding:1px 7px;border-radius:999px}
  /* Thème CLAIR : #c4b5fd sur la pastille violette → ~1,3:1. Même fond, même bordure ;
     texte violet assombri par le jeton --text (--violet seul plafonne à ~4:1). */
  [data-theme="light"] .dt-nav-badge-soon{color:color-mix(in srgb, var(--violet) 75%, var(--text))}
  /* Pied sidebar — liens légaux (recopie :518-520) */
  .dt-sidebar-foot{padding:10px 20px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px;flex-shrink:0;width:100%;box-sizing:border-box;overflow:hidden}
  .dt-foot-link{font-size:11px;color:var(--text3);text-decoration:none;white-space:nowrap;transition:color .2s}
  .dt-foot-link:hover{color:var(--text2)}
}

/* ── Gouttières symétriques du contenu (Layer3) — desktop + panneau ouvert ──
   Système UNIQUE : un padding horizontal ÉGAL gauche/droite sur .dt-center (le
   conteneur carrousel + grilles), piloté par --content-gutter. On neutralise
   l'ancien système asymétrique (gap 56px droite + padding-left 24px de .dt-body)
   pour NE PAS empiler. Résultat net : gutter gauche == gutter droite, à l'œil.
   Scopé min-width:1280 (le garde tablette 769-1279 ne réinitialise pas ce padding).
   .dt-center est en box-sizing:border-box (reset global) → le padding rentre dans la piste. */
@media (min-width:1280px){
  body.sidebar-open .dt-body{ padding-left: 0 !important; }   /* neutralise l'asymétrie gauche 24px */
  body.sidebar-open .dt-center{
    padding-left:  var(--content-gutter);       /* --content-gutter : source unique sur .dt-center (l.565) */
    padding-right: var(--content-gutter);
  }

  /* Panneau FERMÉ (desktop, connecté) — Option A : réserve le slot panneau (piste 320px)
     et applique les MÊMES gouttières que l'état ouvert → carrousel + grilles gardent
     EXACTEMENT la même taille/position (aucun saut au toggle : la grille reste 1fr 320px
     dans les deux états). La piste droite reste vide (.dt-right display:none via base 563)
     = vide à droite façon Layer3. Exclut #app.anon-rich (visiteur : pas de panneau,
     contenu pleine largeur inchangé). Le bouton #sidebarToggle (topbar) reste accessible. */
  body:not(.sidebar-open) #app:not(.anon-rich) .dt-body{
    grid-template-columns: 1fr 320px !important;   /* slot panneau réservé (vide) */
    padding-left: 0 !important;                    /* symétrie gauche = état ouvert */
  }
  body:not(.sidebar-open) #app:not(.anon-rich) .dt-center{
    padding-left:  var(--content-gutter);
    padding-right: var(--content-gutter);
  }
}

/* Bouton Toggle Sidebar */
.dt-sidebar-toggle {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text2);
  transition: color 0.2s;
  font-size: 14px;
}

.dt-sidebar-toggle:hover {
  color: var(--text);
}

.toggle-arrow {
  display: inline-block;
  transition: transform 0.3s ease-in-out;
  font-size: 16px;
  transform: rotate(0deg); /* Flèche → par défaut : cliquer pour ouvrir */
}

body.sidebar-open .toggle-arrow {
  transform: rotate(180deg); /* Flèche ← quand ouvert : cliquer pour fermer */
}

/* ── Campaign Page — Layer3 Design ── */
/* ── CAMPAIGN HERO — Style Layer3 flat ── */
.campaign-hero {
  /* La teinte de marque de la campagne : SEULE la couleur vient de la base
     (--hero-accent, posée par campagne.html via UI_themeAccent) ; la géométrie
     est ici. C'est ce fond que la bannière vient rejoindre en se fondant, donc
     il n'y a qu'un dégradé dans tout le héros — rien à raccorder.
     Repli #222226 : 10 campagnes sur 17 n'ont pas de color_theme, et le montage
     doit être aussi juste en neutre qu'en couleur.
     100deg plutôt que le 135 des cartes : le dégradé court de GAUCHE à droite,
     donc la teinte se pose derrière le titre et s'éteint vers la bannière. */
  --hero-tint: var(--hero-accent, #222226);
  background-color: #222226;
  /* Le sol atteint son ton sombre à 64 %, AVANT que la bannière ne commence.
     Mesuré : à 100 %, le fond était encore vert là où l'image posait son propre
     fond quasi noir, et cette rencontre dessinait une bande verticale molle.
     En arrivant au sombre plus tôt, les deux se rejoignent sur la même valeur —
     il n'y a plus de rencontre à voir. */
  background-image: linear-gradient(100deg,
      color-mix(in srgb, var(--hero-tint) 88%, #000000) 0%,
      color-mix(in srgb, var(--hero-tint) 52%, #101018) 34%,
      #101018 64%);
  border-radius: 16px;
  padding: 20px 24px;
  min-height: 300px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 0 !important;
  overflow: hidden;
  position: relative;
  z-index: 1;
}

/* ⚠️ CE HÉROS ÉTAIT EN cover + right center + masque, ET IL DÉCAPITAIT LA MARQUE.
   Le cadre est piloté par son TEXTE (label, titre, actions) : sur un bureau 1440
   il mesurait 1128×220, soit 5,13:1, pour une image 4:3. cover cale sur la
   largeur, monte l'image à 846px de haut et n'en montre que la bande centrale
   de 220 — 74 % de la hauteur jetée. Le propriétaire voyait « HYBRIDE » coupé
   à droite, et le haut de « DOCTEUR » disparaissait.
   Le masque aggravait le tout : il effaçait les 10 % gauches de l'image. Un
   masque est un recadrage qui ne dit pas son nom.

   Le remède tient en DEUX couches, pas plus : le fond teinté ci-dessus, et la
   bannière ci-dessous qui s'y fond. Un héros qui porte du texte ne peut pas
   laisser l'image occuper toute la surface — le titre se poserait dessus — donc
   l'image tient la droite, le texte la gauche, et le fondu fait la jonction.
   min-height 220 → 300 : à 220, l'image n'avait pas la place d'exister. */

/* LA BANNIÈRE — entière, et fondue dans la teinte par son bord intérieur.
   La boîte prend le RATIO de l'image (--hero-ratio, mesuré en JS sur la source)
   et toute la hauteur du héros : elle épouse donc l'image au pixel près. C'est
   la condition de tout le reste — un fondu ne peut être invisible que s'il est
   calé sur le bord RÉEL de l'image.

   ⚠️ C'est la leçon de la version précédente. Le fondu était porté par un voile
   posé sur tout le héros, avec des seuils en pourcentage de sa largeur ; le bord
   de l'image, lui, tombait où le ratio le décidait. Mesuré sur un bureau 1440 :
   l'image commençait à x=728, le voile y était déjà à 85 % de transparence, et
   il restait une couture verticale nette. Deux repères indépendants ne se
   rejoignent que par hasard. Ici il n'y en a plus qu'un.

   Trois côtés affleurent (haut, bas, droite) et le border-radius + overflow du
   héros les découpe : aucun bord visible. Le quatrième, à GAUCHE, est traité
   par le masque ci-dessous.

   max-width 76 % : une bannière très panoramique (4:1 et plus) dépasserait la
   largeur du héros. Elle est alors bornée, et background-size:contain la garde
   ENTIÈRE — elle cesse simplement d'affleurer le haut et le bas.
   Repli 16/9 si la sonde JS n'a pas encore répondu : jamais de boîte absurde. */
.campaign-hero::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: auto;
  max-width: 76%;
  aspect-ratio: var(--hero-ratio, 16 / 9);
  background-image: var(--header-bg-img, none);
  background-position: right center;
  background-size: contain;
  background-repeat: no-repeat;
  /* Le fondu. Il ne COUPE rien : il éteint progressivement le bord intérieur
     de l'image sur la teinte du héros, qui transparaît dessous. Palier COURT —
     pleine opacité dès 15 % : mesuré à 26 %, le « Q » d'une bannière QWESTIFY
     ressortait terni, et le premier caractère d'une marque est le dernier qu'on
     a le droit d'affadir. 15 % suffit à tuer l'arête ; au-delà, le sujet est
     intact. Ce n'est pas l'ancien masque, qui rabotait 10 % d'une image DÉJÀ
     recadrée en cover ; ici l'image est entière et seul son bord libre fond. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.6) 6%, #000 15%);
  mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.6) 6%, #000 15%);
  /* Lot 2 — la bannière passe DERRIÈRE le texte, plus à côté. Assombrie et
     désaturée, elle devient une texture de fond au lieu d'un sujet qui
     concurrence le titre. Mesuré sur la bannière SIG, à fond blanc : elle
     éclairait tout l'angle droit du héros et le titre s'y perdait.
     `filter` plutôt qu'un voile en plus : une seule couche à composer, et le
     masque de fondu ci-dessus continue de s'appliquer par-dessus. */
  filter: brightness(.42) saturate(.72);
  z-index: 0;
}

/* Téléphone : le titre prend toute la largeur, il ne reste pas de moitié droite
   à donner à l'image. Elle devient une bande basse pleine largeur, et le fondu
   bascule à la VERTICALE — éteint en haut sous le texte, plein en bas. Même
   principe, autre axe. La boîte reprend toute la largeur et laisse
   background-size:contain gérer la hauteur : le sujet reste entier. */
@media (max-width: 768px) {
  .campaign-hero::before {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-width: none;
    height: 58%;
    aspect-ratio: auto;
    background-position: center bottom;
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.6) 8%, #000 22%);
    mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.6) 8%, #000 22%);
  }
}

/* Tous les enfants au-dessus du pseudo-élément */
.campaign-hero > * {
  position: relative;
  z-index: 2;
}

/* Ligne du haut : label à gauche, boutons à droite */
.campaign-hero-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* Corps : titre à gauche, logo à droite */
.campaign-hero-body {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
}

.campaign-hero-left {
  flex: 1;
  min-width: 0;
}

/* L'ŒILLET « CAMPAGNE » — lisible sur N'IMPORTE QUELLE teinte de marque.
   ⚠️ Il était en `var(--text3)` (gris ardoise) sur un fond qui, lui, dépend de
   `campaigns.color_theme` — une donnée LIBRE, que personne ne contraint à être
   sombre. Mesuré sur caf-du-grain-unyx (dégradé orange clair) : fond réel
   derrière le label rgb(133,82,9), texte rgb(90,90,122) → rapport de contraste
   1.01:1. Ce n'est pas « peu lisible », c'est invisible : les deux luminances
   se confondent. Aucune couleur de texte fixe ne peut tenir sur un fond libre.
   D'où un VOILE : le label porte son propre fond, donc son contraste ne dépend
   plus de la marque. Opacité 0.78 choisie sur le PIRE cas, pas sur le cas
   observé — mesuré contre un fond blanc pur, elle donne encore 9.20:1 (AAA) ;
   sur l'orange du café, 14.69:1 ; sur le neutre #222226, 16.29:1.
   ⚠️ Padding HORIZONTAL seul, et liseré en `inset box-shadow` plutôt qu'en
   `border` : la hauteur du label reste 17,6px au pixel près, donc rien d'autre
   ne bouge. La rangée haute mesure 30px et c'est le BOUTON qui la dimensionne
   (mesuré), le label a de la marge — mais sur téléphone la rangée est empilée,
   et là toute hauteur gagnée pousserait le titre. */
.campaign-hero-label {
  font-size: 11px;
  color: rgba(255, 255, 255, .92);
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.8px;
  padding: 0 9px;
  border-radius: 999px;
  background: rgba(9, 9, 14, .78);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .10);
}

.campaign-hero-title {
  font-size: var(--text-xl);
  font-weight: 800;
  color: rgba(255,255,255,0.95);
  line-height: 1.2;
  margin: 0;
}

/* Boutons en ligne haute droite */
.campaign-hero-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* Bouton Règles — pilule solide */
/* Bouton "À propos" — frosted glass style */
.ob-btn-blur {
  padding: 6px 14px;
  border-radius: 20px;
  border: 1px solid rgba(255,255,255,.12);
  background: rgba(255,255,255,.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: #fff;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  font-family: 'Inter', sans-serif;
  transition: background .2s;
  white-space: nowrap;
}
.ob-btn-blur:hover { background: rgba(255,255,255,.14); }

/* Footer lingot — discret par défaut, lumineux au survol */
.footer-lingot {
  /* `display` et `cursor` viennent du style inline retiré en même temps que le
     dimensionnement du mot-symbole : la largeur est passée aux attributs
     width/height (172×20, la taille rendue), et le reste de la déclaration
     devait bien atterrir quelque part. Cette classe ne sert qu'à cette image. */
  display: block;
  cursor: pointer;
  filter: brightness(0.6) saturate(0.8) !important;
  transition: filter 0.3s ease;
}
.footer-lingot:hover {
  filter: brightness(1) saturate(1) !important;
}

/* Actions hero — flex row aligné à droite */
.campaign-hero-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  align-items: center;
}

.campaign-btn-rules {
  padding: 6px 14px;
  border-radius: 20px;
  border: none;
  background: #2c2c35;
  color: #fff;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  font-family: 'Inter', sans-serif;
  transition: background .2s;
  white-space: nowrap;
}
.campaign-btn-rules:hover { background: #35353f; }

/* Bouton Partager — fantôme subtil */
.campaign-btn-share {
  padding: 6px 14px;
  border-radius: 20px;
  border: none;
  background: rgba(255,255,255,.06);
  color: var(--text2);
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  font-family: 'Inter', sans-serif;
  transition: background .2s;
  white-space: nowrap;
}
.campaign-btn-share:hover { background: rgba(255,255,255,.1); }

.campaign-hero-right {
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
}
.campaign-hero-right img {
  max-height: 48px;
  width: auto;
  object-fit: contain;
}

/* ── Campaign Info Row — collés sous le hero ── */
.campaign-info-row {
  display: flex;
  gap: 8px;
  margin-top: -22px !important;   /* annule le gap:24px du .dt-center parent */
  margin-bottom: 20px;
  width: 100%;
}

.campaign-info-block {
  flex: 1;
  background: #222226;
  border-radius: 10px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--text);
}

.campaign-info-block svg {
  color: var(--text2);
}

/* ══ En-tête de campagne — bandeau de période + cartes (Lot 2) ═══════════════
   Le sous-titre n'est rendu que si la campagne a une description ; sa mise en
   forme vivait en `style.cssText` dans le JS, elle est ici. */
.campaign-hero-sub {
  font-size: 13px;
  color: rgba(255, 255, 255, .62);
  margin: 10px 0 0;
  line-height: 1.5;
  max-width: 52ch;
}

/* Bandeau pleine largeur : icône · dates · pastille poussée à droite.
   `flex-wrap` + `margin-left:auto` sur la pastille : sur un écran étroit la
   pastille passe à la ligne au lieu d'écraser les dates. */
.campaign-period {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  row-gap: 6px;
  width: 100%;
  box-sizing: border-box;
  margin-top: -22px;          /* annule le gap:24px du .dt-center parent */
  margin-bottom: 10px;
  padding: 11px 16px;
  border-radius: 12px;
  background: var(--bg2);
  border: 1px solid var(--border);
  color: var(--text2);
  font-size: 13px;
}
.campaign-period[hidden] { display: none; }
/* --text2 et non --text3 : mesuré, --text3 donnait 2,79:1 sur ce bandeau en
   sombre (#5A5A7A sur var(--bg2)) comme en clair — sous les 3:1 d'une icône. */
.campaign-period-icon { width: 16px; height: 16px; flex-shrink: 0; color: var(--text2); }
.campaign-period-dates { font-weight: 600; color: var(--text); }

.campaign-period-pill {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 11px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}
.campaign-period-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* Les trois états réutilisent les classes que _ui_computeStatut renvoie déjà
   (ob-status-*), pour que la page campagne et les cartes parlent la même
   langue visuelle. On ne redéfinit ici que le fond, propre à la pastille. */
.campaign-period-pill.ob-status-active   { color: var(--green);  background: rgba(34,197,94,.14); }
.campaign-period-pill.ob-status-soon     { color: var(--yellow); background: rgba(245,158,11,.14); }
.campaign-period-pill.ob-status-inactive { color: var(--text3);  background: rgba(255,255,255,.07); }

/* Intérieur des deux cartes */
.campaign-info-inner { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.campaign-info-avatars { display: inline-flex; align-items: center; flex-shrink: 0; }
.campaign-info-avatars:empty { display: none; }
.campaign-info-icon { width: 20px; height: 20px; flex-shrink: 0; color: var(--text3); }
.campaign-info-texts { min-width: 0; }
.campaign-info-label {
  font-size: 10px; color: var(--text2); text-transform: uppercase;
  letter-spacing: .5px; margin-bottom: 4px;
}
.campaign-info-value { font-size: 13px; font-weight: 700; color: var(--text); }

/* ── Téléphone (360-380 px) ────────────────────────────────────────────────
   Mesuré : deux cartes côte à côte laissent ~170 px chacune, assez pour
   « 3 Participants » et « 2 Quêtes » avec leurs pastilles. Elles restent donc
   sur une ligne. Le bandeau, lui, porte deux dates longues (« 12 juillet 2026
   → 6 septembre 2026 ») : sa pastille descend d'une ligne, d'où le flex-wrap
   ci-dessus. Rien ne déborde, rien n'est tronqué. */
@media (max-width: 430px) {
  .campaign-period { padding: 10px 12px; gap: 8px; font-size: 12px; }
  .campaign-period-pill { margin-left: 0; }
  .campaign-info-block { padding: 10px 12px; }
  .campaign-info-inner { gap: 8px; }
}

/* Campaign Quests Title */
.campaign-quests-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 16px;
  margin-top: 8px;
}

/* Campaign Grid (3 columns) */
.campaign-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  margin-top: 24px;
}

/* ── Quest Cards — Style Layer3 premium ── */
.quest-card {
  position: relative;
  height: 280px;
  border-radius: 16px;
  overflow: hidden;
  cursor: pointer;
  background: #222226;
  transition: transform .25s cubic-bezier(.4,0,.2,1), box-shadow .25s;
}

.quest-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 40px rgba(0,0,0,.45);
}

/* Image de fond */
.quest-card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Overlay dégradé : transparent en haut → gris mat en bas */
.quest-card-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(34,34,38,.0)  0%,
    rgba(34,34,38,.3) 35%,
    rgba(34,34,38,.85) 70%,
    rgba(34,34,38,1)  100%);
}

/* Zone de contenu (ancrée en bas) */
.quest-card-content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 14px 16px 16px;
  z-index: 2;
}

/* Barre de progression segmentée */
.quest-progress-segments {
  display: flex;
  gap: 4px;
  width: 100%;
  margin-bottom: 10px;
}

.quest-segment {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: rgba(255,255,255,.18);
  transition: background .3s;
}

.quest-segment.done {
  background: #22C55E;
}

/* Textes */
.quest-card-partner {
  font-size: 11px;
  font-weight: 600;
  color: rgba(255,255,255,.5);
  text-transform: uppercase;
  letter-spacing: .6px;
  margin-bottom: 4px;
}

.quest-card-title {
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  line-height: 1.3;
  margin-bottom: 8px;
}

.quest-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.quest-card-xp {
  font-size: 12px;
  font-weight: 700;
  color: var(--green);
  background: rgba(34,197,94,.12);
  border-radius: 6px;
  padding: 3px 8px;
}

/* Icône cœur — apparaît au survol */
/* Cœur — flotte sur l'image, aucun fond */
.quest-heart {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 3;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  opacity: 0;
  transform: scale(.8);
  transition: opacity .2s ease, transform .2s ease;
  line-height: 1;
}

/* SVG du cœur — outline blanc par défaut */
.quest-heart svg {
  width: 26px;
  height: 26px;
  fill: rgba(0,0,0,.2);        /* fond très légèrement assombri pour détacher sur images claires */
  stroke: #ffffff;
  stroke-width: 1.8;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.5));
  transition: fill .15s ease, stroke .15s ease;
}

/* État 1 : survol de la carte → le cœur apparaît */
.quest-card:hover .quest-heart {
  opacity: 1;
  transform: scale(1);
}

/* État 2 : survol direct du cœur → remplissage rouge vif */
.quest-heart:hover svg {
  fill: #ef4444;
  stroke: #ef4444;
}

/* ══════════════════════════════════════════════════════════════
   BLOC "SOYEZ PAYÉS POUR JOUER" — Layer3 style (v3, statique)
   ══════════════════════════════════════════════════════════════ */

.gptp-heading {
  font-size: 20px;
  font-weight: 800;
  color: var(--text);
  margin: 0 0 14px;
  letter-spacing: -.3px;
}

/* Conteneur : grille 3 col + 2 séparateurs */
.gptp-container {
  display: grid;
  grid-template-columns: 1fr 1px 1.2fr 1px 1fr;
  background: #18181c;
  border-radius: 16px;
  overflow: hidden;
  position: relative;
  min-height: 160px;
}

/* Lueur cyan — droite (double glow symétrique) */
.gptp-container::after {
  content: '';
  position: absolute;
  top: -60px; right: -60px;
  width: 300px; height: 300px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(34,211,238,.32) 0%,
    rgba(34,211,238,.12) 40%,
    transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/* Lueur cyan — gauche (symétrique droite) */
.gptp-container::before {
  content: '';
  position: absolute;
  top: -60px; left: -60px;
  width: 300px; height: 300px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(34,211,238,.32) 0%,
    rgba(34,211,238,.12) 40%,
    transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/* Colonnes — flex column, contenu poussé en bas (style Layer3 : visuels haut, texte bas) */
.gptp-col {
  padding: 20px 18px 24px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 8px;
  position: relative;
  z-index: 1;
}

/* Ligne icône + titre sur la même ligne horizontale */
.gptp-col-header {
  display: flex;
  align-items: flex-start;
  gap: 9px;
}

/* Séparateur vertical */
.gptp-divider {
  background: rgba(255,255,255,.07);
  width: 1px;
  align-self: stretch;
  z-index: 1;
}

/* Titres — imposants, style Layer3 */
.gptp-col-title {
  font-size: 18px;
  font-weight: 700;
  color: #fff;
  line-height: 1.25;
}

/* Sous-titres — max-width pour forcer le retour à la ligne propre */
.gptp-col-sub {
  font-size: 12px;
  color: rgba(255,255,255,.45);
  line-height: 1.55;
  max-width: 85%;
}

/* ── Pastilles QWESTIFY dispersées (position absolue) ── */
.gptp-letters {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;               /* derrière le texte des colonnes (z-index: 1) */
}

.gptp-ld {
  position: absolute;
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 1.5px solid;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 800;
  font-family: 'Inter', sans-serif;
}

/* Position et couleur de chaque pastille — anciens style="" du markup, repris
   VALEUR POUR VALEUR : le rendu bureau doit rester au pixel près. */
.gptp-ld--q { top: 8px;  left: 3%;   background: rgba(255,107,43,.18); color: #FF6B2B; border-color: rgba(255,107,43,.4); }
.gptp-ld--w { top: 30%;  left: 18%;  background: rgba(34,197,94,.15);  color: #22C55E; border-color: rgba(34,197,94,.4); }
.gptp-ld--e { top: 9px;  left: 45%;  background: rgba(124,58,237,.15); color: #9D5FF5; border-color: rgba(124,58,237,.4); }
.gptp-ld--s { top: 35%;  left: 60%;  background: rgba(245,158,11,.15); color: #F59E0B; border-color: rgba(245,158,11,.4); }
.gptp-ld--t { top: 8px;  right: 4%;  background: rgba(225,29,72,.15);  color: #E11D48; border-color: rgba(225,29,72,.4); }
.gptp-ld--i { top: 38%;  left: 4%;   background: rgba(8,145,178,.15);  color: #0891B2; border-color: rgba(8,145,178,.4); }
.gptp-ld--f { top: 22%;  right: 7%;  background: rgba(34,197,94,.15);  color: #22C55E; border-color: rgba(34,197,94,.4); }
.gptp-ld--y { top: 42%;  right: 3%;  background: rgba(255,107,43,.18); color: #FF6B2B; border-color: rgba(255,107,43,.4); }

/* Badges de colonne. `flex-shrink:0` était le 9ᵉ style en ligne du bloc.
   ⚠️ Le badge violet et son enveloppe n'existent QUE sous 860px : au-dessus,
   la colonne « Développez » n'a jamais eu de badge, et le bureau ne bouge pas.
   L'enveloppe est masquée elle aussi — vide mais présente, elle aurait ajouté
   une gouttière de 8px dans la colonne (.gptp-col a `gap: 8px`). */
.gptp-badge { flex-shrink: 0; }
.gptp-badge--violet,
.gptp-col--presence .gptp-col-header,
.gptp-btn-icone { display: none; }

/* ── Boutons actions — ultra-compacts, style Layer3 ── */
.gptp-actions {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: 10px;
}

.gptp-action-btn {
  display: inline-flex;
  width: fit-content;
  align-items: center;
  gap: 12px;
  padding: 6px 10px;
  background: rgba(255,255,255,.02);
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 7px;
  color: rgba(255,255,255,.65);
  font-size: 11px;
  font-weight: 600;
  font-family: 'Inter', sans-serif;
  cursor: pointer;
  text-decoration: none;
  transition: background .18s, border-color .18s, color .18s;
  text-align: left;
}

.gptp-action-btn:hover {
  background: rgba(255,255,255,.07);
  border-color: rgba(255,255,255,.15);
  color: #fff;
}

.gptp-action-btn svg {
  flex-shrink: 0;
  color: rgba(255,255,255,.3);
  transition: color .18s, transform .15s;
}

.gptp-action-btn:hover svg {
  color: rgba(255,255,255,.65);
  transform: translateX(2px);
}

/* ══════════════════════════════════════════════════════════════════════════════
   REFONTE MOBILE DE LA CARTE « SOYEZ PAYÉS POUR JOUER » (≤860px)
   ──────────────────────────────────────────────────────────────────────────────
   ⚠️ TOUT ce qui suit est enfermé dans cette media query, à dessein : le rendu
   BUREAU doit rester identique au pixel — sa grille à 3 colonnes et ses lueurs
   cyan sont volontairement conservées. Rien de neuf au-dessus de 860px.

   Avant : les trois colonnes s'empilaient et les pastilles disparaissaient —
   d'où une carte plate et terne sur téléphone.
   Après : deux colonnes CÔTE À CÔTE dans un dégradé « coucher de soleil », un
   trait horizontal, puis la zone basse sombre avec les boutons.

   ⚠️ Aucun wrapper n'a été introduit. Le DOM portait déjà exactement les cinq
   pièces nécessaires — col · séparateur · col · séparateur · col — et c'est le
   SECOND séparateur qui devient le trait horizontal. On ne fait que replacer
   les enfants existants en grille, ce qui garantit que le bureau, lui, retrouve
   sa disposition d'origine sans condition.
   ══════════════════════════════════════════════════════════════════════════════ */
@media (max-width: 860px) {

  /* ── Le dégradé. Le plus dense en haut, éteint dans la base sombre avant
     d'atteindre le texte — c'est la condition de lisibilité, pas un effet. ── */
  .gptp-container {
    grid-template-columns: 1fr 1px 1fr;
    grid-template-rows: auto auto auto;
    min-height: auto;
    border-radius: 20px;
    /* Les paliers sont calés sur la GÉOMÉTRIE, pas à l'œil : les badges vivent
       entre 3 % et 12 % de la hauteur de carte, les titres commencent vers 30 %.
       Le palier sombre est donc posé à 44 % — la couleur est pleine derrière les
       badges et déjà éteinte quand le texte arrive. C'est la condition de
       lisibilité posée par le brief, pas un réglage esthétique. */
    background:
      linear-gradient(163deg,
        rgba(255,107,43,.95)  0%,
        rgba(255,61,129,.85) 13%,
        rgba(124,77,255,.60) 26%,
        rgba(19,19,28,.95)   44%,
        var(--bg2)           100%),
      var(--bg2);
  }

  /* Les deux lueurs d'angle passent du cyan au chaud. Elles restent purement
     décoratives et hors flux (pointer-events:none hérité des règles de base). */
  .gptp-container::before {
    top: -70px; left: -50px; width: 260px; height: 260px;
    background: radial-gradient(circle,
      rgba(255,107,43,.55) 0%, rgba(255,107,43,.18) 45%, transparent 70%);
  }
  .gptp-container::after {
    top: -80px; right: -60px; width: 280px; height: 280px;
    background: radial-gradient(circle,
      rgba(124,77,255,.5) 0%, rgba(255,61,129,.2) 45%, transparent 70%);
  }

  /* ── Replacement des cinq pièces existantes ── */
  .gptp-col--gagner    { grid-column: 1;      grid-row: 1; }
  .gptp-divider--v     { grid-column: 2;      grid-row: 1; width: 1px; height: auto; }
  .gptp-col--presence  { grid-column: 3;      grid-row: 1; }
  .gptp-divider--h     { grid-column: 1 / -1; grid-row: 2; width: auto; height: 1px; }
  .gptp-col--bottom    { grid-column: 1 / -1; grid-row: 3; }
  .gptp-divider        { background: rgba(255,255,255,.15); }

  /* Badges HAUT (dans le vif), textes BAS (dans le sombre) — comme Layer3. */
  .gptp-col--gagner,
  .gptp-col--presence {
    justify-content: space-between;
    padding: 16px 14px 18px;
    gap: 10px;
    min-height: 190px;
  }
  .gptp-col--gagner .gptp-col-header,
  .gptp-col--presence .gptp-col-header {
    display: flex; flex-direction: column; align-items: flex-start; gap: 0;
  }
  .gptp-badge--violet { display: flex; }

  /* 44px, cercle plein, glyphe blanc. Les `fill` du markup sont des attributs
     de présentation : une déclaration CSS les emporte, sans toucher au SVG. */
  .gptp-badge { width: 44px; height: 44px; }
  .gptp-badge--vert   circle { fill: var(--green); }
  .gptp-badge--vert   path   { fill: #fff; }
  .gptp-badge--violet circle { fill: #7c4dff; }
  .gptp-badge--violet path   { fill: #fff; }

  /* Le français est plus long que l'anglais : on resserre AVANT d'empiler.
     Plancher tenu à 12px — jamais moins, quelle que soit la largeur. */
  .gptp-col--gagner .gptp-col-title,
  .gptp-col--presence .gptp-col-title { font-size: 15px; line-height: 1.3; }
  .gptp-col--gagner .gptp-col-sub,
  .gptp-col--presence .gptp-col-sub {
    font-size: 12px; line-height: 1.45; max-width: none; color: rgba(255,255,255,.55);
  }

  /* ── Zone basse, sur fond sombre ── */
  .gptp-col--bottom { padding: 16px 14px 18px; justify-content: flex-start; }
  .gptp-col--bottom .gptp-col-title { font-size: 15px; }
  .gptp-actions { gap: 8px; margin-top: 12px; }

  /* Boutons : pilules vitrées pleine largeur, chevron poussé à droite.
     44px de haut = la cible tactile de référence (WCAG 2.5.8 en demande 24). */
  .gptp-action-btn {
    width: 100%;
    min-height: 44px;
    gap: 10px;
    padding: 10px 14px;
    background: rgba(255,255,255,.05);
    border: 1px solid rgba(255,255,255,.12);
    border-radius: 12px;
    color: #fff;
    font-size: 13px;
    transition: background .18s, border-color .18s, transform .18s;
  }
  .gptp-action-btn:hover {
    background: rgba(255,255,255,.09);
    border-color: rgba(255,255,255,.24);
    transform: translateY(-1px);
  }
  .gptp-action-btn:focus-visible {
    outline: 2px solid var(--orange);
    outline-offset: 2px;
  }
  .gptp-btn-icone   { display: block; color: rgba(255,255,255,.55); }
  .gptp-btn-chevron { margin-left: auto; }

  /* Variante VISITEUR du §11. Même dessin que les boutons pour que la zone
     basse reste cohérente, mais sans survol ni cible : ce n'est pas une
     commande, c'est une phrase. Le sélecteur reprend `#app.anon-rich` parce que
     la règle du §11 (style.css:2116) pèse (1,1,1) — une classe seule perdrait. */
  #app.anon-rich .gptp-gm-anon {
    width: 100%;
    min-height: 44px;
    gap: 8px;
    padding: 10px 14px;
    background: rgba(255,255,255,.05);
    border: 1px solid rgba(255,255,255,.12);
    border-radius: 12px;
    color: rgba(255,255,255,.7);
    font-size: 13px;
    line-height: 1.35;
    max-width: none;
    margin-top: 0;
  }

  /* Pastilles décoratives : elles étaient PUREMENT ET SIMPLEMENT masquées en
     mobile — la carte y perdait tout relief. On les rétablit, cantonnées à la
     bande colorée du haut et adoucies pour ne pas concurrencer le texte. */
  .gptp-letters { display: block; bottom: auto; height: 165px; opacity: .5; }
}

/* Très petits écrans : on resserre encore plutôt que d'empiler. Les deux
   colonnes restent côte à côte — c'est la demande, et le texte y tient. */
@media (max-width: 400px) {
  .gptp-col--gagner,
  .gptp-col--presence { padding: 14px 11px 16px; min-height: 182px; }
  .gptp-col--gagner .gptp-col-title,
  .gptp-col--presence .gptp-col-title { font-size: 14px; }
  .gptp-badge { width: 38px; height: 38px; }
}

/* ── Campaign Card Lock State — Mode hors connexion (STRICT) ── */
.campaign-locked {
  pointer-events: none !important;
  cursor: default !important;
  opacity: 0.5 !important;
  transition: opacity 0.3s ease-in-out !important;
  position: relative !important;
  user-select: none !important;
}

.campaign-locked * {
  pointer-events: none !important;
  cursor: default !important;
  user-select: none !important;
}

.campaign-locked:hover {
  cursor: default !important;
}

.campaign-locked *:hover {
  cursor: default !important;
}

/* ═════════════════════════════════════════════════════════════════
   ── DESIGN SEAMLESS LAYER3 — Suppression des démarcations ──
   ═════════════════════════════════════════════════════════════════ */

/* Supprimer les borders du Header et Navigation */
nav#landingNav {
  border-bottom: none !important;
  box-shadow: none !important;
}

.app-nav {
  border-bottom: none !important;
  box-shadow: none !important;
}

/* Supprimer les borders du Menu Sidebar */
.side-menu {
  border-right: none !important;
}

.side-header {
  border-bottom: none !important;
}

/* Unifier les backgrounds pour le bloc principal */
body, html {
  background: var(--bg) !important;
}

nav#landingNav,
.app-nav,
.side-menu {
  background: var(--bg) !important;
}

/* Supprimer les borders de la zone principale */
.dt-topbar,
.dt-body,
.dt-left,
.dt-center {
  border: none !important;
  border-bottom: none !important;
  border-top: none !important;
  border-right: none !important;
  border-left: none !important;
  box-shadow: none !important;
}

/* ⛔ SUPPRIMÉ — les deux resets `[class*="logo"]` de la passe « seamless »
   (`border`/`box-shadow`, puis `background`). Ils dataient du premier commit et
   visaient à « libérer le logo de son encadrement ».

   Deux raisons de les retirer plutôt que de les restreindre :

   1. Ils ne protégeaient PLUS RIEN. Mesuré sur leurs cibles NOMMÉES
      (.logo-letter, .anl, .nav-logo, .app-logo) : 9 éléments, 0 changement une
      fois les règles neutralisées. Plus rien ne pose de bordure sur .nav-logo
      ni sur .anl ; .logo-letter et .app-logo n'existent dans AUCUN markup.

   2. `[class*="logo"]` matche une SOUS-CHAÎNE de l'attribut class — donc aussi
      « logout ». Et `background` étant un shorthand, le second reset emportait
      la couleur ET l'image ET les dégradés. Dix habillages y passaient en
      silence : .ls-logo-fallback et .wal-brand-logo (dégradés effacés),
      .qt-badge-logo (son orange), .qt-partner-logo, .dt-logo-area (séparateur),
      .l3cc-sqlogo (socle + ombre), .qc-brand-logo, .qt-sponsor-logo, et
      .btn-logout — rouge plein dans admin.html / merchant-dashboard.html.

   Deux rustines par-classe avaient déjà été posées contre ce joker (.anl en
   6783ea0, .ls-logo-img en 215d25e) ; la seconde est retirée avec lui, plus
   haut dans ce fichier. On n'en ajoute pas de troisième. */

/* Supprimer les borders des sections principales */
.quests-main, footer#landingFooter {
  border-top: none !important;
  border-bottom: none !important;
  box-shadow: none !important;
}

/* Continu des backgrounds */
.quests-main, footer#landingFooter {
  background: var(--bg) !important;
}

/* Supprimer les borders internes des widgets de campagne */
.campaign-hero {
  border: none !important;
  box-shadow: none !important;
}

/* ⛔ SUPPRIMÉ (2026-10-14) — `.campaign-info-block { border:none !important;
   background:#222226 !important }`. Il redisait le fond de la règle de base
   (« Campaign Info Row ») en le VERROUILLANT : en thème clair la carte restait
   #222226 pendant que sa valeur passait à var(--text), quasi noire — 1,21:1,
   mesuré. Le sombre n'en dépend pas : la règle de base pose déjà #222226, et
   aucune bordure n'est déclarée ailleurs (styles calculés identiques, prouvé). */

/* Supprimer les borders des cartes de quête */
/* LANDING-3 passe 2 : .qm-card / .qm-featured retirés de ces listes (balisage
   supprimé avec le bloc #landing de campagne.html). .dt-qcard reste. */
.dt-qcard {
  border: none !important;
}

.dt-qcard:hover {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3) !important;
  border: none !important;
}

/* Supprimer les borders des widgets d'activité */
.act-card-v2,
.dt-activity-card,
.acv2-header,
.acv2-xp-section,
.acv2-section,
.acv2-bottom {
  border: none !important;
  border-bottom: none !important;
}

/* Supprimer les séparations internes */
/* ⚠️ `.dt-list-col .ls-item` a été RETIRÉ de ce groupe. Il y annulait le
   séparateur des colonnes « Nouveau » et « Tendances » — mesuré `0px none` aux
   deux largeurs, alors que DEUX règles plus haut (.ls-item et sa variante
   ≥1024px) le déclaraient. Les lignes ne paraissaient donc pas « trop pâles » :
   elles n'avaient aucun trait du tout, et monter l'opacité de ces deux règles
   n'aurait rien changé tant que ce `!important` restait devant.
   Les deux autres sélecteurs gardent la suppression : elle les concernait. */
.dt-act-row,
.acv2-act-item {
  border-bottom: none !important;
}

/* Supprimer les borders des boutons */
.btn-nav,
.btn-secondary,
.icon-btn {
  border: none !important;
  background: transparent !important;
}

.btn-nav:hover,
.btn-secondary:hover,
.icon-btn:hover {
  background: rgba(255, 107, 43, 0.1) !important;
  border: none !important;
}

/* Supprimer les borders des badges */
.hero-badge,
.league-badge,
.avatar-badge {
  border: none !important;
}

/* Supprimer les box-shadows inutiles sur les éléments principaux */
nav#landingNav, .app-nav, .side-menu, .dt-topbar, .dt-left, .dt-body, .quests-main {
  box-shadow: none !important;
}

.campaign-locked::after {
  content: '🔒';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 36px;
  z-index: 100;
  pointer-events: none;
  text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}

/* ── Verrou admin / level-gate (pointer-events ON → toast au clic) ─────────
   Utilisé quand la campagne est fermée manuellement (is_locked) ou nécessite
   un niveau minimum (min_level). Contrairement à .campaign-locked, le clic
   est autorisé pour qu'un handler puisse afficher le message d'explication. */
.campaign-access-locked {
  position: relative !important;
  opacity: 0.55 !important;
  cursor: not-allowed !important;
}
.campaign-access-locked::after {
  content: '🔒';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 36px;
  z-index: 100;
  pointer-events: none;
  text-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
.campaign-access-locked:hover {
  transform: none !important;
  box-shadow: none !important;
  opacity: 0.55 !important;
}

/* ══════════════════════════════════════════════════════════════════
   QWESTIFY PREMIUM CARDS — Starter card + Image grid
   ══════════════════════════════════════════════════════════════════ */

/* ── Starter card "Pour commencer" (Qwestify onboarding) ─────────── */
.lc-starter {
  position: relative;
  border-radius: var(--radius-lg);
  background: linear-gradient(125deg, #FF6B2B 0%, rgba(255,107,43,.55) 45%, #0F172A 100%);
  border: 1px solid rgba(255,107,43,.22);
  padding: 22px 24px;
  display: flex;
  align-items: center;
  gap: 20px;
  overflow: hidden;
  cursor: pointer;
  transition: transform .25s var(--ease), box-shadow .25s var(--ease);
}
.lc-starter:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 40px rgba(255,107,43,.22);
}
.lc-starter::before {
  content: '';
  position: absolute;
  right: -50px; top: -50px;
  width: 220px; height: 220px;
  border-radius: 50%;
  background: rgba(255,255,255,.04);
  pointer-events: none;
}
.lc-starter-left  { flex: 1; min-width: 0; position: relative; z-index: 1; }
.lc-starter-title {
  font-size: 15px; font-weight: 800; color: #fff;
  letter-spacing: -.2px; margin-bottom: 5px;
}
.lc-starter-sub {
  font-size: 12px; color: rgba(255,255,255,.58);
  line-height: 1.5; max-width: 380px;
}
.lc-starter-right {
  flex-shrink: 0;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  position: relative; z-index: 1;
}
.lc-starter-xp {
  font-size: 24px; font-weight: 900; color: #fff;
  letter-spacing: -1px; line-height: 1;
}
.lc-starter-cta {
  display: inline-flex; align-items: center; gap: 5px;
  background: rgba(255,255,255,.14);
  border: 1px solid rgba(255,255,255,.22);
  border-radius: var(--radius-sm);
  padding: 6px 14px;
  font-size: 12px; font-weight: 700; color: #fff;
  white-space: nowrap; transition: background .18s;
}
.lc-starter:hover .lc-starter-cta { background: rgba(255,255,255,.22); }
.lc-starter-tag {
  display: inline-flex; align-items: center; gap: 4px;
  background: rgba(34,197,94,.14);
  border: 1px solid rgba(34,197,94,.28);
  color: var(--green);
  font-size: 11px; font-weight: 700;
  padding: 2px 9px; border-radius: 20px;
}

/* ── Layer3 Image Grid ──────────────────────────────────────────────── */
.lc-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
.lc-card {
  position: relative;
  height: 240px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  cursor: pointer;
  background: var(--bg3);
  border: 1px solid var(--border);
  transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s;
}
.lc-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 48px rgba(0,0,0,.4);
  border-color: transparent;
}
/* Image de fond — JS injecte background-image via style inline */
.lc-img {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  transition: transform .5s var(--ease), filter .3s ease;
}
/* Fallback gradients par marque (background-image pour ne pas écraser size/position) */
.lc-img--uber     { background-image: linear-gradient(160deg,#0a0a0a,#1a1a1a); }
.lc-img--migros   { background-image: linear-gradient(160deg,#7A2E0A,#C2410C); }
.lc-img--shiroshi { background-image: linear-gradient(160deg,#2E0A4C,#6D28D9); }
.lc-img--hybride  { background-image: linear-gradient(160deg,#021A10,#0D9488); }
.lc-img--vtc      { background-image: linear-gradient(160deg,#050E1F,#2563EB); }
.lc-img--kebab    { background-image: linear-gradient(160deg,#3D1200,#B45309); }
/* Hover : zoom léger + légère hausse de luminosité */
.lc-card:hover .lc-img {
  transform: scale(1.06);
  filter: brightness(1.12);
}
/* Dégradé sombre vers le bas pour lisibilité du texte */
.lc-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(0,0,0,.06) 0%,
    rgba(0,0,0,.15) 35%,
    rgba(0,0,0,.62) 65%,
    rgba(0,0,0,.88) 100%
  );
  pointer-events: none;
}
/* Pastille participants — haut droite */
.lc-top {
  position: absolute;
  top: 12px; right: 12px;
  z-index: 2;
}
.lc-badge-participants {
  display: flex; align-items: center; gap: 5px;
  background: rgba(255,255,255,.1);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,.18);
  border-radius: 20px;
  padding: 4px 10px;
  font-size: 11px; font-weight: 700; color: #fff;
  white-space: nowrap;
}
.lc-badge-participants svg { flex-shrink: 0; opacity: .8; }
/* Zone contenu bas */
.lc-footer {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  padding: 14px 16px 16px;
  z-index: 2;
}
.lc-dates {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600;
  color: rgba(255,255,255,.52);
  margin-bottom: 6px;
}
.lc-dates svg { flex-shrink: 0; }
.lc-partner {
  font-size: 10px; font-weight: 600;
  color: rgba(255,255,255,.48);
  text-transform: uppercase; letter-spacing: .7px;
  margin-bottom: 4px;
}
.lc-reward-row {
  display: flex; align-items: flex-end;
  justify-content: space-between; gap: 8px;
}
.lc-title {
  font-size: 15px; font-weight: 800;
  color: #fff; line-height: 1.2;
  letter-spacing: -.2px;
  flex: 1; min-width: 0;
}
.lc-reward {
  flex-shrink: 0;
  display: inline-flex; align-items: center;
  background: rgba(34,197,94,.15);
  border: 1px solid rgba(34,197,94,.28);
  color: #22C55E;
  font-size: 11px; font-weight: 700;
  padding: 3px 9px; border-radius: 20px;
  white-space: nowrap;
}

/* ── Logo badge partenaire — haut gauche ────────────────────────────── */
.lc-logo-badge {
  position: absolute;
  top: 12px; left: 12px;
  z-index: 2;
  background: rgba(0,0,0,.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,.15);
  border-radius: 7px;
  padding: 4px 9px;
  font-size: 10px; font-weight: 800; color: rgba(255,255,255,.88);
  letter-spacing: .3px;
  white-space: nowrap;
  text-transform: uppercase;
}

/* ── Grille 4-cols "Pour commencer" — 1 carte + 3 espaces vides ─── */
.lc-starter-grid {
  display: grid;
  grid-template-columns: 1fr;
}

/* ── Grille dynamique "Pour commencer" — injectée par onboarding.js ── */
.onboarding-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

/* Icon logo dans les cartes d'onboarding dynamiques */
.ob-icon {
  height: 18px;
  width: auto;
  display: block;
  margin-bottom: 8px;
  opacity: .9;
}

/* Badge type "Quête" — variante violette pour distinguer des campagnes */
.ob-tag-quest {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(124,58,237,.14);
  border: 1px solid rgba(124,58,237,.28);
  color: var(--violet);
  font-size: 11px;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: 20px;
  margin-top: 8px;
}

/* ── Responsive ─────────────────────────────────────────────────────── */
@media (min-width: 1024px) {
  /* Starter : 4 colonnes, carte dans col 1 uniquement */
  .lc-starter-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
  }
  .lc-starter-grid .lc-starter {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    min-height: 170px;
  }
  .lc-starter-grid .lc-starter-left { width: 100%; }
  .lc-starter-grid .lc-starter-sub  { max-width: 100%; font-size: 11.5px; }
  .lc-starter-grid .lc-starter-right {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    width: 100%;
  }

  /* Onboarding dynamique : 3 colonnes côte à côte */
  .onboarding-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
  }

  /* Grid campagnes : exactement 2 colonnes */
  .lc-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }
  .lc-card  { height: 256px; }
}

@media (min-width: 600px) and (max-width: 1023px) {
  /* Onboarding : 2 colonnes sur tablette */
  .onboarding-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
}
@media (max-width: 1023px) and (min-width: 769px) {
  .lc-grid { grid-template-columns: repeat(2, 1fr); gap: 14px; }
  .lc-card { height: 210px; }
}
@media (max-width: 768px) {
  .lc-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .lc-card { height: 188px; }
  .lc-starter { flex-direction: column; align-items: flex-start; gap: 14px; }
  .lc-starter-right { flex-direction: row; align-items: center; gap: 14px; }
  .lc-logo-badge { font-size: 9px; padding: 3px 7px; }
}
@media (max-width: 480px) {
  .lc-grid { grid-template-columns: 1fr; }
  .lc-card { height: 200px; }

  /* Onboarding : 2 colonnes sur mobile (maquette de référence) */
  .onboarding-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }

  /* Grille des cartes de quête (campagne.html) — 2 colonnes sur téléphone.
     Sur un conteneur de 347px, trois pistes ne tiennent pas : une carte ne peut
     pas descendre sous son min-content, la grille débordait donc de l'écran
     (mesuré à 375px : pistes « 223px 138px 0px », scrollWidth 385 pour 347 de
     large, page élargie à 399). Deux colonnes et 8px de gouttière ramènent les
     cartes à ~170px — la cible du mockup — SANS rien retirer de la carte.

     ⚠️ CE QUI GAGNAIT ICI N'ÉTAIT PAS UNE RÈGLE CSS. Mesuré via le protocole
     DevTools (CSS.getMatchedStylesForNode) : SEULES deux règles matchent
     #campaignGrid — « .campaign-grid » de base et celle-ci, toutes deux en
     0-1-0. Ni règle d'id, ni :has(#campaignQuestsTitle) ne touche cette grille.
     Le vainqueur est l'ATTRIBUT STYLE, posé par renderGrid au moment du rendu
     (campagne.html : grid.style.cssText = 'display:grid;
     grid-template-columns:repeat(3,1fr);gap:24px;margin-top:0').
     Un style inline appartient à une origine SUPÉRIEURE du cascade : aucune
     spécificité ne le bat, seul !important le fait — même constat qu'aux
     l.8355 et l.8476. Ce n'est donc pas une surenchère : rien d'autre ne
     revendique ces deux propriétés, on n'écrase aucune règle concurrente. On
     borne au strict nécessaire (les 2 propriétés de la grille) ; display et
     margin-top restent au JS.

     ⚠️ minmax(0, 1fr) ET NON 1fr. « 1fr » vaut « minmax(auto, 1fr) », et ce
     minimum « auto » est le min-content de la carte : une piste 1fr ne descend
     JAMAIS sous le contenu qu'elle porte. Mesuré avec repeat(2, 1fr) : pistes
     « 223.172px 138px » — deux colonnes INÉGALES, calées sur le min-content de
     chaque carte, et la grille débordait encore (scrollWidth 369 pour 347).
     minmax(0, 1fr) supprime ce plancher et rend deux pistes strictement égales
     de 169.5px. Vérifié après coup : aucune carte ne déborde (scrollWidth =
     clientWidth = 170), les deux textes serrés s'ellipsent déjà tout seuls
     (.ob-partner-name l.3302, badge de récompense) — rien n'est coupé net.

     ⚠️ :has(> .ob-card-link) ISOLE LE LAYOUT GRILLE. Le layout « linear »
     (renderList) réutilise le MÊME élément #campaignGrid et le repasse en
     display:flex;flex-direction:column;gap:12px. Sans ce filtre, le
     « gap !important » écraserait aussi ses 12px verticaux. Les cartes de la
     grille sont des <a class="ob-card-link"> (UI_buildQuestCard) ; celles de
     la liste sont des <div> sans classe → le crochet sépare les deux sans
     toucher au JS. Il laisse aussi de côté l'écran de chargement, qui n'a pas
     encore de carte et dont le libellé s'étale déjà en grid-column:1/-1. */
  .campaign-grid:has(> .ob-card-link) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 8px !important;
  }

  /* Cartes onboarding — compactage mobile 2 colonnes */
  .ob-card         { min-height: 240px; }  /* +25% vs 190px */
  .ob-card-content { padding: 12px; gap: 8px; }
  .ob-card-title   { font-size: 14px; }    /* proportionnel au +25% */

  /* Pills — taille réduite pour éviter le débordement */
  .ob-partner-pill,
  .ob-participants-pill,
  .ob-category-pill,
  .ob-status-pill { font-size: 10px; padding: 3px 8px; gap: 4px; }

  /* Texte partenaire — taille mobile (agrandie) */
  .ob-partner-name { font-size: 11px; max-width: 68px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* Footer quête : masque les identicons, garde le compteur texte */
  .ob-identicons { display: none; }

  /* Réduction des éléments footer */
  .ob-xp-badge   { font-size: 11px; padding: 4px 9px; }
  .ob-date-start { font-size: 12px; }
  .ob-date-end   { font-size: 10px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   GRILLE DES CARTES DE QUÊTE — TABLETTES (481→768px), campagne.html

   Même mal et MÊME CAUSE qu'en ≤480px (voir le bloc commenté plus haut) : le
   repeat(3, 1fr) posé en ATTRIBUT STYLE par renderGrid gagne sur toute règle,
   et son plancher min-content rend les pistes inégales. Mesuré à 481px, avant
   ce bloc : « 139.3px 170.4px 139.3px » — trois colonnes INÉGALES et étriquées,
   et la grille débordait de son conteneur de 44px (497 posés dans 453).
   Le remède est donc le même, transposé : !important pour passer devant le
   style inline, minmax(0, 1fr) pour supprimer le plancher, et le même crochet
   :has(> .ob-card-link) qui laisse le layout « linear » (renderList) intact.

   ⚠️ POURQUOI 600px COMME BASCULE — c'est une mesure, pas une habitude.
   Le conteneur vaut « viewport − 28 » sur toute la plage (padding de 14px posé
   par .dt-main:has(#campaignQuestsTitle) .dt-body). Trois colonnes n'atteignent
   le seuil de confort de 160px qu'à partir d'un conteneur de 512px, soit un
   viewport de 540 — en dessous elles retombent à 140-147px, plus étroit encore
   que les 170px du téléphone. Relevé : 481px → 3 col = 140px ✗ / 2 col = 221px.
   On bascule donc à 600px (borne DÉJÀ utilisée par ce fichier, l.3259) plutôt
   qu'au strict minimum de 540 : à 540 les trois colonnes tombent pile sur le
   seuil, à 600 elles démarrent à 175px — confortable pour de vrai.

   ⚠️ 600-768px NE CHANGE PAS D'ASPECT, ET C'EST VOLONTAIRE. Le relevé avant
   correctif est net : la casse s'arrête vers 540. Dès ~570 les trois pistes
   étaient DÉJÀ égales et sans débordement (599px → 174px, 768px → 231px). On
   n'y retouche donc ni le nombre de colonnes ni la gouttière — la règle ne
   déclare QUE grid-template-columns, et le « gap:24px » du style inline
   continue de s'appliquer tel quel. Ce qu'elle apporte est une garantie, pas
   un habillage : sans minmax(0, 1fr), l'égalité des pistes n'y tenait qu'au
   contenu du moment. Les pistes 1fr se calent sur le min-content de la carte
   la plus large (mesuré ici : 170px contre 139px pour ses voisines) ; un titre
   ou un nom d'enseigne plus long relève ce plancher et fait rebasculer la
   plage dans l'inégalité, exactement comme à 481px. minmax(0, 1fr) supprime le
   plancher : l'égalité ne dépend plus des données.

   Résultat mesuré : 481-599px → 2 colonnes de 221 à 280px (contre 3 pistes
   inégales de 139px qui débordaient de 44px) · 600-768px → 3 colonnes de 175 à
   236px, au pixel près comme avant. Au-delà de 768px RIEN N'EST TOUCHÉ :
   le desktop garde ses 3 × ~307px et sa gouttière de 24px.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 481px) and (max-width: 599px) {
  .campaign-grid:has(> .ob-card-link) {
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 12px !important;
  }
}

@media (min-width: 600px) and (max-width: 768px) {
  /* Pas de « gap » ici : celui du style inline (24px) est déjà le bon. */
  .campaign-grid:has(> .ob-card-link) {
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  }
}

/* ===================================================================
   CARTES ONBOARDING — Campagnes & Quêtes (injectées par onboarding.js)
   =================================================================== */

/* Enveloppe lien SEO-friendly — neutralise les styles navigateur */
.ob-card-link {
  text-decoration: none;
  color: inherit;
  display: block;
  height: 100%;
}

/* Base commune */
.ob-card {
  position: relative;
  border-radius: var(--radius-lg, 16px);
  overflow: hidden;
  cursor: pointer;
  height: 100%;
  min-height: 190px;   /* densité : −31% vs 275px */
  display: flex;
  flex-direction: column;
  transition: transform .25s var(--ease, ease), box-shadow .25s var(--ease, ease);
}
/* Contour net. Les deux bordures d'origine étaient à la limite du visible sur
   le fond #0E0E14 — rgba(255,255,255,.07) pour la quête, rgba(255,107,43,.2)
   pour la campagne : les cartes se fondaient dans la page. On monte le liseré
   clair et on le double d'un anneau sombre EXTÉRIEUR (première ombre) : le
   contraste ne dépend alors plus de la couleur de fond de la carte, qui varie
   avec la teinte de campagne. Porté du bac à sable (mode « Actuel »). */
.ob-card {
  border: 1px solid rgba(255,255,255,.28);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 4px 16px rgba(0,0,0,.40);
}
.ob-card:hover {
  transform: translateY(-3px);
  border-color: rgba(255,255,255,.42);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 10px 26px rgba(0,0,0,.52);
}
.ob-card-content {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 9px;
  gap: 6px;
}
.ob-card-top {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: 19px;
}
.ob-card-title {
  margin: 0;
  font-size: 13px;   /* densité : −24% vs 17px */
  font-weight: 800;
  letter-spacing: -.3px;
  line-height: 1.3;
}
/* Le pied porte 3 à 4 enfants — « Nouveau » (77px), « Sponsorisé » (81px),
   « Live » (55px, en flex-shrink:0) et la rangée de pastilles (76px) — pour
   154px disponibles sur une carte de 196px. Déficit mesuré : 70px, et 159px
   sur une carte sponsorisée. Aucun des quatre ne peut rétrécir : les libellés
   butent sur leur mot, les pastilles sont des images de 34px. Le dernier
   enfant sortait donc du pied et .ob-card (overflow:hidden) le tranchait —
   16 pastilles invisibles et 13 coupées en deux, mesurées à 1280px.
   Le pied passe donc à la ligne quand il ne tient pas. Il ne wrappe QUE si
   nécessaire : au-delà de ~1600px tout reste sur une seule ligne, comme avant. */
.ob-card-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* ── Bannière standardisée — référence architecture image pour toutes les cartes ── */
/* Couvre la totalité de la carte parent (position: relative; overflow: hidden requis) */
.card-banner-container {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  /* cover : la bannière REMPLIT son cadre. `contain` la laissait entière entre
     deux bandes de --bg2, ce vide que le lot « design au live » supprime. La
     contrepartie est un rognage, assumé. Le fond sombre reste en filet pour les
     rares images à alpha. */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-color: var(--bg2, #13131C);
  object-fit: cover;
  object-position: center;
  z-index: 1;
  pointer-events: none;
  /* Rehaussement luminosité de base — effet photo vibrante */
  filter: brightness(1.3) saturate(1.15);
  transition: filter 0.3s ease;
}
/* ── Cartes de QUÊTE (grille « Pour commencer ») — image en cover ──────────
   Surcharge le `background-size:contain` hérité de .card-banner-container,
   UNIQUEMENT pour les cartes de quête : les bannières panoramiques remplissent
   la carte (léger rognage) au lieu d'être letterboxées (bandes vides).
   Les cartes campagne (.ob-campaign-card) restent en `contain` (inchangé). */
.ob-quest-card .card-banner-container { background-size: cover; }
/* Hover : luminosité poussée à 1.3 (appliqué via parent .ob-card:hover) */
.ob-card:hover .card-banner-container {
  filter: brightness(1.3) contrast(1.1);
}
/* Gradient overlay générique (quêtes + fallback) : sombre vers le bas */
.card-banner-container::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5));
  pointer-events: none;
}

/* Gradient campagne : double sceau sombre haut + bas — laisse le centre respirer
   Haut (0%) sombre → lisibilité bulle partenaire + titre remontés en haut
   Milieu (38-62%) transparent → image visible, espace graphique libre
   Bas (100%) sombre → lisibilité du footer date/statut */
.ob-campaign-card .card-banner-container::after {
  background: linear-gradient(to bottom,
    rgba(0,0,0,.35) 0%,
    rgba(0,0,0,.05) 38%,
    rgba(0,0,0,.05) 62%,
    rgba(0,0,0,.45) 100%
  );
}

.ob-quest-card .card-banner-container::after {
  background: linear-gradient(to top, #0E0E14 0%, rgba(14,14,20,0.9) 20%, transparent 68%, rgba(0,0,0,.18) 100%);
}

/* Carte Campagne — gradient fallback quand aucune image_url n'est fournie */
.ob-campaign-card {
  background: linear-gradient(135deg, var(--bg3) 0%, rgba(255,107,43,.12) 100%);
  border: 1px solid rgba(255,107,43,.2);
  min-height: 300px;   /* +25% vs 240px */
}

/* ── Placeholder campagne — affiché quand image_url est null/absent ─────────
   Remplace le fond noir (absent de card-banner-container) par un dégradé
   de marque orange/sombre qui maintient l'overlay ::after et la lisibilité. */
.ob-card-placeholder {
  background: linear-gradient(135deg,
    #0E0E14 0%,
    rgba(255,107,43,.12) 55%,
    #1C1C28 100%
  );
}

/* ── Banner quête — gradient violet de marque, structure identique campagnes ─
   Les quêtes n'ont pas d'image_url : ce banner assure le même rendu visuel
   (overlay ::after, brightness, arrondis, hauteur) que les cartes campagne. */
.ob-quest-banner {
  background: linear-gradient(135deg,
    rgba(124,58,237,.28) 0%,
    #0E0E14 45%,
    rgba(124,58,237,.10) 100%
  );
}

/* Corps commun campagne + quête — flex:1 remplit l'espace, justify-end ancre en bas */
.ob-campaign-body,
.ob-quest-body {
  position: relative;
  z-index: 10;
  display: flex;
  flex-direction: column;
  flex: 1;
  justify-content: flex-end;
  padding: 20px;   /* +25% vs 16px pour respirer dans les cartes plus hautes */
  gap: 10px;
}

/* ── Campagne : bulle partenaire + titre remontés en haut ──────────────────
   Limité à ≥481px : sur mobile 2-colonnes (~180px/carte), le padding-right:80px
   laisserait trop peu d'espace. La règle mobile dans @media(max-width:480px)
   revert à flex-end, mais la cascade CSS exige que cette règle soit protégée
   par min-width pour ne pas être écrasée par les règles générales.
   ── */
@media (min-width: 481px) {
  .ob-campaign-body {
    justify-content: flex-start;
    padding-top: 12px;
    padding-right: 80px;
    padding-bottom: 0;
  }
}

/* Pastille participants ancrée haut-droite — styles pill inclus */
.ob-camp-pax {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 9999px;
  background: rgba(255,255,255,.12);
  border: 1px solid rgba(255,255,255,.2);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* Titre campagne — blanc forcé + text-shadow renforcé (zone haute, gradient léger) */
.ob-campaign-card .ob-card-title {
  color: #fff;
  text-shadow: 0 1px 8px rgba(0,0,0,.85), 0 2px 24px rgba(0,0,0,.6);
}
.ob-campaign-card .ob-partner-name  { color: rgba(255,255,255,.95); }

/* Quête — le corps occupe le bas de la carte, sous le socle de l'emblème.
   L'ancien « min-height:280px » a disparu avec le fond plein cadre : la hauteur
   se répartit désormais entre le socle (fixe) et le corps (flex:1). */
.ob-quest-card .ob-quest-body {
  justify-content: flex-end;
  padding: 12px;
  gap: 7px;
}

/* Footer quête — pleine largeur, logos alignés droite, la barre au-dessus gère le margin-top:auto */
.ob-quest-body .ob-card-footer {
  width: 100% !important;
  display: flex !important;
  justify-content: space-between !important;
  align-items: center !important;
  margin-top: 0 !important;
}

/* Footer campagne — enfant direct de .ob-campaign-card, au-dessus du banner (z-index: 10) */
.ob-campaign-card > .ob-card-footer {
  position: relative;
  z-index: 10;
  display: flex !important;
  justify-content: space-between !important;
  align-items: center !important;
  width: 100% !important;
  background-color: rgba(0,0,0,.2) !important;
  padding: 14px 16px !important;
  border-bottom-left-radius: 12px !important;
  border-bottom-right-radius: 12px !important;
  border-top: 1px solid rgba(255,255,255,.1) !important;
  flex-shrink: 0 !important;
}

/* ══════════════════════════════════════════════════════
   BLOC DATE LAYER3 STYLE — Campaign cards
   Widget calendrier (mois/jour) + plage de dates
   ══════════════════════════════════════════════════════ */
.ob-date-block {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border2);
  border-radius: 12px;
  padding: 8px 12px;
  flex: 1;
  min-width: 0;
}
[data-theme="light"] .ob-date-block {
  background: var(--bg3);
}

.ob-cal-widget {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--border2);
  border-radius: 8px;
  padding: 5px 9px;
  min-width: 38px;
  flex-shrink: 0;
  gap: 1px;
}
[data-theme="light"] .ob-cal-widget {
  background: var(--bg2);
}

.ob-cal-month {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.8px;
  color: var(--orange);
  text-transform: uppercase;
  line-height: 1;
}

.ob-cal-day {
  font-size: 20px;
  font-weight: 900;
  line-height: 1;
  color: var(--text);
  letter-spacing: -1px;
}

.ob-date-range {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.ob-date-start {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ob-date-end {
  font-size: 11px;
  font-weight: 500;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Carte Quête — fond calme : un dégradé sombre, teinté par l'accent en haut,
   sur lequel l'emblème et sa lueur peuvent se détacher. L'ancien fond
   photographique plein cadre a laissé la place au socle (lot c2) : une photo,
   une lueur et un emblème sur la même surface se disputaient l'attention. */
.ob-quest-card {
  background:
    radial-gradient(125% 78% at 50% 0%,
      color-mix(in srgb, var(--qc-accent) 13%, transparent) 0%, transparent 62%),
    linear-gradient(180deg, #17171F 0%, #101018 52%, #0B0B11 100%);
  border: 1px solid rgba(255,255,255,.07);
  min-height: 300px;  /* unifié avec .ob-campaign-card — grille homogène */
}

/* Titre quête — deux lignes pleines, jamais davantage.
   ⚠️ PAS de -webkit-line-clamp ici, et c'est un choix. line-clamp pose son
   ellipse au dernier CARACTÈRE qui rentre : « particulièrement » devient
   « particuli… », soit exactement la coupure en plein mot que ce lot interdit.
   On borne donc la hauteur à deux interlignes et on masque le reste : le
   retour à la ligne se faisant aux frontières de mots, on ne voit jamais que
   des mots entiers. Contrepartie assumée : pas de « … » de signalement. */
.ob-quest-card .ob-card-title {
  color: #fff;
  line-height: 1.28;
  max-height: 2.56em;        /* 2 × 1.28 */
  overflow: hidden;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

/* Pastille catégorie — ancrée en BAS-GAUCHE du socle (lot c2).
   Elle était en haut à droite, où elle chevauchait le bouton cœur : sur une
   carte de 170px, « Entrée libre » et le cœur se disputaient le même coin.
   Les trois repères occupent désormais trois angles distincts : statut en
   haut-gauche, cœur en haut-droite, catégorie en bas-gauche du socle. */
.ob-quest-card .qc-plinth > .ob-category-pill {
  position: absolute;
  top: auto;
  bottom: 10px;
  left: 10px;
  right: auto;
  z-index: 10;
  max-width: calc(100% - 20px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Pastille statut (Terminé/Live/Bientôt) — elle a quitté le pied pour la
   rangée de repères, aux côtés de la distance (lot c2). Le margin-left:auto
   qui la collait à droite du pied n'a donc plus lieu d'être : sur la rangée,
   elle suit la distance de gauche à droite. */
.ob-quest-card .ob-status-pill {
  position: static;
  margin-left: 0;
  align-self: center;
}

/* Bouton cœur — haut-droite, invisible par défaut, révélé au hover */
/* ── LE CŒUR, SANS DISQUE (porté du bac à sable, mode « Actuel ») ────────────
   Le rond sombre disparaît ; restent le tracé et son ombre portée. Cette ombre
   n'est pas un ornement : depuis que le socle porte le logo à PLEIN CADRE (plus
   bas), un cœur blanc peut tomber sur un logo blanc — Café du grain, Kyutech.
   `drop-shadow` suit le CONTOUR du tracé, là où une ombre de boîte aurait
   redessiné le disque qu'on vient justement de retirer.

   ⚠️ IL EST DÉSORMAIS TOUJOURS VISIBLE. La règle d'avant l'ouvrait au survol
   (`opacity:0` + `.ob-quest-card:hover`), ce qui, au doigt, voulait dire jamais
   — d'où le correctif `@media (hover:none)` plus bas, devenu sans objet mais
   conservé (il ne fait que répéter l'opacité pleine).
   Le comportement d'écriture ne change pas : le gestionnaire délégué de
   shared.js reste seul maître du favori. */
.ob-quest-heart {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 12;
  background: none;
  border: none;
  border-radius: 0;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 1;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.8));
  transition: transform .18s ease;
}
.ob-quest-card:hover .ob-quest-heart {
  opacity: 1;
}
.ob-quest-heart:hover {
  background: none;
  transform: scale(1.12);
}
.ob-quest-heart svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: #fff;
  stroke-width: 2.1;
}
/* Quête likée — cœur rouge plein. */
.ob-quest-heart.is-liked svg { fill: #EF4444; stroke: #EF4444; }

/* Le « pop » au clic. Le léger sous-passage à 94 % donne la sensation de
   ressort : un simple aller-retour 1 → 1.25 → 1 paraît mécanique.
   La classe est posée puis retirée par le gestionnaire de shared.js. */
@keyframes gpHeartPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.25); }
  65%  { transform: scale(.94); }
  100% { transform: scale(1); }
}
.ob-quest-heart.gp-pop { animation: gpHeartPop .34s ease; }
@media (prefers-reduced-motion: reduce) {
  .ob-quest-heart { transition: none; }
  .ob-quest-heart.gp-pop { animation: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARTE QUÊTE — L'EMBLÈME (lot c2)

   Le logo partenaire était un filigrane à opacity:.12 posé derrière le texte :
   présent dans le DOM, invisible à l'œil. Il devient ici le sujet de la carte —
   plein, sur un socle, avec une lueur d'accent derrière lui.

   ⚠️ LA TEINTE EST UNE DONNÉE, PAS UNE MISE EN FORME. --qc-accent est posée
   par UI_buildQuestCard en custom property (même principe que les barres d'XP
   du tunnel, §9) et n'est émise QUE si la campagne porte un color_theme.
   Partout ailleurs aucun attribut style n'est écrit et c'est le défaut
   ci-dessous — l'orange Qwestify — qui s'applique. Aujourd'hui seul
   campagne.html transmet colorTheme : la majorité des cartes montrent donc la
   lueur Qwestify, et c'est le comportement attendu.
   ═══════════════════════════════════════════════════════════════════════════ */
.ob-quest-card {
  --qc-accent: var(--orange, #FF6B2B);
  /* La lueur n'utilise PAS l'accent brut. UI_buildGradient3 retient le premier
     hex de la color_theme, souvent le stop SOMBRE du dégradé de campagne
     (mesuré : « …#7F1D1D, #DC2626 » donne #7F1D1D). Un halo de rouge sombre sur
     fond noir ne se voit pas. On remonte donc la luminance en mélangeant vers
     le blanc, ce qui préserve la teinte — seule la clarté change. */
  --qc-glow-color: color-mix(in srgb, var(--qc-accent) 62%, #FFFFFF);
}

/* Le socle occupe le haut de la carte ; le corps prend exactement la place de
   son contenu. Le socle est ÉLASTIQUE (flex:1) : c'est lui qui absorbe la
   hauteur libre, sinon le trop-plein s'ouvrait entre l'emblème et le texte —
   mesuré : jusqu'à 50px de vide sur les cartes les moins garnies. */
.qc-plinth {
  position: relative;
  z-index: 2;
  flex: 1 1 auto;
  min-height: 118px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;          /* la lueur est floutée : elle ne doit pas baver */
  border-radius: var(--radius-lg, 16px) var(--radius-lg, 16px) 0 0;
}

/* La lueur — un halo radial de la teinte d'accent, derrière l'emblème. */
.qc-glow {
  position: absolute;
  top: 50%; left: 50%;
  width: 150px; height: 150px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--qc-glow-color) 55%, transparent) 0%,
    color-mix(in srgb, var(--qc-glow-color) 20%, transparent) 42%,
    transparent 70%);
  filter: blur(14px);
  pointer-events: none;
}

/* Le socle de l'emblème — plaque translucide, liseré teinté par l'accent. */
.ob-quest-center-icon {
  position: relative;
  z-index: 2;
  width: 88px; height: 88px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 24px;
  background: linear-gradient(158deg, rgba(255,255,255,.13), rgba(255,255,255,.03));
  border: 1px solid color-mix(in srgb, var(--qc-accent) 34%, rgba(255,255,255,.10));
  box-shadow: 0 10px 26px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.12);
}
.ob-quest-icon-img {
  display: block;
  width: 60px; height: 60px;
  object-fit: contain;
}

/* ── LE LOGO REMPLIT SON SOCLE (porté du bac à sable, mode « Actuel ») ───────
   Avant : 60px d'image, dans une plaque de 88px, dans un socle d'au moins
   118px — le logo occupait environ un SEPTIÈME de la surface qui lui était
   réservée, le reste était du vide. On dissout la plaque et on porte l'image au
   socle entier, en `cover` : elle remplit sans jamais se déformer. La
   contrepartie est un ROGNAGE, assumé : un logo carré sur un socle en paysage
   perd du haut et du bas.

   ⚠️ `:has(.ob-quest-icon-img)` n'est pas un raffinement, c'est le garde-fou.
   Sans lui, une carte SANS logo — l'initiale de l'enseigne, ou une image dont
   le `onerror` de shared.js a retiré la balise — verrait sa plaque dissoute et
   son initiale flotter dans un cadre vide. Le repli garde son rendu d'origine. */
.qc-plinth:has(.ob-quest-icon-img) .ob-quest-center-icon {
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.qc-plinth:has(.ob-quest-icon-img) .ob-quest-icon-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}
/* La lueur d'accent vivait DERRIÈRE le logo, pour l'en détacher. Une image
   pleine surface la couvre entièrement : elle ne coûterait plus qu'un `blur`
   par carte, sans rien montrer. */
.qc-plinth:has(.ob-quest-icon-img) .qc-glow { display: none; }

/* Voile de lisibilité. Les trois repères posés SUR le socle — statut en haut,
   cœur en haut-droite, catégorie en bas-gauche — comptaient sur un fond sombre
   maîtrisé ; ils tombent désormais sur une photo quelconque, qui peut être
   claire. Le dégradé n'assombrit que les deux bords où ils vivent et laisse le
   centre de l'image intact.
   z-index 3 : au-dessus de l'image (2), sous la pastille catégorie (10). */
.qc-plinth:has(.ob-quest-icon-img)::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(0,0,0,.38) 0%, transparent 32%,
    transparent 60%, rgba(0,0,0,.46) 100%);
}
/* Sans logo : l'initiale de l'enseigne, jamais une plaque vide. */
.qc-emblem-initial {
  font-size: 34px;
  font-weight: 800;
  line-height: 1;
  color: color-mix(in srgb, var(--qc-accent) 70%, #fff);
}

/* Pastille de statut — UNE SEULE, en haut à gauche, au-dessus du socle. */
/* ── BANDEROLE D'ANGLE (portée du bac à sable, mode « Actuel ») ──────────────
   La pastille devient une bande diagonale au coin haut-GAUCHE. Le cœur tient le
   coin haut-droit : les deux angles sont occupés sans jamais se disputer la
   place. Le débord est rogné par `.ob-card { overflow:hidden }` — c'est ce
   rognage qui en fait un triangle d'angle propre, épousant l'arrondi.

   ⚠️ LA POSITION DÉCIDE DE LA LONGUEUR LISIBLE, PAS `width`. Le débord étant
   rogné, la portion visible est la CORDE du triangle : pour une bande centrée
   en (cx, cy) depuis le coin, elle vaut √2·(cx + cy). À top 15 / left −36 cela
   donnait 76px pour un « SPONSORISÉ » qui en réclame 72 — le É se faisait
   trancher. Élargir `width` n'y change rien (les deux bouts sortent seulement
   plus loin du cadre) : il faut ÉLOIGNER la bande du coin. Ici 91px de corde,
   ~19px de marge. Mesuré.

   z-index 13 : au-dessus du voile du socle (3) et de la pastille catégorie (10).
   Une seule banderole par carte, garantie par le MARKUP : shared.js rend
   « Sponsorisé » OU « Nouveau » (if / else if), jamais les deux. */
.qc-chip {
  position: absolute;
  top: 20px;
  left: -30px;
  z-index: 13;
  width: 132px;
  padding: 4px 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border: none;
  border-radius: 0;
  font-size: 9px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: .10em;
  text-transform: uppercase;
  white-space: nowrap;
  transform: rotate(-45deg);
  box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
/* Code couleur par TYPE de mention : ambre pour le commercial, vert pour la
   fraîcheur. Deux teintes de la palette de marque — jamais le doré, qui
   appartient à la variante Casino du bac à sable et n'a rien à faire ici. */
.qc-chip-sponsored {
  background: linear-gradient(135deg, #F5B451, #E08A1E 52%, #A85F0D);
  color: #2A1708;
}
.qc-chip-new {
  background: linear-gradient(135deg, #5EE894, #22C55E 52%, #14803D);
  border: none;
  color: #04250F;
}

/* Rangée enseigne — petit logo + nom, au-dessus du titre. */
.qc-brand {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;              /* autorise l'ellipse du nom (cf. .ob-partner-name) */
}
.qc-brand-logo {
  width: 18px; height: 18px;
  border-radius: 50%;
  object-fit: contain;
  background: rgba(255,255,255,.08);
  flex-shrink: 0;
}

/* Rangée repères — distance et fenêtre de diffusion (Live / Bientôt / Terminé). */
.qc-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

/* Traits de progression — barre fine et segmentée. */
.ob-progress-dashes {
  display: flex;
  gap: 4px;
  width: 100%;
  position: relative;
  z-index: 5;
}
.ob-dash {
  flex: 1;
  height: 3px;
  border-radius: 3px;
  background-color: rgba(255,255,255,.18);
}
/* Trait d'étape rempli (quête complétée / progression) */
.ob-dash.ob-dash-done { background-color: var(--green); }

/* Pied — participants à gauche */
.ob-pax-block {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text2, rgba(255,255,255,.6));
  min-width: 0;
}
.ob-pax-block strong {
  font-size: 13px;
  font-weight: 800;
  color: var(--text, #fff);
}

/* Pied — badges à droite : XP toujours, puis cadeau (bon à gagner) ou « Q ». */
.quest-rewards-icons {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: flex-end;
  /* Ne rétrécit jamais : ses enfants ont une taille fixe, une largeur
     « négociée » ne ferait que les tronquer. margin-left:auto les garde à
     DROITE même si le pied passe à la ligne.
     ⚠️ NE PAS y ajouter min-width:0 « pour aider » : mesuré au lot précédent,
     la rangée tombe alors à 0px de large et pose ses badges HORS de la carte. */
  flex-shrink: 0;
  margin-left: auto;
}
.qc-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px; height: 26px;
  border-radius: 50%;
  flex-shrink: 0;
  background: rgba(255,255,255,.08);
  border: 1px solid rgba(255,255,255,.12);
  color: #fff;
}
.qc-badge svg { width: 14px; height: 14px; }

/* ── Badges de récompense : les LOGOS livrés, sur un disque neutre ───────────
   L'XP et le cadeau portent désormais leurs vrais logos (assets/images/badges/,
   WebP à alpha, 4 et 5 Ko). Le disque redevient NEUTRE : les teintes color-mix
   d'avant (accent de la carte pour l'XP, vert pour le cadeau) posaient une
   couleur DERRIÈRE un logo qui porte déjà les siennes — les deux se disputaient
   le même rond de 26px.

   `background-image` plutôt qu'un <img> : la règle s'applique d'elle-même aux
   cartes injectées après coup, sans mutation du DOM à chaque rendu, et elle
   couvre les DEUX moteurs qui écrivent ce markup (shared.js et quest-lists.js)
   d'un seul geste. La feuille de style est la seule autorité sur l'apparence.

   15px, c'est l'encombrement optique exact des tracés remplacés
   (`.qc-badge svg` = 14px) : ni le rond, ni son anneau, ni la rangée ne bougent.

   ⚠️ `font-size:0` n'est pas un ornement : le « XP » est un NŒUD TEXTE DIRECT
   du span, qu'aucun `svg{display:none}` ne peut atteindre. Le chiffre, lui, n'a
   jamais vécu ailleurs que dans l'aria-label — l'annonce vocale est intacte.

   ⚠️ Il n'y a plus de `.qc-badge-q` : sans bon, la carte ne montre AUCUN second
   badge. La classe a disparu des trois moteurs de rendu, pas seulement du CSS. */
.qc-badge-xp,
.qc-badge-gift {
  background-color: rgba(255,255,255,.06);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 15px;
  border-color: rgba(255,255,255,.16);
}
.qc-badge-xp {
  font-size: 0;
  background-image: url('assets/images/badges/xp.webp');
}
.qc-badge-gift {
  background-image: url('assets/images/badges/cadeau.webp');
}
.qc-badge-gift svg { display: none; }

/* Nom de l'enseigne — il tient sur la largeur restante et s'ellipse au besoin.
   Spécificité 0-2-0 : elle passe DEVANT le « .ob-partner-name{max-width:68px} »
   du bloc ≤480px, qui appartient à la pastille partenaire des cartes campagne.
   68px sur la rangée enseigne tronquaient « Café du grain » en « Café du… ». */
.ob-quest-card .ob-partner-name {
  font-size: 11px;
  font-weight: 600;
  color: var(--text2, rgba(255,255,255,.62));
  max-width: none;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Téléphone — la carte se resserre. À 170px de large, 300px de haut laissaient
   ~53px de vide entre l'emblème et l'enseigne (mesuré) : de l'espace mort,
   pas de la respiration. 268px donnent un portrait de proportion 1:1,58. */
@media (max-width: 480px) {
  .ob-quest-card { min-height: 268px; }
  .qc-plinth { min-height: 104px; }
  .ob-quest-center-icon { width: 78px; height: 78px; border-radius: 21px; }
  .ob-quest-icon-img { width: 52px; height: 52px; }
  .qc-glow { width: 128px; height: 128px; }
  .ob-quest-card .ob-quest-body { padding: 10px; gap: 6px; }
}

/* Bouton cœur — cible tactile de 40px sans grossir le rond visible de 30px,
   par un pseudo-élément (même technique que les points du carrousel, l.8390).
   ⚠️ Au doigt, « révélé au survol » veut dire « jamais révélé » : sur un
   pointeur grossier le cœur est donc visible en permanence, à demi-opacité. */
.ob-quest-heart::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 40px; height: 40px;
  transform: translate(-50%, -50%);
}
@media (hover: none) {
  .ob-quest-heart { opacity: .75; }
  .ob-quest-heart.is-liked { opacity: 1; }
}

/* Pills — base glassmorphism */
.ob-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 9999px;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
  white-space: nowrap;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* Pastille partenaire — +30% vs taille d'origine (agrandissement proportionnel aux cartes) */
.ob-partner-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border-radius: 9999px;
  font-size: 11px;
  font-weight: 700;
  padding: 6px 13px;
  background: rgba(255,255,255,.12);
  border: 1px solid rgba(255,255,255,.2);
  color: #fff;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  width: fit-content;
}
.ob-partner-name { font-size: 14px; font-weight: 700; }

/* Pastille participants — haut-droite campagne */
.ob-participants-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 9999px;
  background: rgba(255,255,255,.12);
  border: 1px solid rgba(255,255,255,.2);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* Pastille catégorie — haut-droite quête */
.ob-category-pill {
  display: inline-flex;
  align-items: center;
  border-radius: 9999px;
  background: rgba(124,58,237,.18);
  border: 1px solid rgba(124,58,237,.32);
  color: var(--violet);
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
}
/* Pastille « Sponsorisé par Qwestify » — dans le flux de la rangée du bas (dorée) */
.ob-sponsored-pill{display:inline-flex;align-items:center;border-radius:9999px;
  background:#BA7517;color:#fff5de;font-size:11px;font-weight:700;
  padding:4px 10px;white-space:nowrap;line-height:1}
/* Pastille distance « Autour de moi » — info neutre, dans le flux de la rangée du bas */
.ob-distance-pill{display:inline-flex;align-items:center;gap:3px;border-radius:9999px;
  background:rgba(255,255,255,0.08);color:#c5c5cf;font-size:11px;font-weight:600;
  padding:4px 10px;white-space:nowrap;line-height:1}

/* Pastille statut — footer droite campagne */
.ob-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 9999px;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
  flex-shrink: 0;
}
.ob-status-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* ── États automatiques (calculés depuis date_debut / date_fin) ── */
.ob-status-active {
  background: rgba(34, 197, 94, 0.12);
  border: 1px solid rgba(34, 197, 94, 0.3);
  color: var(--green);
}
.ob-status-active .ob-status-dot {
  background: var(--green);
  box-shadow: 0 0 6px var(--green);
  animation: blink 2s ease-in-out infinite;
}

.ob-status-soon {
  background: rgba(245, 158, 11, 0.12);
  border: 1px solid rgba(245, 158, 11, 0.3);
  color: var(--yellow);
}
.ob-status-soon .ob-status-dot {
  background: var(--yellow);
}

.ob-status-inactive {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  color: var(--text3);
}
.ob-status-inactive .ob-status-dot {
  background: var(--text3);
}

/* Traits de progression — quête */
.ob-task-dashes { display: flex; gap: 5px; }
.task-dash {
  height: 3px;
  flex: 1;
  border-radius: 9999px;
  background: var(--border, rgba(255,255,255,.14));
}

/* Footer quête : participants + XP */
.ob-participants-row  { display: flex; align-items: center; gap: 8px; }
.ob-identicons        { display: flex; }
.ob-identicon {
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid var(--bg2);
  flex-shrink: 0;
}
.ob-identicon:nth-child(1) { background: linear-gradient(135deg, var(--orange), var(--violet)); }
.ob-identicon:nth-child(2) { background: linear-gradient(135deg, var(--violet), var(--green)); margin-left: -8px; }
.ob-participants-info  { display: flex; flex-direction: column; line-height: 1.25; }
.ob-participants-count { font-size: 13px; font-weight: 800; color: var(--text, #fff); }
.ob-participants-label { font-size: 10px; color: var(--text3, rgba(255,255,255,.4)); text-transform: uppercase; letter-spacing: .4px; }
.ob-xp-badge {
  display: inline-flex;
  align-items: center;
  background: rgba(245,158,11,.14);
  border: 1px solid rgba(245,158,11,.3);
  color: var(--yellow);
  font-size: 13px;
  font-weight: 900;
  padding: 5px 12px;
  border-radius: 9999px;
  letter-spacing: -.2px;
  flex-shrink: 0;
}

/* Mise à l'échelle typographique — carte "Mon activité" */

/* Texte d'activité récente (déjà forcé, consolidé ici) */
#app .acv2-activity .acv2-act-text {
  font-size: 16px !important;
  line-height: 1.5 !important;
  color: var(--text) !important;
  font-weight: 500 !important;
}
#app .act-card-v2 .acv2-act-xp {
  font-size: 13px !important;
}
#app .act-card-v2 .acv2-act-item {
  padding: 10px 0 !important;
  gap: 10px !important;
}

/* Surtitres uppercase (XP de Saison, Progression de saison) */
#app .act-card-v2 .acv2-xp-label,
#app .act-card-v2 .acv2-section-title {
  font-size: 14px !important;
  letter-spacing: 1px !important;
  margin-bottom: 10px !important;
}

/* Sous-labels compacts du bloc inférieur (Série GM, Fin de saison) */
#app .act-card-v2 .acv2-bottom-label {
  font-size: 12px !important;
  letter-spacing: 0.5px !important;
}

/* Valeurs du bloc inférieur */
#app .act-card-v2 .acv2-bottom-val {
  font-size: 16px !important;
}

/* Labels et valeurs des lignes de stats */
#app .act-card-v2 .acv2-row-label {
  font-size: 14px !important;
}
#app .act-card-v2 .acv2-row-val {
  font-size: 14px !important;
}

/* Indication XP suivant ("/ 500 XP") */
#app .act-card-v2 .acv2-xp-next {
  font-size: 13px !important;
}

/* Lien "En savoir plus" */
#app .act-card-v2 .acv2-learn {
  font-size: 13px !important;
}

/* ════════════════════════════════════════════════════════
   SHARE DROPDOWN
════════════════════════════════════════════════════════ */
.share-container {
  position: relative;
  display: inline-flex;
}

.share-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 180px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 6px;
  z-index: 500;
  box-shadow: 0 8px 32px rgba(0,0,0,.45);
}
.share-dropdown.open { display: block; }

.share-dropdown-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 12px;
  border: none;
  border-radius: 8px;
  background: none;
  color: var(--text2);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  text-align: left;
  transition: background .15s, color .15s;
}
.share-dropdown-item:hover {
  background: var(--bg3);
  color: var(--text);
}
.share-dropdown-item svg { flex-shrink: 0; opacity: .8; }

.share-dropdown-sep {
  height: 1px;
  background: var(--border);
  margin: 4px 8px;
}

/* ════════════════════════════════════════════════════════
   RULES MODAL
════════════════════════════════════════════════════════ */
.rules-modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(0,0,0,.6);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.rules-modal-overlay.open {
  display: flex;
}

.rules-modal-content {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 16px;
  width: 100%;
  max-width: 480px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 24px 64px rgba(0,0,0,.6);
}

.rules-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 20px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.rules-modal-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  margin: 0;
}

.rules-modal-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: var(--bg3);
  color: var(--text2);
  cursor: pointer;
  transition: background .2s, color .2s;
  flex-shrink: 0;
}
.rules-modal-close:hover {
  background: rgba(255,255,255,.12);
  color: var(--text);
}

.rules-modal-body {
  padding: 20px;
  overflow-y: auto;
}

#rulesList {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

#rulesList li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text2);
  padding-bottom: 12px;
  border-bottom: 1px solid rgba(255,255,255,.05);
}
#rulesList li:last-child { border-bottom: none; padding-bottom: 0; }

#rulesList li::before {
  content: '→';
  color: var(--orange);
  font-weight: 700;
  flex-shrink: 0;
  margin-top: 1px;
}

/* ════════════════════════════════════════════════════════════════
   TUNNEL IMMERSIF — Modale plein écran de quête
   3 zones : Header (A) · Stage question (B) · Métadonnées (C)
════════════════════════════════════════════════════════════════ */
/* ════════════════════════════════════════════════════════════════════════
   QUEST TUNNEL — COUCHE DE TOKENS SÉMANTIQUES (source de vérité unique)
   ────────────────────────────────────────────────────────────────────────
   Indirection : le tunnel ne consomme JAMAIS un token brut du :root global.
   Il passe par cette couche `--qt-*`, ce qui permet de re-thémer tout le
   tunnel depuis un seul endroit (DRY) sans toucher au reste de l'app.

   ⚠ REMAP : le brief référençait --surface / --surface-2 / --surface-3, qui
   N'EXISTENT PAS dans ce dépôt. Les surfaces réelles sont --bg / --bg2 / --bg3.
   On mappe donc sur les tokens réels (cf. :root, ligne 9-20) — sinon le thème
   se casserait (var() non résolue → valeur initiale).
════════════════════════════════════════════════════════════════════════ */
:root {
  /* Calque (overlay) ----------------------------------------------------- */
  --qt-overlay-bg: rgba(0, 0, 0, 0.85);

  /* Surfaces (brief: --surface* → réel: --bg*) --------------------------- */
  --qt-surface:          var(--bg);    /* surface profonde (calque/scène)        */
  --qt-surface-elevated: var(--bg2);   /* surfaces remontées (header, badges)    */
  --qt-surface-soft:     var(--bg3);   /* puces, pistes de barre, logos          */

  /* Texte ---------------------------------------------------------------- */
  --qt-text:        var(--text);
  --qt-text-muted:  var(--text2);
  --qt-text-subtle: var(--text3);      /* extension DRY (indicateurs discrets)   */

  /* Bordures ------------------------------------------------------------- */
  --qt-border:        var(--border);
  --qt-border-strong: var(--border2);  /* extension DRY (boutons, flèches)       */

  /* Accents sémantiques -------------------------------------------------- */
  --qt-accent:    var(--orange);
  --qt-accent-2:  var(--orange2);      /* extension DRY (états hover/gradient)   */
  /* Fond du bouton orange (CTA) du tunnel : le même orange → or/olive qu'avant,
     en plus profond. Le libellé reste blanc (--qt-text) et doit tenir 4,5:1 sur
     le point LE PLUS CLAIR du fond, reflet compris, au repos, au survol et
     pressé. Mesuré au pire : 4,8:1 au repos comme pressé, 4,66:1 au survol
     (qui garde son opacité 0,96). L'ancien dégradé plus clair : 2,1:1. */
  --qt-cta-from:  #AF3C0B;
  --qt-cta-to:    #7E600F;
  --qt-success:   var(--green);
  --qt-danger:    var(--red);

  /* Géométrie & motion partagées (évite les valeurs magiques dupliquées) - */
  --qt-radius:      24px;
  --qt-radius-pill: 9999px;
  --qt-gap:         20px;
  --qt-ease:        cubic-bezier(.22, .61, .36, 1);
  --qt-dur:         .2s;

  /* Échelle d'espacement φ (1,618) : 8 · 13 · 21 · 34 · 55. Bureau : 13 entre
     éléments liés, 21 entre groupes ; téléphones : un cran plus bas. */
  --qt-s1: 8px;
  --qt-s2: 13px;
  --qt-s3: 21px;
  --qt-s4: 34px;
  --qt-s5: 55px;

  /* Titre d'étape — UNE définition, lue par les titres ET par le texte court
     d'une étape info sans titre (qui en prend la taille et la graisse).
     Valeurs bureau inchangées ; redéfinies sous 760px (1,618 × texte). */
  --qt-title-size:   clamp(1.9rem, 4.5vw, 3.65rem);
  --qt-title-lh:     0.98;
  --qt-title-weight: 950;
  --qt-title-ls:     -0.06em;
}

/* ════════════════════════════════════════════════════════════════════════
   TUNNEL — SALLE NOIRE ASSUMÉE (contexte sombre explicite)
   ────────────────────────────────────────────────────────────────────────
   LE PROBLÈME. `.qt-stage` portait un dégradé sombre EN DUR pendant que son
   texte suivait le thème. En thème clair, `--text` passe à #0E0E14 : du noir
   sur du noir, mesuré à 1,01:1 sur les titres, les boutons et les options de
   quiz — c'est-à-dire du texte invisible, sur les huit écrans du tunnel.

   LE CHOIX. Le tunnel ne suit PAS le thème, et c'est délibéré : c'est une
   modale plein écran posée sur un voile à 85 % d'opacité — le contexte d'une
   visionneuse, pas d'une page. Son visuel est conçu pour le sombre et ne
   s'inverse pas mécaniquement : dégradé vert-sombre, liseré haut en blanc à
   4 %, lueurs, dégradé clair du titre de récompense. Une vraie variante claire
   est un travail de DESIGN, tenu en lot séparé.

   ⚠️ POURQUOI CE BLOC SURCHARGE LES `--qt-*` ET NON `--text` / `--bg`.
   C'est le piège de ce correctif, et il est silencieux. La couche `--qt-*` est
   déclarée sur `:root` sous la forme `--qt-text: var(--text)`. Or le `var()`
   d'une propriété personnalisée est substitué à la valeur calculée SUR
   L'ÉLÉMENT OÙ ELLE EST DÉCLARÉE : `--qt-text` est donc figé sur le `--text`
   de `:root`. Redéfinir `--text` ici n'aurait RIEN changé aux `--qt-*` —
   vérifié à la mesure, zéro ratio déplacé. On surcharge donc les deux : les
   `--qt-*` pour le contenu tokenisé, et les globaux pour la poignée de règles
   du tunnel qui lisent encore `--text2` / `--green` / `--orange` en direct.

   ⚠️ DUPLICATION ASSUMÉE : ces huit valeurs recopient la palette sombre de
   `[data-theme="dark"]` (en tête de ce fichier). Il n'existe pas de token
   « palette sombre » indépendant du thème à référencer. Si cette palette
   change, CE BLOC DOIT CHANGER AVEC ELLE.
   ════════════════════════════════════════════════════════════════════════ */
.quest-tunnel-overlay {
  /* Couche sémantique du tunnel */
  --qt-surface:          #0E0E14;
  --qt-surface-elevated: #13131C;
  --qt-surface-soft:     #1C1C28;
  --qt-text:             #F0F0FA;
  --qt-text-muted:       #9898B8;
  /* ⚠️ PAS #5A5A7A. Le token « subtle » d'origine donnait 2,07:1 sur la scène
     — un échec qui existait DÉJÀ en thème sombre, indépendant de ce lot. On
     l'aligne sur « muted », qui atteint 5,66:1. */
  --qt-text-subtle:      #9898B8;
  --qt-border:           rgba(255, 255, 255, 0.07);
  --qt-border-strong:    rgba(255, 255, 255, 0.12);

  /* Globaux, pour les règles du tunnel qui les lisent encore en direct */
  --bg:      #0E0E14;
  --bg2:     #13131C;
  --bg3:     #1C1C28;
  --text:    #F0F0FA;
  --text2:   #9898B8;
  --text3:   #9898B8;
  --border:  rgba(255, 255, 255, 0.07);
  --border2: rgba(255, 255, 255, 0.12);

  /* Champs de formulaire, barres de défilement et sélection natifs suivent. */
  color-scheme: dark;
}

.quest-tunnel-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  overflow-y: auto;
  /* Fondu SEUL. Le glissement d'ouverture est porté par .qt-shell (plus bas) :
     une transform, même en cours d'animation, fait du voile le bloc conteneur
     de ses descendants `position: fixed`. La croix (#qtClose) se plaçait alors
     à 8px du HAUT DU VOILE — donc sur la carte d'en-tête — au lieu de 8px du
     haut de l'écran : pendant les 0,3s de chaque ouverture, et durablement sur
     iPhone quand l'animation restait suspendue. La croix reste DANS le dialogue
     (role="dialog" aria-modal) : la sortir du voile l'aurait rendue
     inatteignable à VoiceOver. */
  transition: opacity 0.3s ease;
}
/* Fond et flou du tunnel : portés par .qt-tunnel-backdrop, élément FRÈRE placé
   juste après le voile (quete.html), et plus par le voile lui-même.
   · Un `backdrop-filter` fait de son élément le bloc conteneur de tous ses
     descendants `position: fixed`. Posé sur le voile — qui est aussi le
     conteneur de défilement — il faisait défiler la croix « fixe » avec le
     contenu dès qu'une étape dépassait l'écran.
   · ⚠️ PAS un pseudo-élément du voile : essayé, un calque flouté fixe À
     L'INTÉRIEUR du conteneur de défilement faisait disparaître par
     intermittence le bouton qui venait de changer d'état (« J'ai suivi »,
     absent de 3 captures sur 6 à +600ms ; 6 sur 6 présent sans lui).
   Même rendu qu'avant : même voile, même flou, même fondu (0,3s), et le
   voile reste plein écran sur bureau — cliquer à côté ferme toujours. */
.qt-tunnel-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9998;                  /* juste sous le voile (9999) */
  background: var(--qt-overlay-bg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: opacity 0.3s ease;
}
.quest-tunnel-overlay.hidden + .qt-tunnel-backdrop {
  opacity: 0;
  pointer-events: none;
}

/* État masqué — invisible et non interactif, animé en fondu + glissement.
   Le glissement porte sur la COLONNE (.qt-shell), pas sur le voile : voir le
   commentaire de .quest-tunnel-overlay. Même mouvement, même durée ; seule la
   croix ne glisse plus — elle reste dans sa bande. */
.quest-tunnel-overlay.hidden {
  opacity: 0;
  pointer-events: none;
}
.quest-tunnel-overlay.hidden .qt-shell {
  transform: translateY(20px);
}

/* Blocage du scroll de la page de fond quand le tunnel est ouvert */
body.no-scroll { overflow: hidden; }

/* ── Utilitaire d'état piloté par classe (remplace les style= inline JS) ──
   `.qt-hidden` : masquage par classe au lieu de element.style.display = 'none'.
   Portée stricte au tunnel (préfixe qt-) → zéro collision globale. */
.qt-hidden { display: none !important; }

/* Conteneur central qui regroupe les 3 zones */
.qt-shell {
  width: 100%;
  max-width: 1200px;
  transition: transform 0.3s ease;   /* glissement d'ouverture — voir .quest-tunnel-overlay.hidden */
  display: flex;
  flex-direction: column;
  gap: var(--qt-s3);
  /* En-tête ANCRÉ : la colonne (en-tête, carte, badges) occupe toute la
     hauteur du voile au lieu d'y être centrée. L'en-tête commence donc au même
     y sur toutes les étapes (24px bureau, 40px téléphone) ; centrée, la
     colonne le faisait sauter d'une étape à l'autre (146 / 40 px à 375px). */
  min-height: 100%;
  /* `auto` et PAS `0 auto` : le voile centre verticalement sur bureau
     (align-items: center). Avec des marges auto, une colonne plus haute que
     l'écran déborde vers le BAS (atteignable en défilant) ; avec `0 auto`,
     elle déborderait des deux côtés et son haut serait inatteignable. */
  margin: auto;
}

/* ── CONTENEUR DE REGROUPEMENT (invisible) ──
   Regroupe le bloc d'action (.qt-stage) et la rangée de badges (.qt-meta)
   sans aucune décoration. C'est le .qt-stage qui porte le visuel. */
.qt-frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  flex: 1 0 auto;                 /* prend la hauteur que l'en-tête et les badges laissent */
}

/* Croix de fermeture — absolue en haut à droite */
.qt-close {
  position: fixed;
  top: 24px;
  right: 24px;
  z-index: 10001;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  cursor: pointer;
  transition: background .2s, transform .2s;
}
.qt-close:hover { background: rgba(255, 255, 255, 0.16); transform: rotate(90deg); }

/* ──────────── ZONE A — Header ──────────── */
.qt-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  max-width: 1200px;
  margin: 0 auto 8px auto;
  padding: 16px 24px;
  background: rgba(255, 255, 255, 0.04);
  border-radius: 16px;
}
.qt-header-left {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}
.qt-partner-logo {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-md);
  background: var(--bg3);
  border: 1px solid var(--border);
  flex-shrink: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
.qt-partner-logo img { width: 100%; height: 100%; object-fit: contain; }
/* Posés par renderPartnerLogo (questionnaire.js). `contain` et non `cover` :
   un logo ne se rogne pas — un sceau rond y perdait son pourtour. Sans logo
   exploitable, l'initiale de l'enseigne : même repli que les cartes de quête
   (.qc-emblem-initial), jamais une tuile vide. */
.qt-partner-initial {
  font-size: 24px;
  font-weight: 800;
  line-height: 1;
  color: color-mix(in srgb, var(--qt-accent) 70%, #fff);
}

.qt-header-titles { min-width: 0; }
.qt-sponsored {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--text3);
  margin-bottom: 4px;
}
.qt-sponsor-logo {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg3);
  flex-shrink: 0;
}
.qt-title-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.qt-quest-title {
  font-size: clamp(20px, 3.5vw, 28px);
  font-weight: 800;
  color: var(--text);
  line-height: 1.15;
  margin: 0;
}
.qt-heart {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg3);
  color: var(--text2);
  cursor: pointer;
  flex-shrink: 0;
  transition: color .2s, border-color .2s, background .2s;
}
.qt-heart:hover { color: var(--red); border-color: var(--red); }
.qt-heart.active { color: var(--red); }
.qt-heart.active svg { fill: var(--red); }
.qt-heart svg { width: 18px; height: 18px; }

/* Bloc Rewards (droite) */
.qt-rewards {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  flex-shrink: 0;
}
.qt-rewards-label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--text3);
}
.qt-rewards-icons {
  display: flex;
  align-items: center;
  gap: 8px;
}
.qt-trophy { color: var(--yellow); display: flex; }
.qt-xp-icon { width: 22px; height: 22px; object-fit: contain; }
.qt-xp-value {
  font-size: var(--text-md);
  font-weight: 800;
  color: var(--green);
}

/* ──────────── ZONE B — Stage / Question ──────────── */
.qt-stage {
  position: relative;
  width: 100%;
  max-width: 1200px;
  border-radius: var(--qt-radius);
  border: 1px solid var(--qt-border);
  background: linear-gradient(150deg, #16261D 0%, #11141C 55%, #0C0C12 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 16px 48px rgba(0, 0, 0, 0.35);
  padding: var(--qt-s5) 72px;
  /* La carte remplit la hauteur restante (flex), et son contenu y est placé
     en section dorée (.qt-stage-body). Plus haute que la place : c'est le
     voile qui défile (bloc « UN SEUL DÉFILEMENT » plus bas). Pas de
     `min-height` en `vh` : sur mobile ce n'est pas la hauteur visible. */
  flex: 1 0 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.qt-step-indicator {
  position: absolute;
  top: 20px;
  right: 24px;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text3);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border);
  border-radius: 9999px;
  padding: 5px 12px;
}
/* Flèches de navigation latérales — absolues, centrées verticalement */
.qt-nav-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text2);
  cursor: pointer;
  z-index: 6;
  transition: background .2s, color .2s, transform .15s;
}
.qt-nav-arrow:hover { background: rgba(255, 255, 255, 0.12); color: var(--text); }
.qt-nav-arrow:active { transform: translateY(-50%) scale(.92); }
.qt-nav-prev { left: 16px; }
.qt-nav-next { right: 16px; }

/* Indicateurs carrousel (dots) — bas centre du stage */
.qt-dots {
  position: absolute;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 5;
}
.qt-dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--text3);
  opacity: .5;
  cursor: pointer;
  transition: opacity .2s, width .2s, background .2s;
}
.qt-dot.active {
  width: 22px;
  border-radius: 4px;
  background: var(--green);
  opacity: 1;
}

/* ──────────── ZONE C — Métadonnées ──────────── */
.qt-meta {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  flex-wrap: wrap;
  width: 100%;
  max-width: 1200px;
  margin-top: 16px;
}
.qt-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 9999px;
  border: 1px solid var(--border);
  background: var(--bg2);
  color: var(--text2);
  font-size: var(--text-sm);
  font-weight: 700;
}
/* Tout le texte des bulles meta en gras */
.qt-meta,
.qt-meta span { font-weight: 700; }
.qt-badge svg { color: var(--green); flex-shrink: 0; }
.qt-badge-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 5px;
  background: var(--orange);
  /* ⚠️ PAS #fff. Blanc sur l'orange de marque = 2,84:1 — un échec qui existait
     dans LES DEUX thèmes, indépendant du contexte sombre posé plus haut. On
     garde l'orange (c'est l'identité) et on assombrit la lettre : 6,77:1.
     Assombrir l'orange à la place aurait atteint le seuil en abîmant la
     marque. */
  color: #0E0E14;
  font-size: 11px;
  font-weight: 800;
  flex-shrink: 0;
}
.qt-avatars { display: inline-flex; align-items: center; }
.qt-avatar {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--bg2);
  background: linear-gradient(135deg, var(--violet), var(--orange));
  margin-left: -7px;
}
.qt-avatar:first-child { margin-left: 0; }
.qt-avatar:nth-child(2) { background: linear-gradient(135deg, var(--green), var(--blue)); }
.qt-avatar:nth-child(3) { background: linear-gradient(135deg, var(--yellow), var(--red)); }

/* ──────────── Responsive — Smartphone ──────────── */
/* ════════════════════════════════════════════════════════════════════════
   TUNNEL — UN SEUL DÉFILEMENT : CELUI DU VOILE
   ────────────────────────────────────────────────────────────────────────
   AVANT (lot « hauteur bornée »). Chaque écran de phase était sa propre boîte
   défilante (`overflow-y: auto` sur .qt-flow-screen), rangée d'actions collée
   en bas (`position: sticky`). Mesuré sur iPhone SE (375×667), 14 écrans :
     · une barre de défilement VISIBLE dans la carte, sur chacun ;
     · 16px de défilement HORIZONTAL en prime. Un axe en `auto` entraîne
       l'autre (`overflow-x` passe de `visible` à `auto`), et la lueur `::before`
       de l'écran (90vw, centrée par translate) dépassait de 16px. Au doigt, la
       boîte glissait de côté et tranchait les mots au bord — la capture du
       fondateur (« pécialité », « octeur ») se reproduit en la décalant de 16px ;
     · la rangée collante recouvrait ce qui défilait dessous (« +50 XP » caché
       par « Démarrer la quête »), et les dots se posaient sur la 2e réponse.

   MAINTENANT. Le contenu garde sa hauteur naturelle et c'est le VOILE,
   plein écran, qui défile : la « page » du tunnel, seul conteneur de
   défilement. Sa barre est masquée à l'œil ; le défilement reste entier au
   doigt, à la molette et au clavier.
   Quand tout tient, la colonne remplit l'écran (en-tête ancré, carte
   étirée, contenu en section dorée — voir .qt-shell et .qt-stage-body) ;
   quand ça déborde, c'est ce voile qui défile.

   `overflow-x: hidden` n'est qu'un FILET. Les causes — lueur trop large,
   titres sans césure, mots insécables — sont traitées à la source (lueur à
   100 % de l'écran, `overflow-wrap: anywhere`). Un filet qui corrigerait à
   leur place rognerait le texte en silence.
   ════════════════════════════════════════════════════════════════════════ */
.quest-tunnel-overlay {
  max-height: 100vh;              /* repli : moteurs sans dvh */
  max-height: 100dvh;             /* hauteur RÉELLEMENT visible */
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;   /* ne fait jamais défiler la page restée derrière */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;          /* barre masquée — Firefox */
}
.quest-tunnel-overlay::-webkit-scrollbar { display: none; }   /* idem — Blink, WebKit */

/* Écran de phase : toute la largeur de la scène. Seule déclaration conservée
   de l'ancienne règle de défilement intérieur, parce qu'elle fixe la mise en
   page du BUREAU : sans elle, l'écran retombe à `min(760px, 100%)`, la
   question passe de 4 à 5 lignes et la vidéo perd un tiers de sa largeur
   (mesuré à 1440×900). */
.qt-stage-body > .qt-flow-screen,
.qt-stage-body > .qt-reward-stage {
  width: 100%;
  /* Pas de marge verticale propre : la carte (padding φ) et les ressorts de
     la section dorée font l'espace, sans quoi le rapport 1 : 1,618 se
     diluerait dans deux marges égales. */
  padding-top: 0;
  padding-bottom: 0;
}

/* Rangée d'actions : dans le flux, sous ce qu'elle conclut. Elle garde la
   pleine largeur de la scène, héritée du lot « hauteur bornée » (elle y
   portait un fond collant) : on ne la change pas, pour ne rien déplacer sur
   bureau. D'où la contrepartie — `.btn-primary` porte un `width: 100%` global
   (en tête de ce fichier) qui l'étirait sur 1011px de large : on lui rend sa
   largeur intrinsèque. Le mobile reprend le plein format dans sa media query. */
.qt-stage-body .qt-flow-actions,
.qt-stage-body .qt-reward-actions {
  flex: 0 0 auto;
  width: 100%;
}
.qt-stage-body .qt-flow-actions > .btn-primary,
.qt-stage-body .qt-reward-actions > .btn-primary {
  width: auto;
}

@media (max-width: 640px) {
  /* BANDE DE LA CROIX. La zone qui défile commence à 40px : au-dessus, une
     bande où vit la croix (32px posée à 8, elle s'arrête pile à 40). Le
     contenu défile SOUS la bande, jamais sous la croix — sur mobile la scène
     n'a que 16px de marge et une réponse passait sinon sous le bouton. Au
     repos, rien ne bouge : l'en-tête commence à 40px comme avant (16px de
     voile + 24px de coquille, remplacés par la bande). La bande n'est couverte
     que par le fond flouté (.qt-tunnel-backdrop) : un toucher à côté de la
     croix n'y fait rien — ni fermeture, ni clic sur la page de derrière.
     32px reste au-dessus du minimum de 24×24 du critère WCAG 2.5.8 ; le
     bureau garde ses 44px, dans le rembourrage de la scène (72px de côté). */
  .quest-tunnel-overlay { top: 40px; padding: 0 16px 16px; align-items: flex-start; }
  .qt-close { top: 8px; right: 14px; width: 32px; height: 32px; }
  .qt-shell { gap: var(--qt-s1); padding-top: 0; }

  /* En-tête compact : 112px → ~88px. Logo, sponsor, titre et cœur sur une
     rangée ; récompenses dessous, alignées sur le titre (44px de tuile +
     10px d'écart). */
  .qt-header { flex-direction: column; align-items: stretch; gap: var(--qt-s1); padding: var(--qt-s1) 12px; border-radius: 14px; margin-bottom: 0; }
  .qt-header-left { gap: 10px; }
  .qt-partner-logo { width: 44px; height: 44px; border-radius: 12px; }
  .qt-partner-initial { font-size: 20px; }
  .qt-sponsored { margin-bottom: 2px; letter-spacing: .5px; }
  .qt-title-row { gap: 8px; }
  .qt-quest-title { font-size: 17px; line-height: 1.2; }
  .qt-heart { width: 32px; height: 32px; }
  .qt-heart svg { width: 16px; height: 16px; }
  .qt-rewards { flex-direction: row; align-items: center; align-self: flex-start; gap: 8px; padding-left: 54px; }
  .qt-rewards-label { order: -1; }
  .qt-rewards-icons { gap: 6px; }
  .qt-trophy svg,
  .qt-xp-icon { width: 20px; height: 20px; }

  .qt-frame { padding: 0; border-radius: 18px; gap: 16px; }
  /* Rognage mobile : 110px de padding vertical (bureau) → 21px, sur l'échelle φ. */
  .qt-stage { padding: var(--qt-s1) 16px var(--qt-s2); min-height: 0; }

  /* BARRE DE SCÈNE — chevron « retour » à gauche, pastille « Quiz x/y » à
     droite, AU-DESSUS du contenu, jamais dessus. Sur bureau, ils restent
     ancrés dans le rembourrage de la scène (72px de côté, 48px en haut), qui
     suffit à ne rien recouvrir. Ici ce rembourrage tombe à 16px, et le
     chevron, centré verticalement, se posait sur la question.
     La scène devient une grille : rangée 1 la barre, rangée 2 le contenu,
     rangée 3 les dots. Aucun gabarit touché, seul le placement change. Une
     rangée sans élément visible (accueil, récompense) mesure 0px. */
  .qt-stage { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: auto 1fr auto; align-items: center; align-content: start; }
  .qt-nav-prev       { grid-area: 1 / 1; }
  .qt-step-indicator { grid-area: 1 / 2; justify-self: end; }
  .qt-nav-next       { grid-area: 1 / 3; }
  .qt-stage-body     { grid-area: 2 / 1 / 3 / -1; align-self: stretch; }   /* la rangée 1fr prend la hauteur libre */
  .qt-dots           { grid-area: 3 / 1 / 4 / -1; justify-self: center; }
  .qt-nav-arrow,
  .qt-step-indicator { position: static; transform: none; margin-bottom: var(--qt-s1); }
  .qt-nav-arrow { width: 36px; height: 36px; }
  .qt-nav-arrow:active { transform: scale(.92); }
  /* Dots : sous le contenu, dans le flux. Ancrés à 24px du bas de la scène,
     ils se posaient sur la 2e réponse. */
  .qt-dots { position: static; transform: none; margin-top: var(--qt-s2); }

  .qt-meta { gap: 8px; }
  .qt-badge { padding: 7px 12px; font-size: var(--text-xs); }
}

/* ════════════════════════════════════════════════════════════════
   TUNNEL — Contenu dynamique (slides + QCM)
   Injecté par questionnaire.js dans #qtStageBody (.qt-stage)
   Styles scopés sous .qt-stage — utilisent les tokens :root
════════════════════════════════════════════════════════════════ */
.qt-stage-body {
  flex: 1 0 auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  text-align: center;
}
/* SECTION DORÉE — l'espace libre de la carte se partage 1 : 1,618 au-dessus
   et au-dessous du contenu (au lieu d'être centré, 1 : 1). Deux ressorts
   invisibles, de croissance 1 et 1,618. Contenu plus haut que la place : ils
   tombent à 0 et c'est le voile qui défile. */
.qt-stage-body::before,
.qt-stage-body::after {
  content: "";
  flex: 1 1 0;
}
.qt-stage-body::after {
  flex-grow: 1.618;
}

/* ════════════════════════════════════════════════════════════════
   TUNNEL — FLOW 5 PHASES (contenu dynamique, injecté dans #qtStageBody)
   Squelettes = <template> dans quete.html, repeuplés par questionnaire.js.
   Tout consomme la couche de tokens --qt-* (source de vérité unique).
════════════════════════════════════════════════════════════════ */
.qt-stage-body,
.qt-stage-body *,
.qt-stage-body *::before,
.qt-stage-body *::after {
  box-sizing: border-box;
}

/* Mots insécables et URL collées passent à la ligne au lieu de pousser la
   carte hors de l'écran — tous les textes que le marchand écrit, sur toutes
   les phases. `anywhere` et non `break-word` : seul le premier réduit aussi la
   largeur min-content, et c'est elle qui permet à une colonne de réponses de
   rétrécir au lieu de déborder. */
.quest-tunnel-overlay .qt-quest-title,
.quest-tunnel-overlay .qt-sponsored,
.quest-tunnel-overlay .qt-flow-title,
.quest-tunnel-overlay .qt-flow-subtitle,
.quest-tunnel-overlay .qt-flow-card strong,
.quest-tunnel-overlay .qt-flow-card p,
.quest-tunnel-overlay .question-title,
.quest-tunnel-overlay .quiz-option-btn,
.quest-tunnel-overlay .reward-title,
.quest-tunnel-overlay .reward-subtitle,
.quest-tunnel-overlay .signup-sub {
  overflow-wrap: anywhere;
}

/* B2-1 — conteneur vidéo 16:9 (iframe YouTube injectée par le moteur, renderVideoStep) */
.qt-video-frame {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 14px;
  overflow: hidden;
  background: var(--bg3);
  margin: var(--qt-s3) 0 0;
}
.qt-video-frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
/* Paquet 8 : la case locale « Lire la vidéo » (rien n'est demandé à YouTube avant l'appui).
   Neutre : aucune image ; le triangle de lecture est dessiné en CSS. */
.qt-video-lire {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 100%;
  min-height: 44px;
  padding: 12px;
  border: 0;
  background: var(--bg3);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.qt-video-lire:focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: -3px;
}
.qt-video-lire-icone {
  position: relative;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--orange);
  flex-shrink: 0;
}
.qt-video-lire-icone::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 54%;
  transform: translate(-50%, -50%);
  border-style: solid;
  border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent #fff;
}
.qt-video-lire-texte {
  font-weight: 800;
  font-size: var(--text-lg);
}
.qt-video-lire-note {
  font-size: var(--text-base);
  color: var(--text2);
}

.qt-flow-screen,
.qt-reward-stage {
  position: relative;
  isolation: isolate;
  width: min(720px, 100%);
  margin: 0 auto;
  padding: clamp(0.85rem, 2vw, 1.35rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  color: var(--qt-text);
  text-align: center;
}

.qt-flow-screen {
  opacity: 0;
  transform: translate3d(0, 14px, 0) scale(0.985);
  animation: qtFlowIn 580ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.qt-flow-screen::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 2%;
  left: 50%;
  /* 100 % et non 90vw : l'écran est plus étroit que 90vw sur mobile (305px
     pour 337 à 375px de large), et ce surplus de 16px était la cause du
     défilement horizontal de la carte. */
  width: min(520px, 100%);
  height: min(300px, 48vw);
  border-radius: var(--qt-radius-pill);
  background:
    radial-gradient(
      circle at 50% 42%,
      color-mix(in srgb, var(--qt-success) 18%, transparent),
      transparent 56%
    ),
    radial-gradient(
      circle at 38% 36%,
      color-mix(in srgb, var(--qt-accent) 16%, transparent),
      transparent 42%
    );
  opacity: 0.78;
  transform: translate3d(-50%, 0, 0);
  pointer-events: none;
}

.qt-flow-icon {
  position: relative;
  width: clamp(78px, 10vw, 112px);
  aspect-ratio: 1;
  margin: 0 auto var(--qt-s3);
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--qt-success) 38%, var(--qt-text) 10%);
  border-radius: var(--qt-radius-pill);
  background:
    radial-gradient(
      circle at 35% 26%,
      color-mix(in srgb, var(--qt-text) 16%, transparent),
      transparent 34%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 78%, var(--qt-success) 22%),
      color-mix(in srgb, var(--qt-surface) 88%, var(--qt-accent) 12%)
    );
  color: var(--qt-text);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 14%, transparent),
    inset 0 -16px 36px color-mix(in srgb, var(--qt-overlay-bg) 34%, transparent),
    0 26px 76px color-mix(in srgb, var(--qt-overlay-bg) 58%, transparent),
    0 0 0 8px color-mix(in srgb, var(--qt-success) 10%, transparent),
    0 0 58px color-mix(in srgb, var(--qt-success) 22%, transparent);
  font-size: clamp(2.1rem, 4.2vw, 3.1rem);
  line-height: 1;
  font-weight: 950;
  letter-spacing: -0.08em;
  opacity: 0;
  transform: translate3d(0, 12px, 0) scale(0.84);
  animation: qtFlowIconPop 700ms cubic-bezier(0.16, 1, 0.3, 1) 80ms forwards;
}

.qt-flow-kicker {
  margin: 0 0 var(--qt-s2);
  /* Orange clair de la marque : l'orange pur tombait à 4,1–4,5:1 sur le haut
     verdâtre de la scène (« Nouvelle quête », « Quête sociale »). */
  color: var(--qt-accent-2);
  font-size: 0.78rem;
  line-height: 1;
  font-weight: 900;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-shadow:
    0 0 22px color-mix(in srgb, var(--qt-accent) 34%, transparent);
}

.qt-flow-title,
.qt-flow-quiz .question-title {
  margin: 0;
  max-width: 780px;
  color: var(--qt-text);
  font-size: var(--qt-title-size);
  line-height: var(--qt-title-lh);
  font-weight: var(--qt-title-weight);
  letter-spacing: var(--qt-title-ls);
  text-wrap: balance;
  text-shadow:
    0 20px 54px color-mix(in srgb, var(--qt-overlay-bg) 62%, transparent);
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  animation: qtFlowContentIn 550ms cubic-bezier(0.16, 1, 0.3, 1) 160ms forwards;
}

.qt-flow-subtitle {
  margin: var(--qt-s2) 0 0;
  max-width: 620px;
  color: var(--qt-text-muted);
  font-size: clamp(0.95rem, 1.7vw, 1.08rem);
  line-height: 1.55;
  font-weight: 610;
  letter-spacing: -0.012em;
  text-wrap: balance;
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  animation: qtFlowContentIn 550ms cubic-bezier(0.16, 1, 0.3, 1) 240ms forwards;
}

/* Message « Cette quête a été mise à jour… » (filet du tunnel, 746adc7) : le seul
   sous-titre posé AU-DESSUS d'une question. .qt-flow-subtitle n'a qu'une marge HAUTE
   (il suit d'ordinaire un titre) ; sans marge basse il collait à la question (0 px).
   Même pas en dessous qu'au-dessus — réduit à --qt-s1 sous 760px, comme sa marge haute. */
.qt-flow-quiz .qt-flow-subtitle {
  margin-bottom: var(--qt-s2);
}

/* Étape info SANS titre : pas de titre vide à l'écran, et le texte n'est plus
   grisé — c'est le message de l'étape. */
.qt-stage-body .qt-flow-title:empty {
  display: none;
}
.qt-stage-body .qt-flow-title:empty + .qt-flow-subtitle {
  margin-top: 0;
  color: var(--qt-text);
}
/* …et s'il est COURT (classe posée par renderInfoStep, ≤ 90 caractères), il
   prend la taille et la graisse d'un titre d'étape — les mêmes jetons, donc
   la même échelle à toutes les largeurs. Un texte long garde la taille du
   texte courant : 10 des 12 étapes info en ligne font 178 à 500 caractères,
   qui deviendraient une vingtaine de lignes de titre. */
.qt-stage-body .qt-flow-subtitle.qt-flow-subtitle--lead {
  font-size: var(--qt-title-size);
  line-height: var(--qt-title-lh);
  font-weight: var(--qt-title-weight);
  letter-spacing: var(--qt-title-ls);
}

.qt-flow-card {
  position: relative;
  overflow: hidden;
  width: min(560px, 100%);
  margin-top: var(--qt-s3);
  padding: clamp(0.8rem, 1.8vw, 1rem) clamp(0.9rem, 2vw, 1.15rem);
  border: 1px solid color-mix(in srgb, var(--qt-text) 10%, transparent);
  border-radius: calc(var(--qt-radius) - 6px);
  background:
    radial-gradient(
      circle at 86% 0%,
      color-mix(in srgb, var(--qt-accent) 14%, transparent),
      transparent 46%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 88%, var(--qt-success) 12%),
      color-mix(in srgb, var(--qt-surface) 94%, transparent)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 10%, transparent),
    0 20px 62px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent);
  opacity: 0;
  transform: translate3d(0, 12px, 0) scale(0.98);
  animation: qtFlowPanelIn 560ms cubic-bezier(0.16, 1, 0.3, 1) 320ms forwards;
}

.qt-flow-card span {
  display: block;
  color: var(--qt-text-muted);
  font-size: 0.74rem;
  font-weight: 900;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.qt-flow-card strong {
  display: block;
  margin-top: 0.42rem;
  color: var(--qt-text);
  font-size: clamp(1.02rem, 2vw, 1.32rem);
  line-height: 1.18;
  font-weight: 950;
  letter-spacing: -0.035em;
}

.qt-flow-card p {
  margin: 0.5rem 0 0;
  color: var(--qt-text-muted);
  font-size: 0.95rem;
  line-height: 1.45;
}

.qt-flow-actions,
.qt-reward-actions {
  margin-top: var(--qt-s3);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  animation: qtFlowContentIn 550ms cubic-bezier(0.16, 1, 0.3, 1) 420ms forwards;
}

/* `.qt-stage-body .btn-primary:hover` figure dans cette liste et dans la suivante
   pour une seule raison : la règle GLOBALE `.btn-primary:hover` (haut du fichier)
   repeint tout bouton orange en --orange2, décale de -2px et change l'ombre. À
   spécificité égale, elle ne perdait ici que par l'ordre du fichier ; avec
   :hover dans le sélecteur (0,3,0), le tunnel l'emporte quel que soit l'ordre —
   y compris sur mobile, où un toucher laisse :hover collé au bouton. */
.qt-stage-body .btn-primary,
.qt-stage-body .btn-primary:hover,
.qt-stage-body .btn-ghost {
  appearance: none;
  min-height: 50px;
  padding: 0 1.7rem;
  border-radius: var(--qt-radius-pill);
  font: inherit;
  font-size: 1rem;
  font-weight: 900;
  letter-spacing: -0.015em;
  cursor: pointer;
  transform: translate3d(0, 0, 0) scale(1);
  transition:
    transform 220ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 220ms ease;
}

.qt-stage-body .btn-primary,
.qt-stage-body .btn-primary:hover {
  min-width: min(220px, 76vw);
  border: 1px solid color-mix(in srgb, var(--qt-accent) 72%, var(--qt-text) 28%);
  background:
    radial-gradient(
      circle at 28% 18%,
      /* Reflet 20 % → 6 % : c'est lui qui fait le point le plus clair du bouton. */
      color-mix(in srgb, var(--qt-text) 6%, transparent),
      transparent 32%
    ),
    linear-gradient(
      135deg,
      var(--qt-cta-from),
      var(--qt-cta-to)
    );
  color: var(--qt-text);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 18%, transparent),
    0 20px 56px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent),
    0 0 0 7px color-mix(in srgb, var(--qt-accent) 12%, transparent),
    0 0 42px color-mix(in srgb, var(--qt-accent) 28%, transparent);
}

.qt-stage-body .btn-ghost {
  min-width: min(150px, 44vw);
  border: 1px solid color-mix(in srgb, var(--qt-text) 12%, transparent);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 82%, transparent),
      color-mix(in srgb, var(--qt-surface) 92%, transparent)
    );
  color: var(--qt-text-muted);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 9%, transparent),
    0 14px 42px color-mix(in srgb, var(--qt-overlay-bg) 42%, transparent);
}

@media (hover: hover) and (pointer: fine) {
  .qt-stage-body .btn-primary:hover,
  .qt-stage-body .btn-ghost:hover {
    opacity: 0.96;
    transform: translate3d(0, -3px, 0) scale(1.025);
  }
}

.qt-stage-body .btn-ghost:active {
  opacity: 0.86;
  transform: translate3d(0, 1px, 0) scale(0.985);
}

/* Bouton orange pressé : AUCUN fondu — l'opacité 0,86 rapprochait le libellé
   blanc du fond. Il s'enfonce (échelle 0,97) et son halo se resserre (anneau
   7 → 4 px, lueur 42 → 28 px) ; le libellé reste plein. */
.qt-stage-body .btn-primary:active {
  opacity: 1;
  transform: translate3d(0, 1px, 0) scale(0.97);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 18%, transparent),
    0 20px 56px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent),
    0 0 0 4px color-mix(in srgb, var(--qt-accent) 12%, transparent),
    0 0 28px color-mix(in srgb, var(--qt-accent) 28%, transparent);
}

.qt-stage-body .btn-primary:focus-visible,
.qt-stage-body .btn-ghost:focus-visible,
.quiz-option-btn:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--qt-accent) 72%, var(--qt-text) 28%);
  outline-offset: 5px;
}

/* ═══ POLISH TUNNEL (sous-lots 1-2) — apparence uniquement, zéro hook JS modifié ═══ */

/* SL1 — État désactivé VISIBLE (ex. « Valider » social tant qu'on n'a pas suivi). */
.qt-stage-body .btn-primary:disabled,
.qt-stage-body .btn-ghost:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  background: var(--qt-surface-soft);
  border: 1px solid var(--qt-border);
  color: var(--qt-text-muted);
  box-shadow: none;
  transform: none;
}
.qt-stage-body .btn-primary:disabled:hover,
.qt-stage-body .btn-ghost:disabled:hover {
  opacity: 0.42;
  transform: none;
  box-shadow: none;
}

/* SL1 — Social : largeurs égales + icône réseau DANS le bouton « Suivre » + note d'erreur rouge. */
.qt-stage-body .qt-flow-actions .qt-social-follow,
.qt-stage-body .qt-flow-actions .qt-social-validate-wrap {
  flex: 1 1 0;
  min-width: 0;
  /* Alignés par le HAUT : l'aide allonge le groupe, les deux boutons restent
     sur la même ligne au lieu que « Suivre » se centre sur bouton + aide. */
  align-self: flex-start;
}
.qt-social-validate-wrap {
  display: flex;
  flex-direction: column;
}
.qt-stage-body .qt-social-validate-wrap .qt-social-validate {
  width: 100%;
}
.qt-social-follow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
}
.qt-social-follow svg {
  width: 1.15em;
  height: 1.15em;
  flex-shrink: 0;
}
.qt-flow-subtitle.qt-social-note {
  color: var(--qt-danger);
  font-weight: 700;
}

/* Bouton « pas encore » : « J'ai suivi » AVANT que « Suivre » ne soit touché,
   et « Continuer » d'une question AVANT qu'une réponse soit validée (rangée
   aria-hidden="true", voir la règle du quiz plus bas). Bouton secondaire
   LISIBLE : « J'ai suivi » héritait de l'état désactivé générique ci-dessus
   (opacité 0,42 sur fond gris, libellé mesuré à 2,05:1 sur main) et
   « Continuer » était invisible. Ici : contour net, fond transparent, libellé
   pleine opacité, curseur interdit.
   Seule l'APPARENCE change. Le moment où chaque bouton s'active est celui de
   questionnaire.js ; l'état actif garde son style (.btn-ghost / .btn-primary). */
.qt-stage-body .qt-flow-actions .qt-social-validate:disabled,
.qt-stage-body .qt-flow-actions .qt-social-validate:disabled:hover,
.qt-flow-quiz .qt-flow-actions[aria-hidden="true"] > .btn-primary,
.qt-flow-quiz .qt-flow-actions[aria-hidden="true"] > .btn-primary:hover,
.qt-flow-quiz .qt-flow-actions[aria-hidden="true"] > .btn-primary:active {
  opacity: 1;
  background: transparent;
  border: 1.5px solid color-mix(in srgb, var(--qt-text-muted) 65%, transparent);
  color: var(--qt-text-muted);   /* pas d'alpha : à 88 % il tombait à 4,45:1 sur mobile */
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}
/* La ligne d'aide, directement sous le bouton. Masquée dès que le bouton
   s'active — y compris quand il naît actif (lien marchand invalide).
   `visibility` et non `display` : sa place reste, la mise en page ne saute
   pas sous le doigt du joueur au moment où il vient de toucher « Suivre ». */
.qt-social-hint,
.qt-quiz-hint {
  margin: 0.5rem 0 0;
  color: var(--qt-text-muted);
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
}
.qt-social-validate:not(:disabled) + .qt-social-hint {
  visibility: hidden;
}

/* SL2 — Inscription (gate Layer3) : champs dark cohérents, erreur rouge, sous-titre muted. */
/* Aide « Au moins 10 caractères. » sous un champ de NOUVEAU mot de passe
   (posée par mot-de-passe.js, ou écrite dans le modèle du tunnel). Couleur
   HÉRITÉE et atténuée : lisible dans les fenêtres sombres de l'accueil et des
   campagnes comme sur login.html, en thème clair ou sombre. Pas de display :
   l'attribut hidden (onglet « Se connecter » de login.html) reste souverain. */
.qw-mdp-aide {
  margin: -4px 2px 10px;
  font-size: 12.5px;
  line-height: 1.4;
  opacity: .72;
}
/* Fenêtres d'inscription de l'accueil et des campagnes : fond TOUJOURS sombre
   (#1a1a2e, posé par leur script), même en thème clair. Même gris que leur
   case « bons plans ». */
.qw-mdp-aide--fond-sombre {
  color: #aaaaaa;
  opacity: 1;
}
.qt-signup-form .qw-mdp-aide {
  margin: -0.3rem 0.25rem 0;
  color: var(--qt-text-muted);
  opacity: 1;
}

/* L'ŒIL « Afficher / Masquer le mot de passe » (paquet 4, posé par mot-de-passe.js
   sur TOUT champ mot de passe). Le champ est enveloppé : l'enveloppe reprend ses
   marges (lues sur le champ, propriétés --qw-mdp-marge-*), le champ cède 48 px de
   marge intérieure droite au bouton — la mise en page autour ne bouge pas.
   Cible de 44 × 44 px (§18). Deux couleurs fixes, choisies sur le fond RÉEL du
   champ (les fenêtres de l'accueil et des campagnes sont sombres même en thème
   clair — même raison que .qw-mdp-aide--fond-sombre) : ≥ 7:1 sur leurs fonds. */
.qw-mdp-boite {
  position: relative;
  display: block;
  margin-top: var(--qw-mdp-marge-haut, 0);
  margin-bottom: var(--qw-mdp-marge-bas, 0);
}
.qw-mdp-boite > input {
  display: block;            /* en ligne, le champ ajouterait sous lui l'espace d'une ligne de texte (mesuré : 2 px) */
  margin-top: 0 !important;
  margin-bottom: 0 !important;
  padding-right: 48px !important;
}
.qw-mdp-oeil {
  position: absolute;
  top: 50%;
  right: 2px;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.qw-mdp-oeil--sur-sombre { color: #B9B9CC; }
.qw-mdp-oeil--sur-clair  { color: #55556B; }
.qw-mdp-oeil:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: -3px;
}
@media (hover: hover) {
  .qw-mdp-oeil--sur-sombre:hover { color: #FFFFFF; }
  .qw-mdp-oeil--sur-clair:hover  { color: #1C1C28; }
}

/* ── Titre conservé d'une étape « texte » (paquet 6, QwEtapes.poserTitreConserve) ──
   Une ligne sous le texte, dans les formulaires admin et portail. */
.sb-titre-conserve { overflow-wrap: anywhere; }

/* ── « Mot de passe oublié » de login.html (paquet 5, mot-de-passe-oublie.js) ──
   La vue reprend les classes de la carte de connexion (.brand-header, .form-group,
   .auth-input, .btn-submit, définies dans le <style> de login.html) ; seules ses
   pièces propres vivent ici. Leurs champs sont à 16 px comme tous ceux de la page
   (.auth-input, paquet 6). Les étapes se masquent par l'attribut hidden : aucune règle ne force leur display. */
.oubli-view .brand-header { margin-bottom: 22px; }
.oubli-logo { display: inline-block; margin-bottom: 12px; }
.oubli-titre {
  margin: 0 0 6px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.4px;
  color: var(--text);
}
.oubli-code { letter-spacing: 0.18em; font-variant-numeric: tabular-nums; }
.oubli-texte {
  margin: 0 0 16px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text2);
  overflow-wrap: anywhere;
}
.oubli-texte--essai {
  padding: 10px 12px;
  border: 1px dashed var(--yellow);
  border-radius: 10px;
  color: var(--text);
  font-size: 13px;
}
.oubli-liens {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 12px;
  margin-top: 10px;
}
.oubli-lien, .oubli-retour {
  min-height: 44px;
  padding: 0 4px;
  background: none;
  border: 0;
  font-family: 'Inter', sans-serif;
  font-size: 14px;
  font-weight: 600;
  color: var(--text2);
  cursor: pointer;
}
.oubli-lien:disabled { cursor: default; opacity: .75; }
.oubli-retour { display: block; margin: 14px auto 0; }
.oubli-retour[hidden] { display: none; }
.oubli-lien:focus-visible, .oubli-retour:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 2px;
  border-radius: 8px;
}
@media (hover: hover) {
  .oubli-lien:hover:not(:disabled), .oubli-retour:hover { color: var(--orange); }
}
.oubli-message {
  margin: 14px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  text-align: center;
}
.oubli-message--erreur { color: var(--red); }
.oubli-message--ok     { color: var(--green); }
[data-theme="light"] .oubli-message--erreur { color: #B91C1C; }   /* --red sur blanc : 3,8:1 seulement */
[data-theme="light"] .oubli-message--ok     { color: #15803D; }   /* --green sur blanc : 2,3:1 seulement */
.success-view.success-view--oubli { display: flex; }
/* Le lien « Mot de passe oublié ? » de login.html : zone de toucher portée à 44 px de haut
   sans rien déplacer (le texte reste à sa place, la zone déborde autour). */
.forgot-link { position: relative; }
.forgot-link::before { content: ''; position: absolute; inset: -14px -8px; }
/* inscription-commerce.html : « Mot de passe oublié ? » (vers login.html?oubli=vous, au « vous »),
   texte de 12 px : même agrandissement de la zone de toucher, sans rien déplacer. */
.qw-lien-oubli { position: relative; }
.qw-lien-oubli::before { content: ''; position: absolute; inset: -15px -6px; }

.qt-signup-form {
  width: min(420px, 100%);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin: clamp(0.9rem, 2vw, 1.3rem) 0 0;
}
.qt-signup-input {
  width: 100%;
  min-height: 50px;
  padding: 0 1.05rem;
  border-radius: calc(var(--qt-radius) - 10px);
  border: 1px solid var(--qt-border-strong);
  background: var(--qt-surface-soft);
  color: var(--qt-text);
  font-size: 1rem;
  font-weight: 600;
  transition: border-color var(--qt-dur) var(--qt-ease),
              box-shadow var(--qt-dur) var(--qt-ease);
}
.qt-signup-input::placeholder {
  color: var(--qt-text-muted);
  font-weight: 500;
}
.qt-signup-input:focus {
  outline: none;
  border-color: var(--qt-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--qt-accent) 22%, transparent);
}
.qt-signup-error {
  width: min(420px, 100%);
  margin-top: 0.2rem;
  padding: 0.55rem 0.85rem;
  border-radius: calc(var(--qt-radius) - 14px);
  background: color-mix(in srgb, var(--qt-danger) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--qt-danger) 45%, transparent);
  color: var(--qt-danger);
  font-size: 0.9rem;
  font-weight: 700;
  text-align: left;
}
.qt-signup-error:empty {
  display: none;
}
.signup-sub {
  max-width: 460px;
  margin-top: clamp(0.4rem, 1vw, 0.6rem);
  color: var(--qt-text-muted);
  font-size: clamp(0.95rem, 1.6vw, 1.05rem);
  line-height: 1.5;
  font-weight: 600;
}

.qt-flow-quiz {
  width: min(760px, 100%);
}

.qt-flow-quiz .options-container {
  width: min(680px, 100%);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--qt-s2);
  margin-top: var(--qt-s3);
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  animation: qtFlowContentIn 550ms cubic-bezier(0.16, 1, 0.3, 1) 260ms forwards;
}

.qt-flow-quiz .quiz-option-btn {
  appearance: none;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  width: 100%;
  min-height: 62px;
  padding: 1rem 3.7rem 1rem 1.1rem;
  border: 1px solid color-mix(in srgb, var(--qt-text) 11%, transparent);
  border-radius: calc(var(--qt-radius) - 8px);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 92%, var(--qt-accent) 8%),
      color-mix(in srgb, var(--qt-surface) 94%, var(--qt-text) 6%)
    );
  color: var(--qt-text);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 10%, transparent),
    0 18px 50px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent);
  cursor: pointer;
  font: inherit;
  font-size: clamp(0.96rem, 1.5vw, 1.08rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.015em;
  text-align: left;
  opacity: 0.94;
  transform: translate3d(0, 0, 0) scale(1);
  transition:
    transform 220ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 220ms ease,
    border-color 220ms ease,
    background 220ms ease,
    box-shadow 220ms ease;
}

.qt-flow-quiz .quiz-option-btn::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: inherit;
  background:
    linear-gradient(
      135deg,
      color-mix(in srgb, var(--qt-accent) 18%, transparent),
      transparent 58%
    );
  opacity: 0;
  transform: translate3d(-8px, 0, 0) scaleX(0.96);
  transform-origin: left center;
  transition:
    transform 260ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 220ms ease;
}

.qt-flow-quiz .quiz-option-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 1rem;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--qt-text) 16%, transparent);
  border-radius: var(--qt-radius-pill);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-soft) 86%, var(--qt-text) 14%),
      color-mix(in srgb, var(--qt-surface-elevated) 94%, var(--qt-text) 6%)
    );
  opacity: 0;
  transform: translate3d(8px, -50%, 0) scale(0.78);
  transition:
    transform 240ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 180ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .qt-flow-quiz .quiz-option-btn:hover {
    opacity: 1;
    transform: translate3d(0, -3px, 0) scale(1.01);
    border-color: color-mix(in srgb, var(--qt-accent) 52%, var(--qt-text) 10%);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, var(--qt-text) 13%, transparent),
      0 24px 70px color-mix(in srgb, var(--qt-overlay-bg) 62%, transparent),
      0 0 0 1px color-mix(in srgb, var(--qt-accent) 18%, transparent);
  }

  .qt-flow-quiz .quiz-option-btn:hover::before {
    opacity: 0.78;
    transform: translate3d(0, 0, 0) scaleX(1);
  }
}

.qt-flow-quiz .quiz-option-btn:active {
  opacity: 0.9;
  transform: translate3d(0, 1px, 0) scale(0.992);
}

.qt-flow-quiz .quiz-option-btn.is-success {
  opacity: 1;
  transform: translate3d(0, -3px, 0) scale(1.014);
  border-color: color-mix(in srgb, var(--qt-success) 88%, var(--qt-text) 12%);
  background:
    radial-gradient(
      circle at 18% 18%,
      color-mix(in srgb, var(--qt-success) 34%, transparent),
      transparent 44%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 64%, var(--qt-success) 36%),
      color-mix(in srgb, var(--qt-surface) 84%, var(--qt-success) 16%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 16%, transparent),
    0 26px 76px color-mix(in srgb, var(--qt-overlay-bg) 64%, transparent),
    0 0 0 1px color-mix(in srgb, var(--qt-success) 48%, transparent),
    0 0 0 8px color-mix(in srgb, var(--qt-success) 15%, transparent);
  animation: qtFlowSuccessPop 460ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.qt-flow-quiz .quiz-option-btn.is-success::before {
  opacity: 1;
  transform: translate3d(0, 0, 0) scaleX(1);
  background:
    linear-gradient(
      135deg,
      color-mix(in srgb, var(--qt-success) 22%, transparent),
      transparent 64%
    );
}

.qt-flow-quiz .quiz-option-btn.is-success::after {
  content: "✓";
  opacity: 1;
  transform: translate3d(0, -50%, 0) scale(1);
  border-color: color-mix(in srgb, var(--qt-success) 72%, var(--qt-text) 28%);
  background: var(--qt-success);
  color: var(--qt-surface);
  font-size: 1.2rem;
  font-weight: 950;
}

.qt-flow-quiz .quiz-option-btn.is-error {
  opacity: 0.98;
  border-color: color-mix(in srgb, var(--qt-danger) 86%, var(--qt-text) 14%);
  background:
    radial-gradient(
      circle at 18% 18%,
      color-mix(in srgb, var(--qt-danger) 32%, transparent),
      transparent 44%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 68%, var(--qt-danger) 32%),
      color-mix(in srgb, var(--qt-surface) 86%, var(--qt-danger) 14%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 13%, transparent),
    0 24px 70px color-mix(in srgb, var(--qt-overlay-bg) 62%, transparent),
    0 0 0 1px color-mix(in srgb, var(--qt-danger) 46%, transparent),
    0 0 0 8px color-mix(in srgb, var(--qt-danger) 14%, transparent);
  animation: qtFlowErrorShake 420ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}

.qt-flow-quiz .quiz-option-btn.is-error::before {
  opacity: 1;
  transform: translate3d(0, 0, 0) scaleX(1);
  background:
    linear-gradient(
      135deg,
      color-mix(in srgb, var(--qt-danger) 20%, transparent),
      transparent 64%
    );
}

.qt-flow-quiz .quiz-option-btn.is-error::after {
  content: "×";
  opacity: 1;
  transform: translate3d(0, -50%, 0) scale(1);
  border-color: color-mix(in srgb, var(--qt-danger) 72%, var(--qt-text) 28%);
  background: var(--qt-danger);
  color: var(--qt-surface);
  font-size: 1.35rem;
  font-weight: 950;
}

.qt-flow-quiz .quiz-option-btn:disabled {
  cursor: not-allowed;
  opacity: 0.42;
  transform: translate3d(0, 0, 0) scale(0.992);
  border-color: color-mix(in srgb, var(--qt-text-muted) 18%, transparent);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 76%, var(--qt-text-muted) 24%),
      color-mix(in srgb, var(--qt-surface) 86%, var(--qt-text-muted) 14%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 6%, transparent),
    0 10px 28px color-mix(in srgb, var(--qt-overlay-bg) 42%, transparent);
}

/* Réponses NON choisies une fois le quiz tranché (ni juste ni fausse). Avant :
   tout le bouton à 42 % d'opacité, texte compris (3,4:1). Ici le bouton reste
   opaque et chaque couleur de la boîte porte ce 42 % elle-même — même fond,
   même bordure, mêmes ombres qu'avant — tandis que le texte passe en gris
   atténué plein : lisible, et toujours bien en retrait de la bonne réponse.
   Les états juste / faux (.is-success / .is-error) sont exclus : inchangés. */
.qt-flow-quiz .quiz-option-btn:disabled:not(.is-success):not(.is-error) {
  opacity: 1;
  color: var(--qt-text-muted);
  border-color: color-mix(in srgb, var(--qt-text-muted) 8%, transparent);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, color-mix(in srgb, var(--qt-surface-elevated) 76%, var(--qt-text-muted) 24%) 42%, transparent),
      color-mix(in srgb, color-mix(in srgb, var(--qt-surface) 86%, var(--qt-text-muted) 14%) 42%, transparent)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 3%, transparent),
    0 10px 28px color-mix(in srgb, var(--qt-overlay-bg) 18%, transparent);
}
.qt-flow-quiz .quiz-option-btn:disabled:not(.is-success):not(.is-error)::after {
  opacity: 0.07;
}

.qt-flow-quiz .quiz-option-btn:disabled::before {
  opacity: 0;
  transform: translate3d(-8px, 0, 0) scaleX(0.96);
}

.qt-flow-quiz .quiz-option-btn:disabled::after {
  opacity: 0.16;
  transform: translate3d(0, -50%, 0) scale(0.9);
}

.qt-flow-quiz .quiz-option-btn.is-error:disabled {
  opacity: 0.78;
  border-color: color-mix(in srgb, var(--qt-danger) 58%, var(--qt-text-muted) 42%);
  background:
    radial-gradient(
      circle at 18% 18%,
      color-mix(in srgb, var(--qt-danger) 20%, transparent),
      transparent 44%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 76%, var(--qt-danger) 24%),
      color-mix(in srgb, var(--qt-surface) 88%, var(--qt-danger) 12%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 8%, transparent),
    0 16px 44px color-mix(in srgb, var(--qt-overlay-bg) 52%, transparent),
    0 0 0 6px color-mix(in srgb, var(--qt-danger) 10%, transparent);
}

.qt-flow-quiz .quiz-option-btn.is-error:disabled::after {
  opacity: 1;
  transform: translate3d(0, -50%, 0) scale(1);
}

.qt-flow-quiz .quiz-option-btn.is-success:disabled {
  opacity: 1;
  border-color: color-mix(in srgb, var(--qt-success) 88%, var(--qt-text) 12%);
  background:
    radial-gradient(
      circle at 18% 18%,
      color-mix(in srgb, var(--qt-success) 34%, transparent),
      transparent 44%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 64%, var(--qt-success) 36%),
      color-mix(in srgb, var(--qt-surface) 84%, var(--qt-success) 16%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 16%, transparent),
    0 26px 76px color-mix(in srgb, var(--qt-overlay-bg) 64%, transparent),
    0 0 0 1px color-mix(in srgb, var(--qt-success) 48%, transparent),
    0 0 0 8px color-mix(in srgb, var(--qt-success) 15%, transparent);
}

.qt-flow-quiz .quiz-option-btn.is-success:disabled::before {
  opacity: 1;
  transform: translate3d(0, 0, 0) scaleX(1);
}

.qt-flow-quiz .quiz-option-btn.is-success:disabled::after {
  opacity: 1;
  transform: translate3d(0, -50%, 0) scale(1);
}

@media (hover: hover) and (pointer: fine) {
  .qt-flow-quiz .quiz-option-btn:disabled:hover {
    opacity: 0.42;
    transform: translate3d(0, 0, 0) scale(0.992);
    border-color: color-mix(in srgb, var(--qt-text-muted) 18%, transparent);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, var(--qt-text) 6%, transparent),
      0 10px 28px color-mix(in srgb, var(--qt-overlay-bg) 42%, transparent);
  }

  .qt-flow-quiz .quiz-option-btn.is-error:disabled:hover {
    opacity: 0.78;
    transform: translate3d(0, 0, 0) scale(0.992);
    border-color: color-mix(in srgb, var(--qt-danger) 58%, var(--qt-text-muted) 42%);
  }

  .qt-flow-quiz .quiz-option-btn.is-success:disabled:hover {
    opacity: 1;
    transform: translate3d(0, -3px, 0) scale(1.014);
    border-color: color-mix(in srgb, var(--qt-success) 88%, var(--qt-text) 12%);
  }
}

/* « Continuer » AVANT qu'une réponse soit validée : visible dès l'affichage de
   la question, en bouton désactivé (même style que « J'ai suivi » avant
   « Suivre », règle partagée plus haut) et suivi de sa ligne d'aide. Il était
   invisible (opacité 0) : le joueur ne voyait pas qu'une étape suivait.
   LA CONDITION NE CHANGE PAS : c'est toujours l'attribut aria-hidden de la
   rangée, que questionnaire.js passe à "false" à la bonne réponse. Seul l'état
   « caché » devient un état « désactivé visible ». Un toucher ne fait rien :
   le gestionnaire du bouton n'avance que si la réponse est enregistrée.
   Une seule animation d'entrée (celle de .qt-flow-actions) dans les deux
   états : le passage à "false" ne la rejoue pas, le bouton change d'aspect
   sans clignoter. La ligne d'aide garde sa place une fois masquée : rien ne
   saute sous le doigt. */
.qt-flow-quiz .qt-flow-actions {
  flex-direction: column;
  gap: 0;
}
.qt-flow-quiz .qt-flow-actions[aria-hidden="true"] {
  pointer-events: none;
}
/* Le bouton seul reçoit le pointeur, pour afficher le curseur « interdit ». */
.qt-flow-quiz .qt-flow-actions[aria-hidden="true"] > .btn-primary {
  pointer-events: auto;
}
.qt-flow-quiz .qt-flow-actions[aria-hidden="false"] .qt-quiz-hint {
  visibility: hidden;
}

/* ═══ LOT 0171 — QUIZ À PLUSIEURS RÉPONSES ET SONDAGE ═══════════════════════
   Mêmes boutons que le quiz (.quiz-option-btn) et mêmes états juste / faux :
   seuls s'ajoutent l'état « coché » (.is-selected), le choix retenu d'un
   sondage (.is-chosen, sans juste ni faux) et les barres de résultats. La
   pastille de droite (::after) annonce la nature du choix AVANT le clic :
   carré = case à cocher (plusieurs réponses), rond = une seule réponse. */
.qt-flow-choix .qt-choix-consigne {
  margin: 0.35rem 0 0;
  color: var(--qt-text-muted);
  font-size: 0.92rem;
  font-weight: 700;
  text-align: center;
}
.qt-flow-choix .quiz-option-btn::after {
  opacity: 0.55;
  transform: translate3d(0, -50%, 0) scale(0.9);
}
.qt-flow-choix .quiz-option-btn[role="checkbox"]::after {
  border-radius: 9px;
}
/* « Coché » ne s'applique PAS à un choix déjà tranché : juste / faux (.is-success /
   .is-error) gardent leur couleur et leur pastille d'origine. */
.qt-flow-choix .quiz-option-btn.is-selected:not(.is-success):not(.is-error) {
  opacity: 1;
  border-color: color-mix(in srgb, var(--qt-accent) 78%, var(--qt-text) 22%);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 12%, transparent),
    0 18px 50px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent),
    0 0 0 1px color-mix(in srgb, var(--qt-accent) 42%, transparent);
}
.qt-flow-choix .quiz-option-btn.is-selected:not(.is-success):not(.is-error)::before {
  opacity: 1;
  transform: translate3d(0, 0, 0) scaleX(1);
}
.qt-flow-choix .quiz-option-btn.is-selected:not(.is-success):not(.is-error)::after,
.qt-flow-sondage .quiz-option-btn.is-chosen:disabled:not(.is-success):not(.is-error)::after {
  content: "✓";
  opacity: 1;
  transform: translate3d(0, -50%, 0) scale(1);
  border-color: color-mix(in srgb, var(--qt-accent) 72%, var(--qt-text) 28%);
  background: var(--qt-accent);
  color: var(--qt-surface);
  font-size: 1.1rem;
  font-weight: 950;
}
/* Sondage voté : le choix retenu reste lisible et marqué, sans vert ni rouge.
   Même sélecteur que la règle « réponses non choisies » du quiz (:disabled:not(…)),
   préfixé .qt-flow-sondage : elle l'emporte, sinon le choix retenu s'éteignait
   comme les autres (constaté sur capture, 390 px). */
.qt-flow-sondage .quiz-option-btn.is-chosen:disabled:not(.is-success):not(.is-error) {
  opacity: 1;
  color: var(--qt-text);
  border-color: color-mix(in srgb, var(--qt-accent) 70%, var(--qt-text) 30%);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 80%, var(--qt-accent) 20%),
      color-mix(in srgb, var(--qt-surface) 90%, var(--qt-accent) 10%)
    );
}
/* « Valider » tant que rien n'est coché : même aspect désactivé que « Continuer »
   avant la bonne réponse (règle partagée plus haut), le texte reste lisible. */
.qt-flow-choix .qt-flow-actions > .qt-choix-valider:disabled,
.qt-flow-choix .qt-flow-actions > .qt-choix-valider:disabled:hover,
.qt-flow-choix .qt-flow-actions > .qt-choix-valider:disabled:active {
  opacity: 1;
  background: transparent;
  border: 1.5px solid color-mix(in srgb, var(--qt-text-muted) 65%, transparent);
  color: var(--qt-text-muted);
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}
.qt-sondage-statut {
  width: min(680px, 100%);
  min-height: 1.35em;
  margin: 0.85rem 0 0;
  color: var(--qt-text-muted);
  font-size: 0.92rem;
  font-weight: 700;
  line-height: 1.35;
  text-align: center;
}
.qt-sondage-statut:empty {
  min-height: 0;
  margin: 0;
}
.qt-sondage-resultats {
  width: min(680px, 100%);
  margin: 0.8rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.qt-sondage-ligne {
  text-align: left;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.3rem 0.8rem;
  align-items: baseline;
}
.qt-sondage-libelle {
  color: var(--qt-text);
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.qt-sondage-pct {
  color: var(--qt-text);
  font-size: 0.95rem;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
}
.qt-sondage-barre {
  grid-column: 1 / -1;
  height: 8px;
  overflow: hidden;
  border-radius: var(--qt-radius-pill);
  background: color-mix(in srgb, var(--qt-text) 10%, transparent);
}
.qt-sondage-barre-remplie {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: color-mix(in srgb, var(--qt-accent) 70%, var(--qt-text) 30%);
  transform: scaleX(var(--qt-sondage-part, 0));
  transform-origin: left center;
  transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.qt-sondage-ligne.is-mien .qt-sondage-barre-remplie {
  background: var(--qt-accent);
}
.qt-sondage-note {
  width: min(680px, 100%);
  margin: 0.75rem 0 0;
  color: var(--qt-text-muted);
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
}
@media (prefers-reduced-motion: reduce) {
  .qt-sondage-barre-remplie {
    transition: none;
  }
}

.qt-reward-stage {
  overflow: hidden;
  width: min(760px, 100%);
  padding:
    clamp(1rem, 2.2vw, 1.45rem)
    clamp(0.8rem, 2vw, 1.6rem);
}

.qt-reward-stage > * {
  position: relative;
  z-index: 1;
}

.qt-reward-stage::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: -8%;
  left: 50%;
  width: min(560px, 82vw);
  height: min(330px, 48vw);
  border-radius: var(--qt-radius-pill);
  background:
    radial-gradient(
      circle at 50% 28%,
      color-mix(in srgb, var(--qt-accent) 24%, transparent),
      transparent 36%
    ),
    radial-gradient(
      circle at 50% 44%,
      color-mix(in srgb, var(--qt-success) 17%, transparent),
      transparent 58%
    );
  opacity: 0;
  transform: translate3d(-50%, 18px, 0) scale(0.92);
  animation: qtRewardAuraIn 900ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
  pointer-events: none;
}

.qt-reward-stage::after {
  content: "";
  position: absolute;
  top: 31%;
  left: 50%;
  z-index: 0;
  width: 7px;
  height: 7px;
  border-radius: var(--qt-radius-pill);
  background: var(--qt-accent);
  box-shadow:
    -300px -96px 0 0 color-mix(in srgb, var(--qt-accent) 88%, transparent),
    -250px -24px 0 1px color-mix(in srgb, var(--qt-success) 88%, transparent),
    -206px -142px 0 0 color-mix(in srgb, var(--qt-text) 72%, transparent),
    -166px 42px 0 1px color-mix(in srgb, var(--qt-accent) 82%, transparent),
    -118px -72px 0 0 color-mix(in srgb, var(--qt-success) 86%, transparent),
    -70px -156px 0 1px color-mix(in srgb, var(--qt-accent) 78%, transparent),
    -22px 62px 0 0 color-mix(in srgb, var(--qt-text) 66%, transparent),
    46px -118px 0 1px color-mix(in srgb, var(--qt-success) 82%, transparent),
    98px 32px 0 0 color-mix(in srgb, var(--qt-accent) 86%, transparent),
    154px -78px 0 1px color-mix(in srgb, var(--qt-text) 62%, transparent),
    204px -146px 0 0 color-mix(in srgb, var(--qt-success) 82%, transparent),
    256px 18px 0 1px color-mix(in srgb, var(--qt-accent) 82%, transparent),
    300px -92px 0 0 color-mix(in srgb, var(--qt-text) 64%, transparent);
  opacity: 0;
  transform: translate3d(-50%, 12px, 0) scale(0.56);
  animation: qtRewardFireworksIn 1280ms cubic-bezier(0.16, 1, 0.3, 1) 140ms both;
  pointer-events: none;
}

.reward-icon-container {
  position: relative;
  isolation: isolate;
  width: clamp(88px, 11vw, 112px);
  aspect-ratio: 1;
  margin: 0 auto var(--qt-s3);
  border: 1px solid color-mix(in srgb, var(--qt-success) 38%, var(--qt-text) 10%);
  border-radius: var(--qt-radius-pill);
  background:
    radial-gradient(
      circle at 50% 38%,
      color-mix(in srgb, var(--qt-text) 13%, transparent),
      transparent 24%
    ),
    radial-gradient(
      circle at 50% 48%,
      color-mix(in srgb, var(--qt-success) 34%, transparent),
      transparent 44%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 78%, var(--qt-success) 22%),
      color-mix(in srgb, var(--qt-surface) 88%, var(--qt-accent) 12%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 14%, transparent),
    inset 0 -16px 36px color-mix(in srgb, var(--qt-overlay-bg) 34%, transparent),
    0 24px 70px color-mix(in srgb, var(--qt-overlay-bg) 60%, transparent),
    0 0 0 7px color-mix(in srgb, var(--qt-success) 10%, transparent),
    0 0 52px color-mix(in srgb, var(--qt-success) 22%, transparent);
  opacity: 0;
  transform: translate3d(0, 16px, 0) scale(0.82);
  animation: qtRewardSealIn 760ms cubic-bezier(0.16, 1, 0.3, 1) 80ms forwards;
}

.reward-icon-container::before {
  content: "✓";
  position: absolute;
  z-index: 2;
  inset: 22%;
  display: grid;
  place-items: center;
  border-radius: var(--qt-radius-pill);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-success) 72%, var(--qt-accent) 28%),
      color-mix(in srgb, var(--qt-accent) 52%, var(--qt-success) 48%)
    );
  color: var(--qt-text);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 22%, transparent),
    0 14px 34px color-mix(in srgb, var(--qt-success) 26%, transparent);
  font-size: clamp(1.9rem, 4vw, 2.7rem);
  line-height: 1;
  font-weight: 950;
  opacity: 0;
  transform: scale(0.72);
  animation: qtRewardCheckIn 520ms cubic-bezier(0.16, 1, 0.3, 1) 360ms forwards;
}

.reward-icon-container::after {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 10%;
  border: 1px solid color-mix(in srgb, var(--qt-success) 54%, transparent);
  border-radius: var(--qt-radius-pill);
  background:
    radial-gradient(
      circle,
      transparent 54%,
      color-mix(in srgb, var(--qt-success) 20%, transparent) 56%,
      transparent 68%
    );
  opacity: 0;
  transform: scale(0.72);
  animation: qtRewardRingIn 720ms cubic-bezier(0.16, 1, 0.3, 1) 260ms forwards;
  pointer-events: none;
}

.reward-title {
  margin: 0;
  max-width: 760px;
  background:
    linear-gradient(
      180deg,
      var(--qt-text),
      color-mix(in srgb, var(--qt-text) 70%, var(--qt-accent) 30%)
    );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-size: clamp(1.9rem, 4vw, 3.6rem);
  line-height: 0.96;
  font-weight: 950;
  letter-spacing: -0.06em;
  text-wrap: balance;
  opacity: 0;
  transform: translate3d(0, 14px, 0) scale(0.985);
  animation: qtFlowContentIn 620ms cubic-bezier(0.16, 1, 0.3, 1) 210ms forwards;
}

.reward-subtitle {
  margin: var(--qt-s2) 0 0;
  max-width: 540px;
  color: var(--qt-text-muted);
  font-size: clamp(0.92rem, 1.4vw, 1.05rem);
  line-height: 1.45;
  font-weight: 600;
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  animation: qtFlowContentIn 560ms cubic-bezier(0.16, 1, 0.3, 1) 310ms forwards;
}

.qt-xp-engine {
  position: relative;
  overflow: hidden;
  width: min(560px, 100%);
  margin-top: var(--qt-s3);
  padding: clamp(0.72rem, 1.5vw, 0.95rem);
  border: 1px solid color-mix(in srgb, var(--qt-border) 72%, var(--qt-accent) 28%);
  border-radius: calc(var(--qt-radius) - 6px);
  background:
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 88%, var(--qt-accent) 12%),
      color-mix(in srgb, var(--qt-surface) 92%, var(--qt-success) 8%)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 10%, transparent),
    0 18px 58px color-mix(in srgb, var(--qt-overlay-bg) 54%, transparent),
    0 0 0 1px color-mix(in srgb, var(--qt-accent) 10%, transparent);
  opacity: 0;
  transform: translate3d(0, 18px, 0) scale(0.98);
  animation: qtFlowPanelIn 620ms cubic-bezier(0.16, 1, 0.3, 1) 420ms forwards;
}

.qt-xp-engine + .qt-xp-engine {
  margin-top: var(--qt-s1);
  background:
    radial-gradient(
      circle at 84% 0%,
      color-mix(in srgb, var(--qt-accent) 14%, transparent),
      transparent 46%
    ),
    linear-gradient(
      145deg,
      color-mix(in srgb, var(--qt-surface-elevated) 90%, var(--qt-accent) 10%),
      color-mix(in srgb, var(--qt-surface) 94%, var(--qt-success) 6%)
    );
  animation-delay: 520ms;
}

.xp-meta {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.62rem;
}

.xp-label {
  color: var(--qt-text-muted);
  font-size: 0.75rem;
  font-weight: 850;
  letter-spacing: 0.075em;
  text-transform: uppercase;
}

.xp-value {
  color: var(--qt-accent);
  font-size: clamp(1rem, 1.8vw, 1.25rem);
  line-height: 1;
  font-weight: 950;
  letter-spacing: -0.035em;
  text-shadow:
    0 0 22px color-mix(in srgb, var(--qt-accent) 42%, transparent);
}

.qt-xp-engine + .qt-xp-engine .xp-value {
  color: var(--qt-text);
  text-shadow:
    0 0 22px color-mix(in srgb, var(--qt-accent) 34%, transparent);
}

.qt-bar-track {
  position: relative;
  z-index: 1;
  overflow: hidden;
  width: 100%;
  height: 18px;
  padding: 3px;
  border: 1px solid color-mix(in srgb, var(--qt-border) 76%, var(--qt-text) 24%);
  border-radius: var(--qt-radius-pill);
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--qt-surface) 82%, var(--qt-overlay-bg) 18%),
      color-mix(in srgb, var(--qt-surface-soft) 76%, var(--qt-overlay-bg) 24%)
    );
  box-shadow:
    inset 0 2px 8px color-mix(in srgb, var(--qt-overlay-bg) 52%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 10%, transparent),
    0 0 34px color-mix(in srgb, var(--qt-success) 12%, transparent);
}

.qt-bar-track::before {
  content: "";
  position: absolute;
  inset: 3px;
  z-index: 2;
  border-radius: inherit;
  background:
    repeating-linear-gradient(
      90deg,
      color-mix(in srgb, var(--qt-text) 12%, transparent) 0 1px,
      transparent 1px 16px
    );
  opacity: 0.34;
  pointer-events: none;
}

.qt-bar-fill {
  position: relative;
  overflow: hidden;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--qt-radius-pill);
  transform-origin: left center;
  background:
    radial-gradient(
      circle at 18% 50%,
      color-mix(in srgb, var(--qt-text) 22%, transparent),
      transparent 24%
    ),
    linear-gradient(
      90deg,
      var(--qt-success),
      color-mix(in srgb, var(--qt-success) 58%, var(--qt-accent) 42%),
      var(--qt-accent)
    );
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 24%, transparent),
    inset 0 -7px 14px color-mix(in srgb, var(--qt-overlay-bg) 24%, transparent),
    0 0 26px color-mix(in srgb, var(--qt-success) 32%, transparent),
    0 0 44px color-mix(in srgb, var(--qt-accent) 22%, transparent);
  will-change: transform, opacity;
}

.qt-bar-fill::before {
  content: "";
  position: absolute;
  inset: 0;
  width: 46%;
  border-radius: inherit;
  background:
    linear-gradient(
      90deg,
      transparent,
      color-mix(in srgb, var(--qt-text) 28%, transparent),
      transparent
    );
  opacity: 0;
  transform: translate3d(-120%, 0, 0);
  animation: qtBarShine 2100ms cubic-bezier(0.16, 1, 0.3, 1) 760ms infinite;
}

.reward-subtitle + .qt-xp-engine .qt-bar-fill {
  animation: qtQuestXpFill 1150ms cubic-bezier(0.16, 1, 0.3, 1) 580ms both;
}

.reward-subtitle + .qt-xp-engine + .qt-xp-engine .qt-bar-fill {
  background:
    radial-gradient(
      circle at 18% 50%,
      color-mix(in srgb, var(--qt-text) 18%, transparent),
      transparent 24%
    ),
    linear-gradient(
      90deg,
      var(--qt-accent),
      color-mix(in srgb, var(--qt-accent) 56%, var(--qt-success) 44%),
      var(--qt-success)
    );
  animation: qtPlayerXpGain 1050ms cubic-bezier(0.16, 1, 0.3, 1) 820ms both;
}

.qt-flow-completed .qt-flow-icon {
  color: var(--qt-text);
  font-size: clamp(2.5rem, 5vw, 3.7rem);
  text-shadow:
    0 0 30px color-mix(in srgb, var(--qt-accent) 42%, transparent),
    0 0 42px color-mix(in srgb, var(--qt-success) 28%, transparent);
}

.qt-flow-completed .qt-xp-engine {
  width: min(560px, 100%);
  margin-top: var(--qt-s3);
  animation: none;
  opacity: 1;
  transform: none;
}

.qt-flow-completed .qt-xp-engine + .qt-xp-engine {
  margin-top: var(--qt-s1);
}

.qt-flow-completed .qt-bar-fill {
  animation: none;
  opacity: 1;
  transform: scaleX(1);
}

.qt-flow-completed .qt-bar-fill::before {
  animation: none;
  opacity: 0;
}

@media (max-width: 760px) {
  .qt-flow-screen,
  .qt-reward-stage {
    width: 100%;
    padding: 0.85rem 0.05rem;
  }

  /* Le halo du bouton « Récupérer » était tranché net par ce cadre : un
     rectangle sombre autour du bouton. Sur mobile, rien d'autre n'a besoin
     d'y être rogné — les feux d'artifice sont masqués sous 420px et
     s'éteignent en 1,3s au-dessus, l'aura ne déborde que dans le rembourrage
     de la scène. Le bureau garde son `overflow: hidden`. */
  .qt-reward-stage {
    overflow: visible;
  }

  /* Titre d'étape = 1,618 × texte courant (16px à 375px → 26px). Interlettrage
     normal : le serrage d'affiche ne vaut qu'aux grandes tailles du bureau. */
  :root {
    --qt-title-size:   calc(1.618 * clamp(15px, 4.27vw, 17px));
    --qt-title-lh:     1.15;
    --qt-title-weight: 800;
    --qt-title-ls:     normal;
  }

  .qt-flow-icon {
    width: 60px;
    margin-bottom: var(--qt-s2);
    font-size: 1.7rem;
  }

  /* La question : 1,618 × le texte des réponses (réduit à 1,5 × sous 381px,
     voir plus bas). Taille des titres d'étape : jetons ci-dessus. */
  .qt-flow-quiz .question-title {
    font-size: calc(1.618 * clamp(15px, 4vw, 16px));
  }

  .qt-flow-kicker {
    margin-bottom: var(--qt-s1);
  }

  .qt-flow-subtitle {
    margin-top: var(--qt-s1);
    font-size: clamp(15px, 4.27vw, 17px);
    line-height: 1.5;
    letter-spacing: normal;
  }

  .qt-flow-quiz .qt-flow-subtitle {
    margin-bottom: var(--qt-s1);
  }

  .qt-flow-card {
    margin-top: var(--qt-s2);
    padding: 0.78rem 0.82rem;
  }

  .qt-video-frame {
    margin-top: var(--qt-s2);
  }

  .qt-flow-actions,
  .qt-reward-actions {
    margin-top: var(--qt-s2);
    flex-wrap: wrap;
  }

  .qt-stage-body .btn-primary,
  .qt-stage-body .btn-ghost {
    min-height: 48px;
  }

  /* En mobile le bouton occupe toute la largeur — c'était déjà le cas avant la
     mise en hauteur bornée, on le rétablit explicitement. */
  .qt-stage-body .qt-flow-actions > .btn-primary,
  .qt-stage-body .qt-reward-actions > .btn-primary {
    width: 100%;
  }

  .qt-flow-quiz .options-container {
    grid-template-columns: 1fr;
    gap: var(--qt-s1);
    margin-top: var(--qt-s1);
  }

  .qt-flow-quiz .quiz-option-btn {
    min-height: 52px;
    padding: 0.8rem 3.25rem 0.8rem 1rem;
    font-size: clamp(15px, 4vw, 16px);
    line-height: 1.25;
    letter-spacing: normal;
    font-weight: 700;
  }

  .reward-icon-container {
    width: 76px;
    margin-bottom: var(--qt-s2);
  }

  .reward-title {
    font-size: var(--qt-title-size);
    line-height: 1.15;
    letter-spacing: -0.01em;
  }

  .reward-subtitle {
    margin-top: var(--qt-s1);
    font-size: 0.95rem;
  }

  .qt-xp-engine,
  .qt-flow-completed .qt-xp-engine {
    margin-top: var(--qt-s2);
    padding: 0.78rem;
  }

  .xp-meta {
    gap: 0.5rem;
  }

  .xp-label {
    font-size: 0.68rem;
  }

  .xp-value {
    font-size: 1rem;
  }

  .qt-bar-track {
    height: 17px;
  }
}

@media (max-width: 420px) {
  .qt-flow-icon {
    width: 56px;
  }

  .qt-flow-card p {
    font-size: 0.9rem;
  }

  .qt-flow-actions,
  .qt-reward-actions {
    gap: 0.55rem;
  }

  .qt-stage-body .btn-ghost {
    min-width: min(130px, 40vw);
  }

  .qt-reward-stage::after {
    display: none;
  }

  .reward-icon-container {
    width: 72px;
  }

  .qt-xp-engine {
    padding: 0.72rem;
  }
}

@media (max-width: 380px) {
  /* Petits téléphones : la question à 1,5 × les réponses (22,5px à 375px).
     À 1,618 × (24,3px), une question de ~80 caractères prend une 5e ligne et
     « Continuer » sortait de l'écran à 375×667 (mesuré : bas à 691px). */
  .qt-flow-quiz .question-title {
    font-size: calc(1.5 * clamp(15px, 4vw, 16px));
  }
}

@media (max-width: 480px) {
  /* Social : « Suivre » et « J'ai suivi, continuer » l'un sous l'autre. Côte
     à côte, chacun n'avait que ~146px et le second se cassait sur deux lignes
     dans sa pastille. */
  .qt-stage-body .qt-flow-actions .qt-social-follow,
  .qt-stage-body .qt-flow-actions .qt-social-validate-wrap {
    flex: 1 1 100%;
  }

  .qt-stage-body .btn-primary,
  .qt-stage-body .btn-ghost {
    padding: 0 1.2rem;
  }
}

@keyframes qtFlowIn {
  from {
    opacity: 0;
    transform: translate3d(0, 14px, 0) scale(0.985);
  }

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

@keyframes qtFlowIconPop {
  0% {
    opacity: 0;
    transform: translate3d(0, 12px, 0) scale(0.84);
  }

  62% {
    opacity: 1;
    transform: translate3d(0, -4px, 0) scale(1.055);
  }

  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }
}

@keyframes qtFlowContentIn {
  from {
    opacity: 0;
    transform: translate3d(0, 10px, 0) scale(0.985);
  }

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

@keyframes qtFlowPanelIn {
  from {
    opacity: 0;
    transform: translate3d(0, 12px, 0) scale(0.98);
  }

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

@keyframes qtFlowSuccessPop {
  0% {
    transform: translate3d(0, -1px, 0) scale(0.985);
  }

  58% {
    transform: translate3d(0, -5px, 0) scale(1.026);
  }

  100% {
    transform: translate3d(0, -3px, 0) scale(1.014);
  }
}

@keyframes qtFlowErrorShake {
  0%,
  100% {
    transform: translate3d(0, 0, 0) scale(1);
  }

  18% {
    transform: translate3d(-7px, 0, 0) scale(1.006);
  }

  36% {
    transform: translate3d(6px, 0, 0) scale(1.006);
  }

  54% {
    transform: translate3d(-4px, 0, 0) scale(1.003);
  }

  72% {
    transform: translate3d(3px, 0, 0) scale(1.003);
  }
}

@keyframes qtRewardAuraIn {
  from {
    opacity: 0;
    transform: translate3d(-50%, 18px, 0) scale(0.92);
  }

  to {
    opacity: 1;
    transform: translate3d(-50%, 0, 0) scale(1);
  }
}

@keyframes qtRewardFireworksIn {
  0% {
    opacity: 0;
    transform: translate3d(-50%, 18px, 0) scale(0.56);
  }

  16% {
    opacity: 1;
  }

  72% {
    opacity: 0.72;
  }

  100% {
    opacity: 0;
    transform: translate3d(-50%, -46px, 0) scale(1.18);
  }
}

@keyframes qtRewardSealIn {
  0% {
    opacity: 0;
    transform: translate3d(0, 16px, 0) scale(0.82);
  }

  62% {
    opacity: 1;
    transform: translate3d(0, -5px, 0) scale(1.055);
  }

  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }
}

@keyframes qtRewardCheckIn {
  0% {
    opacity: 0;
    transform: scale(0.72);
  }

  68% {
    opacity: 1;
    transform: scale(1.08);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes qtRewardRingIn {
  0% {
    opacity: 0;
    transform: scale(0.72);
  }

  70% {
    opacity: 0.9;
    transform: scale(1.08);
  }

  100% {
    opacity: 0.72;
    transform: scale(1);
  }
}

@keyframes qtQuestXpFill {
  from {
    opacity: 0.72;
    transform: scaleX(0);
  }

  to {
    opacity: 1;
    transform: scaleX(var(--qt-xp-to, 1));
  }
}

@keyframes qtPlayerXpGain {
  from {
    opacity: 0.88;
    transform: scaleX(var(--qt-xp-from, 0.70));
  }

  to {
    opacity: 1;
    transform: scaleX(var(--qt-xp-to, 0.75));
  }
}

@keyframes qtBarShine {
  0% {
    opacity: 0;
    transform: translate3d(-120%, 0, 0);
  }

  24% {
    opacity: 0.82;
  }

  100% {
    opacity: 0;
    transform: translate3d(230%, 0, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .qt-flow-screen,
  .qt-flow-icon,
  .qt-flow-title,
  .qt-flow-subtitle,
  .qt-flow-card,
  .qt-flow-actions,
  .qt-reward-actions,
  .qt-flow-quiz .options-container,
  .qt-flow-quiz .quiz-option-btn.is-success,
  .qt-flow-quiz .quiz-option-btn.is-error,
  .qt-reward-stage::before,
  .qt-reward-stage::after,
  .reward-icon-container,
  .reward-icon-container::before,
  .reward-icon-container::after,
  .reward-title,
  .reward-subtitle,
  .qt-xp-engine,
  .qt-bar-fill,
  .qt-bar-fill::before {
    animation: none;
  }

  .qt-flow-screen,
  .qt-flow-icon,
  .qt-flow-title,
  .qt-flow-subtitle,
  .qt-flow-card,
  .qt-flow-actions,
  .qt-reward-actions,
  .qt-flow-quiz .options-container,
  .reward-icon-container,
  .reward-icon-container::before,
  .reward-title,
  .reward-subtitle,
  .qt-xp-engine {
    opacity: 1;
    transform: none;
  }

  .qt-reward-stage::before {
    opacity: 1;
    transform: translate3d(-50%, 0, 0) scale(1);
  }

  .qt-reward-stage::after {
    opacity: 0;
  }

  .reward-icon-container::after {
    opacity: 0.72;
    transform: none;
  }

  .reward-subtitle + .qt-xp-engine .qt-bar-fill {
    transform: scaleX(var(--qt-xp-to, 1));
  }

  .reward-subtitle + .qt-xp-engine + .qt-xp-engine .qt-bar-fill {
    transform: scaleX(var(--qt-xp-to, 0.75));
  }

  .qt-flow-completed .qt-bar-fill {
    transform: scaleX(1);
  }
}

/* Toast d'erreur du tunnel (avant ouverture) */
.qt-toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10002;
  background: var(--red);
  color: #fff;
  padding: 14px 24px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 700;
  max-width: 90vw;
  text-align: center;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}

/* ════════════════════════════════════════════════════════════════════════
   PORTEFEUILLE JOUEUR — mes-bons.html (le « coffre au trésor »)
   Dark mode glassmorphism · or (--yellow) = loot · cadenas/flou = verrouillé.
   Tout repose sur les tokens du :root. Préfixe .wal- → zéro collision globale.
   ════════════════════════════════════════════════════════════════════════ */

/* Layout : colonne unique centrée (~1100px), neutralise la grille d'activité
   et l'état "sidebar-open" persisté (cette page n'a pas de panneau de droite). */
.dt-body.wal-body,
body.sidebar-open .dt-body.wal-body {
  grid-template-columns: minmax(0, 1100px) !important;
  justify-content: center !important;
  gap: 0 !important;
  max-width: none !important;
}

/* Helpers chrome (remplacent les style= inline du markup copié) */
/* Croix du burger. Elle n'avait ni width, ni height, ni padding : sa zone
   tactile se réduisait au glyphe, ~13×24px. `.side-close` n'existe que dans
   #sideMenu (shared.js:1953) — pas besoin de la borner davantage.
   44×44 est la zone visée ; les marges négatives de 4px la ramènent à 36px de
   hauteur DANS LE FLUX, sinon c'est elle qui dicterait la hauteur de l'en-tête
   qu'on vient justement de resserrer. Le -10px à droite rattrape le
   décentrage : le glyphe est au milieu de 44px, il s'éloignerait sinon du bord.
   Le glyphe descend à 18px — la cible grandit, le dessin non. */
.side-close { background: none; border: none; font-size: 18px; cursor: pointer; color: var(--text2);
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  margin: -4px -10px -4px 0; border-radius: 12px; flex-shrink: 0;
  transition: color .3s ease, background-color .3s ease; }
/* Sans fond, les 44px de cible et le border-radius étaient invisibles : rien
   ne disait où l'on pouvait appuyer. Même idiome que `.side-link:hover` du
   même menu (var(--bg3) + texte plein) — on n'invente pas un langage pour un
   seul bouton. `:focus-visible` et pas `:focus` : le clavier voit la cible,
   la souris ne laisse pas de fond collé après le clic. */
.side-close:hover,
.side-close:focus-visible { background: var(--bg3); color: var(--text); }
.app-nav-logo { cursor: pointer; }

.wallet { display: flex; flex-direction: column; gap: 26px; width: 100%; min-width: 0; }
.wal-page-title { font-size: var(--text-hero); font-weight: 900; letter-spacing: -1px; margin: 0; }
.wal-page-sub { font-size: var(--text-base); color: var(--text2); margin: 6px 0 0; }

/* ── Section générique ── */
.wal-section { display: flex; flex-direction: column; gap: 16px; }
.wal-section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.wal-section-title { font-size: var(--text-lg); font-weight: 900; letter-spacing: -.4px; margin: 0; display: flex; align-items: center; gap: 9px; }
.wal-section-title .wal-section-emoji { font-size: 18px; }
.wal-section-sub { font-size: var(--text-sm); color: var(--text3); font-weight: 600; }
/* ── A. Hero « Fierté » — vitrine de l'accumulation ── */
.wal-hero {
  position: relative; overflow: hidden;
  background: linear-gradient(135deg, rgba(124,58,237,.20), rgba(255,107,43,.10)), var(--bg2);
  border: 1px solid var(--border2); border-radius: var(--radius-lg);
  padding: 30px 32px; backdrop-filter: blur(8px);
  box-shadow: 0 24px 60px -28px rgba(0,0,0,.85);
}
.wal-hero::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(70% 130% at 100% 0%, rgba(245,158,11,.20), transparent 60%);
}
.wal-hero-eyebrow {
  position: relative; z-index: 1;
  font-size: 10px; font-weight: 900; letter-spacing: 1.4px; text-transform: uppercase; color: var(--yellow);
  display: inline-flex; align-items: center; gap: 7px;
}
.wal-hero-title { position: relative; z-index: 1; font-size: var(--text-xl); font-weight: 900; letter-spacing: -.6px; margin: 10px 0 4px; }
.wal-hero-text { position: relative; z-index: 1; font-size: var(--text-sm); color: var(--text2); margin: 0; max-width: 560px; line-height: 1.5; }

/* Quatre compteurs depuis que « Expirés » complète l'addition — gagnés =
   utilisés + à récupérer + expirés. DEUX colonnes à toutes les largeurs, et
   c'est un choix mesuré, pas un pis-aller :

   · Une rangée de quatre ne tient pas. Sur cette page, `.wal-hero` ne fait que
     572px de large en bureau, et non les ~1100 attendus — une règle générique
     de la landing, `section{margin:0 auto}` (≥1024px), rétrécit et centre les
     sections du portefeuille, qui sont des éléments flex. Quatre colonnes y
     tomberaient à 116px, sous la largeur de « BONS UTILISÉS », qui passerait à
     la ligne. (Ce rétrécissement est une anomalie antérieure à ce lot ; s'il
     est corrigé un jour, cette grille reste correcte.)
   · `auto-fit` non plus : à 572px il donne TROIS pistes, donc trois compteurs
     alignés et un quatrième seul en dessous. Une addition qu'on veut lire d'un
     coup ne se présente pas en 3 + 1.

   2×2 se lit comme un bilan, ne coupe aucun libellé, et ne dépend d'aucune
   largeur de conteneur. */
.wal-stats { position: relative; z-index: 1; display: grid;
  grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 24px; }
.wal-stat {
  background: rgba(255,255,255,.045); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: 16px 18px;
  display: flex; flex-direction: column; gap: 6px;
}
.wal-stat-val { font-size: var(--text-xl); font-weight: 900; letter-spacing: -.6px; color: var(--text); line-height: 1; }
.wal-stat-val.is-gold { color: var(--yellow); text-shadow: 0 0 18px rgba(245,158,11,.35); }
/* Le compteur des expirés est en veilleuse, comme les cartes de l'historique :
   c'est une information de bilan, pas un appel à l'action. Rien de rouge — un
   bon périmé n'est pas une erreur du joueur. */
.wal-stat-val.is-spent { color: var(--text3); }
.wal-stat-lbl { font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .7px; color: var(--text3); }

/* ── B. Bons prêts à l'emploi (FOMO) ── */
.wal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(258px, 1fr)); gap: 16px; }
.wal-coupon {
  position: relative; overflow: hidden;
  background: var(--bg2); border: 1px solid var(--border2); border-radius: var(--radius-lg);
  padding: 18px; display: flex; flex-direction: column; gap: 14px;
  transition: transform .25s var(--ease), border-color .25s, box-shadow .25s;
}
.wal-coupon::after {
  content: ''; position: absolute; top: -45%; right: -30%; width: 170px; height: 170px;
  background: radial-gradient(circle, rgba(245,158,11,.18), transparent 70%); pointer-events: none;
}
.wal-coupon:hover { transform: translateY(-4px); border-color: rgba(245,158,11,.42); box-shadow: 0 18px 44px -22px rgba(245,158,11,.55); }
.wal-coupon-top { display: flex; align-items: center; gap: 11px; position: relative; z-index: 1; }
.wal-brand-logo {
  width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 17px; font-weight: 900; color: #fff;
  background: linear-gradient(135deg, var(--orange), var(--violet));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2);
}
/* Le vrai logo du commerce. L'URL arrive par --wal-logo, posée en JS après
   l'injection (mes-bons.html, walPeindreLogos) : seule la donnée est en ligne,
   la mise en forme reste ici — même partage que --ls-logo et les --qt-* du §9.

   `contain` et non `cover` : un logo se voit en entier ou ne se voit pas. Les
   logos de commerce sont carrés (400×400, profil `contain` de ccResizeImage),
   mais un ancien téléversement peut ne pas l'être, et `cover` lui couperait la
   moitié du mot. Le dégradé cède la place à un fond neutre : derrière un logo à
   fond transparent — la plupart en ont un — l'orange-violet se serait invité
   dans la marque du commerçant. */
.wal-brand-logo--img {
  background: var(--wal-logo) center / contain no-repeat, var(--bg3);
  box-shadow: inset 0 0 0 1px var(--border);
}
.wal-brand-name { font-size: var(--text-sm); font-weight: 800; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wal-coupon-value { position: relative; z-index: 1; font-size: 30px; font-weight: 900; letter-spacing: -1px; color: var(--yellow); line-height: 1; }
.wal-coupon-title { position: relative; z-index: 1; font-size: var(--text-base); font-weight: 700; color: var(--text); margin: -4px 0 0; line-height: 1.3; }
.wal-coupon-foot { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 4px; }
.wal-expiry {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-xs); font-weight: 700; color: var(--text2);
  background: var(--bg3); border: 1px solid var(--border); border-radius: 20px; padding: 6px 11px;
}
.wal-expiry svg { width: 13px; height: 13px; }
.wal-expiry.is-urgent { color: var(--red); background: rgba(239,68,68,.12); border-color: rgba(239,68,68,.3); animation: walPulse 1.8s var(--ease) infinite; }
@keyframes walPulse { 0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,.0); } 50% { box-shadow: 0 0 0 4px rgba(239,68,68,.14); } }
.wal-use-btn {
  display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
  font-size: var(--text-sm); font-weight: 800; color: #fff; text-decoration: none;
  background: linear-gradient(135deg, var(--orange), var(--orange2));
  border-radius: 11px; padding: 9px 16px;
  box-shadow: 0 6px 18px -6px rgba(255,107,43,.6);
  transition: transform .2s var(--ease), box-shadow .2s;
}
.wal-use-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 24px -8px rgba(255,107,43,.75); }
.wal-use-btn svg { width: 15px; height: 15px; }

/* ── Bons consommés ou périmés : même carte, éteinte, sans action ──
   État piloté par statut_effectif, calculé par get_my_rewards() côté serveur.
   Le front ne compare aucune date pour décider de l'apparence d'un bon. */
.wal-coupon.is-used,
.wal-coupon.is-expired { opacity: .58; filter: grayscale(.6); }
.wal-coupon.is-used:hover,
.wal-coupon.is-expired:hover { transform: none; border-color: var(--border); box-shadow: none; }
.wal-coupon.is-used .wal-coupon-value,
.wal-coupon.is-expired .wal-coupon-value { color: var(--text2); }
.wal-coupon-state {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-xs); font-weight: 700; color: var(--text3);
  background: var(--bg3); border: 1px solid var(--border); border-radius: 20px; padding: 6px 11px;
}
.wal-coupon-state.is-ok { color: var(--green); }
.wal-coupon-state svg { width: 13px; height: 13px; }

/* ── États vides ── */
.wal-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
  background: var(--bg2); border: 1px dashed var(--border2); border-radius: var(--radius-lg);
  padding: 38px 24px;
}
.wal-empty-icon { font-size: 40px; opacity: .9; }
.wal-empty-title { font-size: var(--text-base); font-weight: 800; color: var(--text); margin: 0; }
.wal-empty-text { font-size: var(--text-sm); color: var(--text3); margin: 0; max-width: 380px; line-height: 1.5; }
.wal-empty-cta {
  margin-top: 6px; display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--text-sm); font-weight: 800; color: #fff; text-decoration: none;
  background: linear-gradient(135deg, var(--orange), var(--orange2));
  border-radius: 11px; padding: 11px 20px; box-shadow: 0 8px 22px -8px rgba(255,107,43,.6);
  transition: transform .2s var(--ease);
}
.wal-empty-cta:hover { transform: translateY(-1px); }

/* ── Pied de page ── */
.wal-foot { margin-top: 8px; padding: 20px 0 4px; text-align: center; font-size: var(--text-xs); color: var(--text3); }

/* ── Responsive ──
   Le framework masque .dt-main et révèle #mobileContent sous 768px. Cette page
   n'a PAS de bloc #mobileContent séparé (zéro duplication de contenu) : on garde
   donc SON .dt-main visible — scopé via :has(.wal-body) pour ne toucher aucune
   autre page — afin d'afficher le portefeuille pleine largeur sous la barre
   mobile. La barre desktop (.dt-topbar) reste masquée ; .app-nav/.bottom-nav la
   remplacent (markup déjà présent dans la page).

   ⚠️ Le sélecteur est préfixé #app : une règle plus récente du framework,
   « #app.logged-in-layout .dt-main { display:none !important } » (spécificité
   1-2-0), l'emportait sur la version en .logged-in-layout (0-3-0) et rendait le
   portefeuille INVISIBLE sous 768px — sur l'écran où il sert le plus. Deux
   !important de même origine se départagent à la spécificité : il faut donc
   repasser devant, sans quoi le :has(.wal-body) ne sert à rien. */
@media (max-width: 768px) {
  #app.logged-in-layout .dt-main:has(.wal-body) { display: flex !important; flex-direction: column; margin-left: 0 !important; }
  .dt-body.wal-body { display: block; padding: 18px 14px 96px; }
  /* Les colonnes ne sont PAS redéclarées : le 2×2 vient déjà de la règle de
     base. Ne se resserrent ici que l'écart et le rembourrage, pour que
     « BONS UTILISÉS » tienne sur une ligne à 360px — la largeur la plus étroite
     qu'on vise. (La règle d'avant posait `1fr`, soit quatre compteurs empilés,
     qui repoussaient les cartes de bons sous la ligne de flottaison.) */
  .wal-stats { gap: 10px; }
  .wal-stat { padding: 14px 12px; }
  .wal-page-title { font-size: var(--text-xl); }
  .wal-hero { padding: 24px 20px; }
  .wal-grid { grid-template-columns: 1fr; }
}

/* Miniature d'aperçu après upload d'image (Lot C — helper ccUploadFile / previewContainerId) */
.cc-thumb{max-width:120px;max-height:80px;border-radius:10px;object-fit:cover;margin-top:8px;border:1px solid var(--border2,#2a2a35)}

/* Bouton « ✕ Supprimer » d'un champ image (vide input + miniature) — muet, rouge au survol */
.cc-clear-btn{display:inline-flex;align-items:center;gap:6px;margin-top:6px;margin-left:6px;padding:8px 12px;border-radius:10px;border:1px solid var(--border2,#2a2a35);background:transparent;color:var(--text2,#8a8a99);font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;font-family:'Inter',sans-serif}
.cc-clear-btn:hover{color:var(--red,#EF4444);border-color:var(--red,#EF4444);background:rgba(239,68,68,.08)}

/* ════════════════════════════════════════════════════════════════════════
   BLOC « BON À GAGNER » — formulaires quête et campagne (G2-1, mutualisé G2-2)
   Écrit d'abord dans le <style> de merchant-dashboard.html, déplacé ici quand
   admin.html a dû afficher le même bloc : les deux pages chargent style.css,
   et une seconde copie aurait divergé au premier ajustement.

   Volontairement INDÉPENDANT du contexte de formulaire — merchant-dashboard
   utilise .md-field, admin.html .form-group. Aucun sélecteur ne suppose l'un
   ni l'autre.

   .md-check input[type="checkbox"] pèse (0,2,1) et l'emporte donc sur le
   .md-field input (0,1,1) du dashboard, qui collerait à la case le padding
   d'un champ texte — quel que soit l'ordre de chargement des feuilles.
   ════════════════════════════════════════════════════════════════════════ */
.md-check{display:flex;flex-direction:row;align-items:center;gap:10px;cursor:pointer;font-size:13px;font-weight:700;color:var(--text)}
.md-check input[type="checkbox"]{width:18px;height:18px;flex-shrink:0;padding:0;margin:0;accent-color:var(--orange);cursor:pointer}
.md-bon-fields{display:flex;flex-direction:column;gap:12px;margin-top:12px;padding-left:28px;border-left:2px solid var(--border2)}
.md-bon-fields[hidden]{display:none}   /* l'attribut hidden doit gagner sur le display:flex ci-dessus */
.md-bon-note{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.5;color:var(--text2);background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.24);border-radius:10px;padding:10px 12px;margin:0}
.md-bon-note strong{color:var(--yellow)}
.md-bon-err{margin:0;font-size:12px;font-weight:600;line-height:1.4;color:#ff6b6b}
.md-bon-err[hidden]{display:none}

/* ════════════════════════════════════════════════════════════════════════
   Bandeaux de reprise après confirmation d'email (AUTH-3b et 3c)
   Affichés par shared.js quand une demande de partenariat (.qw-mr-toast) ou
   les XP d'une quête démo (.qw-xp-toast) sont rejoués sur la première session
   confirmée. Ils peuvent apparaître sur N'IMPORTE QUELLE page — d'où le
   position:fixed et un z-index au-dessus des modales du portail.
   ════════════════════════════════════════════════════════════════════════ */
.qw-mr-toast,
.qw-xp-toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:10000;
  max-width:min(560px,calc(100vw - 32px));padding:14px 18px;border-radius:14px;
  background:var(--bg2,#13131C);border:1px solid rgba(34,197,94,.45);
  color:var(--text,#fff);font-family:'Inter',sans-serif;font-size:13px;font-weight:600;
  line-height:1.5;text-align:center;box-shadow:0 16px 48px rgba(0,0,0,.45)}

/* ════════════════════════════════════════════════════════════════════════
   ANIMATION « COMPTE CRÉÉ » (overlay joueur, fin de démo)
   Source : anims/qwestify-anim1-compte-cree.html. Ce qui a été RETIRÉ de
   l'original : le :root (voir ci-dessous), la barre d'aperçu .preview-bar,
   les règles *, html et body de la page de démo.

   ⚠️ Les variables de l'animation étaient sur :root — donc GLOBALES, avec des
   noms génériques (--qw-orange, --qw-text, --qw-border…) qui écrasaient ou
   se faisaient écraser par celles de l'app. Elles sont ici SCOPÉES sous la
   racine de l'overlay : aucune fuite, aucune collision.

   Les 8 particules portaient leurs coordonnées en style="--x:…" INLINE.
   Converties en :nth-child pour respecter le cliquet anti-dette (§ lint).
   ════════════════════════════════════════════════════════════════════════ */
.qw-account-success{
  --qw-panel:#0f111a;
  --qw-border:rgba(255,255,255,.08);
  --qw-orange:#f97316;
  --qw-amber:#fbbf24;
  --qw-green:#34d399;
  --qw-green-soft:rgba(52,211,153,.16);
  --qw-text:#f4f4f7;
  --qw-text-2:#aab0c0;
  --qw-text-3:#6a7080;
  --qw-card:#f6f4ee;
  --qw-card-ink:#1b1d24;
  --qw-card-xp:#ea580c;
  font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
/* ============ OVERLAY ============ */
  .qw-account-success{
    /* z-index : 40 dans la maquette — suffisant sur une page vide, insuffisant
       ici. L'overlay s'ouvre PENDANT que le tunnel occupe l'écran, et celui-ci
       monte à 9999 (#quest-tunnel) avec sa croix de fermeture à 10001. Le
       panneau peignait donc correctement mais restait DERRIÈRE : visible pour
       getComputedStyle, invisible pour le joueur.
       Même valeur que l'overlay marchand, pour que les deux se comportent
       pareil. Aucun piège d'empilement : le seul ancêtre est <body>, en
       position static — le z-index se compare bien à la racine. */
    position:fixed;inset:0;z-index:2147483000;
    display:flex;align-items:center;justify-content:center;padding:24px;
    opacity:0;pointer-events:none;transition:opacity .25s ease;
  }
  .qw-account-success.is-open{opacity:1;pointer-events:auto}

  .qw-account-success__backdrop{
    position:absolute;inset:0;
    background:rgba(4,5,9,.78);backdrop-filter:blur(3px);
  }

  .qw-account-success__panel{
    position:relative;width:min(440px,100%);
    background:var(--qw-panel);
    border:1px solid var(--qw-border);
    border-radius:24px;
    padding:38px 30px 30px;
    overflow:hidden;
    box-shadow:0 30px 80px rgba(0,0,0,.55);
    transform:translateY(18px) scale(.94);
    opacity:0;
  }
  .is-open .qw-account-success__panel{
    animation:qw-panel-in .55s cubic-bezier(.16,1,.3,1) .05s forwards;
  }
  @keyframes qw-panel-in{to{transform:translateY(0) scale(1);opacity:1}}

  .qw-account-success__ambient{
    position:absolute;top:-40%;left:50%;transform:translateX(-50%);
    width:360px;height:360px;border-radius:50%;
    background:radial-gradient(circle, rgba(52,211,153,.28), transparent 65%);
    filter:blur(14px);pointer-events:none;
  }

  .qw-account-success__particles{position:absolute;inset:0;pointer-events:none}
  .qw-account-success__particles span{
    position:absolute;
    left:var(--x);top:var(--y);
    width:var(--size);height:var(--size);border-radius:50%;
    background:var(--qw-amber);
    opacity:0;
  }
  .is-open .qw-account-success__particles span{
    animation:qw-particle 1.5s ease-out var(--delay) forwards;
  }
  @keyframes qw-particle{
    0%{opacity:0;transform:translateY(6px) scale(.6)}
    30%{opacity:1}
    100%{opacity:0;transform:translateY(-26px) scale(1)}
  }

  .qw-account-success__content{position:relative;text-align:center}

  /* --- Coche --- */
  .qw-account-success__status{display:flex;justify-content:center;margin-bottom:18px}
  .qw-account-success__status-ring{
    width:96px;height:96px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:var(--qw-green-soft);
    box-shadow:0 0 0 1px rgba(52,211,153,.25);
    transform:scale(.6);opacity:0;
  }
  .is-open .qw-account-success__status-ring{
    animation:qw-pop .5s cubic-bezier(.16,1.4,.3,1) .15s forwards;
  }
  @keyframes qw-pop{to{transform:scale(1);opacity:1}}

  .qw-account-success__check{width:54px;height:54px}
  .qw-account-success__check-circle{
    fill:none;stroke:var(--qw-green);stroke-width:4;
    stroke-dasharray:170;stroke-dashoffset:170;
  }
  .qw-account-success__check-path{
    fill:none;stroke:var(--qw-green);stroke-width:5;
    stroke-linecap:round;stroke-linejoin:round;
    stroke-dasharray:48;stroke-dashoffset:48;
  }
  .is-open .qw-account-success__check-circle{animation:qw-draw .6s ease .35s forwards}
  .is-open .qw-account-success__check-path{animation:qw-draw .35s ease .75s forwards}
  @keyframes qw-draw{to{stroke-dashoffset:0}}

  .qw-account-success__eyebrow{
    font-size:12px;letter-spacing:.14em;text-transform:uppercase;
    font-weight:700;color:var(--qw-orange);margin-bottom:8px;
  }
  .qw-account-success__title{
    font-size:27px;font-weight:800;line-height:1.15;color:var(--qw-text);
    margin-bottom:18px;
  }
  .qw-account-success__title span{color:var(--qw-green)}

  /* --- Carte XP --- */
  .qw-account-success__xp-card{
    position:relative;
    background:var(--qw-card);color:var(--qw-card-ink);
    border-radius:16px;padding:16px 18px;margin-bottom:16px;overflow:hidden;
  }
  .qw-account-success__xp-glow{
    position:absolute;inset:0;
    background:radial-gradient(120px 60px at 50% 120%, rgba(234,88,12,.18), transparent 70%);
    pointer-events:none;
  }
  .qw-account-success__xp-label{
    position:relative;font-size:16px;font-weight:600;line-height:1.4;
  }
  .qw-account-success__xp-value{
    font-size:26px;font-weight:800;color:var(--qw-card-xp);
    font-variant-numeric:tabular-nums;
  }
  .qw-account-success__xp-unit{font-size:16px;font-weight:800;color:var(--qw-card-xp)}
  .qw-account-success__sparkle{display:inline-block;margin-left:2px}
  .is-open .qw-account-success__sparkle{animation:qw-sparkle 1.6s ease-in-out .9s infinite}
  @keyframes qw-sparkle{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.35);opacity:1}}

  .qw-account-success__xp-progress{
    position:relative;margin-top:12px;height:6px;border-radius:999px;
    background:rgba(0,0,0,.08);overflow:hidden;
  }
  .qw-account-success__xp-progress span{
    display:block;height:100%;width:0;border-radius:999px;
    background:linear-gradient(90deg,var(--qw-green),#10b981);
  }
  .is-open .qw-account-success__xp-progress span{animation:qw-fill 1s ease .8s forwards}
  @keyframes qw-fill{to{width:100%}}

  .qw-account-success__description{
    font-size:14px;line-height:1.55;color:var(--qw-text-2);
    max-width:320px;margin:0 auto 14px;
  }

  .qw-account-success__mail-hint{
    display:inline-flex;align-items:center;gap:7px;
    font-size:12.5px;color:var(--qw-text-3);margin-bottom:22px;
  }
  .qw-account-success__mail-icon svg{width:16px;height:16px;display:block}
  .qw-account-success__mail-icon svg path{fill:none;stroke:var(--qw-text-3);stroke-width:1.6}

  .qw-account-success__button{
    display:inline-flex;align-items:center;gap:8px;
    font:inherit;font-size:15px;font-weight:700;color:#0a0a0a;cursor:pointer;border:0;
    background:linear-gradient(135deg,var(--qw-orange),var(--qw-amber));
    padding:13px 26px;border-radius:999px;
    box-shadow:0 8px 24px rgba(249,115,22,.28);
    transition:transform .12s ease;
  }
  .qw-account-success__button:hover{transform:translateY(-1px)}
  .qw-account-success__button:active{transform:scale(.98)}
  .qw-account-success__button svg{width:18px;height:18px}
  .qw-account-success__button svg path{fill:none;stroke:#0a0a0a;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

  @media (prefers-reduced-motion: reduce){
    /* SCOPÉ. La maquette écrivait `*{animation:none!important}` — inoffensif dans
       une page autonome, dévastateur ici : posé dans style.css il aurait coupé
       TOUTES les animations de TOUTE l'application pour quiconque a activé
       « réduire les animations » — tunnel, carrousel, barres d'XP comprises.
       Restreint aux descendants de l'overlay. */
    .qw-account-success *{animation:none!important}
    .qw-account-success__panel{opacity:1;transform:none}
    .qw-account-success__status-ring{opacity:1;transform:none}
    .qw-account-success__check-circle,.qw-account-success__check-path{stroke-dashoffset:0}
    .qw-account-success__xp-progress span{width:100%}
  }

.qw-account-success__particles span:nth-child(1){--x:13%;--y:28%;--delay:0ms;--size:5px}
.qw-account-success__particles span:nth-child(2){--x:22%;--y:18%;--delay:180ms;--size:4px}
.qw-account-success__particles span:nth-child(3){--x:31%;--y:32%;--delay:320ms;--size:3px}
.qw-account-success__particles span:nth-child(4){--x:68%;--y:21%;--delay:120ms;--size:4px}
.qw-account-success__particles span:nth-child(5){--x:78%;--y:31%;--delay:260ms;--size:5px}
.qw-account-success__particles span:nth-child(6){--x:88%;--y:19%;--delay:420ms;--size:3px}
.qw-account-success__particles span:nth-child(7){--x:17%;--y:63%;--delay:500ms;--size:3px}
.qw-account-success__particles span:nth-child(8){--x:84%;--y:66%;--delay:380ms;--size:4px}

/* ════════════════════════════════════════════════════════════════════════
   ANIMATION « DEMANDE MARCHAND EN VALIDATION » (overlay annonceur)
   Source : anims/qwestify-demande-marchand-validation.html.
   RETIRÉ de l'original : le décor de la page de démo (.qw-demo-page et son
   bouton « Rejouer l'animation »), les remises à zéro universelles (*, html,
   body). Aucun :root dans ce fichier — donc aucune collision à traiter.
   ════════════════════════════════════════════════════════════════════════ */

    html,

    .qw-merchant-success,
    .qw-merchant-success *,
    .qw-merchant-success *::before,
    .qw-merchant-success *::after {
      box-sizing: border-box;
    }

/* Variables de l'animation — RESTAURÉES et SCOPÉES.
   Elles vivaient sur :root dans la maquette : globales, donc en
   concurrence avec celles de l'app. Les scoper sous .qw-merchant-success les rend
   locales à l'animation — aucune fuite, aucune collision.
   Sans elles, var(--qw-merchant-ease-out) ne resolvait rien : le raccourci
   animation devenait invalide, animation-name retombait a none, et le
   panneau restait fige a opacity 0 — seul le fond noir s'affichait. */
.qw-merchant-success {

      --qw-merchant-bg: #070a09;
      --qw-merchant-bg-elevated: #0c110f;
      --qw-merchant-bg-soft: #111815;

      --qw-merchant-white: #ffffff;
      --qw-merchant-card: #f7faf8;
      --qw-merchant-card-soft: #edf3ef;

      --qw-merchant-text: #101512;
      --qw-merchant-text-soft: #59645e;
      --qw-merchant-text-on-dark: #f5f8f6;
      --qw-merchant-text-on-dark-soft: rgba(245, 248, 246, 0.7);

      --qw-merchant-green: #25d979;
      --qw-merchant-green-bright: #42f291;
      --qw-merchant-green-dark: #087846;

      --qw-merchant-orange: #ff6a1a;
      --qw-merchant-amber: #ffb323;
      --qw-merchant-amber-dark: #8e5700;

      --qw-merchant-teal: #0a5b4e;
      --qw-merchant-teal-dark: #06362f;

      --qw-merchant-backdrop: rgba(2, 6, 5, 0.88);
      --qw-merchant-panel-border: rgba(255, 255, 255, 0.08);
      --qw-merchant-panel-highlight: rgba(255, 255, 255, 0.025);

      --qw-merchant-teal-halo: rgba(10, 91, 78, 0.28);
      --qw-merchant-green-halo: rgba(37, 217, 121, 0.2);
      --qw-merchant-green-border: rgba(66, 242, 145, 0.28);
      --qw-merchant-green-shadow: rgba(37, 217, 121, 0.2);

      --qw-merchant-amber-soft: rgba(255, 179, 35, 0.14);
      --qw-merchant-amber-border: rgba(255, 179, 35, 0.35);
      --qw-merchant-amber-shadow: rgba(255, 179, 35, 0.18);

      --qw-merchant-card-border: rgba(16, 21, 18, 0.08);
      --qw-merchant-card-shadow: rgba(0, 0, 0, 0.24);

      --qw-merchant-close-bg: rgba(255, 255, 255, 0.06);
      --qw-merchant-close-bg-hover: rgba(255, 255, 255, 0.11);
      --qw-merchant-close-border: rgba(255, 255, 255, 0.09);
      --qw-merchant-focus: rgba(255, 179, 35, 0.5);

      --qw-merchant-font:
        Inter,
        ui-sans-serif,
        system-ui,
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        sans-serif;

      --qw-merchant-panel-width: 570px;
      --qw-merchant-panel-radius: 28px;
      --qw-merchant-card-radius: 19px;
      --qw-merchant-pill-radius: 999px;

      --qw-merchant-panel-padding: clamp(30px, 6vw, 58px);

      --qw-merchant-panel-shadow:
        0 34px 100px rgba(0, 0, 0, 0.54),
        0 0 0 1px rgba(255, 255, 255, 0.02);

      --qw-merchant-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
      --qw-merchant-ease-pop: cubic-bezier(0.22, 1.3, 0.36, 1);
}
    .qw-merchant-success {
      position: fixed;
      z-index: 2147483000;
      inset: 0;
      display: grid;
      place-items: center;
      padding:
        max(16px, env(safe-area-inset-top))
        max(16px, env(safe-area-inset-right))
        max(16px, env(safe-area-inset-bottom))
        max(16px, env(safe-area-inset-left));
      overflow-x: hidden;
      overflow-y: auto;
      font-family: var(--qw-merchant-font);
      visibility: hidden;
      pointer-events: none;
      opacity: 0;
      transition:
        opacity 220ms ease,
        visibility 0s linear 220ms;
    }

    .qw-merchant-success.is-visible {
      visibility: visible;
      pointer-events: auto;
      opacity: 1;
      transition:
        opacity 220ms ease,
        visibility 0s linear 0s;
    }

    .qw-merchant-success__backdrop {
      position: fixed;
      inset: 0;
      background:
        radial-gradient(
          circle at 50% 20%,
          var(--qw-merchant-teal-halo),
          transparent 42%
        ),
        radial-gradient(
          circle at 12% 86%,
          var(--qw-merchant-green-halo),
          transparent 34%
        ),
        var(--qw-merchant-backdrop);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
    }

    .qw-merchant-success__panel {
      position: relative;
      width: min(100%, var(--qw-merchant-panel-width));
      min-height: 500px;
      overflow: hidden;
      border: 1px solid var(--qw-merchant-panel-border);
      border-radius: var(--qw-merchant-panel-radius);
      background:
        linear-gradient(
          145deg,
          var(--qw-merchant-panel-highlight),
          transparent 45%
        ),
        var(--qw-merchant-bg);
      box-shadow: var(--qw-merchant-panel-shadow);
      color: var(--qw-merchant-text-on-dark);
      transform: translateY(22px) scale(0.975);
      opacity: 0;
      outline: none;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__panel {
      animation:
        qwMerchantPanelEnter
        650ms
        var(--qw-merchant-ease-out)
        forwards;
    }

    .qw-merchant-success__ambient {
      position: absolute;
      inset: -35%;
      pointer-events: none;
      background:
        radial-gradient(
          circle at 50% 18%,
          var(--qw-merchant-green-halo),
          transparent 24%
        ),
        radial-gradient(
          circle at 8% 68%,
          var(--qw-merchant-teal-halo),
          transparent 29%
        ),
        radial-gradient(
          circle at 95% 75%,
          var(--qw-merchant-amber-soft),
          transparent 25%
        );
      filter: blur(9px);
      transform: scale(0.88);
      opacity: 0;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__ambient {
      animation:
        qwMerchantAmbientEnter
        1200ms
        ease
        100ms
        forwards;
    }

    .qw-merchant-success__close {
      position: absolute;
      z-index: 4;
      top: 20px;
      right: 20px;
      display: grid;
      width: 42px;
      height: 42px;
      place-items: center;
      padding: 0;
      border: 1px solid var(--qw-merchant-close-border);
      border-radius: 50%;
      background: var(--qw-merchant-close-bg);
      color: var(--qw-merchant-text-on-dark-soft);
      cursor: pointer;
      transition:
        background 180ms ease,
        color 180ms ease,
        transform 180ms ease;
    }

    .qw-merchant-success__close:hover {
      background: var(--qw-merchant-close-bg-hover);
      color: var(--qw-merchant-white);
      transform: scale(1.04);
    }

    .qw-merchant-success__close:active {
      transform: scale(0.96);
    }

    .qw-merchant-success__close:focus-visible {
      outline: 3px solid var(--qw-merchant-focus);
      outline-offset: 3px;
    }

    .qw-merchant-success__close svg {
      width: 19px;
      height: 19px;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.9;
      stroke-linecap: round;
    }

    .qw-merchant-success__content {
      position: relative;
      z-index: 2;
      display: flex;
      min-height: 500px;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: var(--qw-merchant-panel-padding);
      text-align: center;
    }

    .qw-merchant-success__check-zone {
      position: relative;
      display: grid;
      width: 108px;
      height: 108px;
      margin-bottom: 25px;
      place-items: center;
    }

    .qw-merchant-success__check-halo {
      position: absolute;
      inset: 0;
      border: 1px solid var(--qw-merchant-green-border);
      border-radius: 50%;
      opacity: 0;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__check-halo {
      animation:
        qwMerchantCheckHalo
        900ms
        ease-out
        300ms
        both;
    }

    .qw-merchant-success__check {
      width: 88px;
      height: 88px;
      overflow: visible;
      filter: drop-shadow(0 14px 25px var(--qw-merchant-green-shadow));
      transform: scale(0.55);
      opacity: 0;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__check {
      animation:
        qwMerchantCheckEnter
        620ms
        var(--qw-merchant-ease-pop)
        180ms
        forwards;
    }

    .qw-merchant-success__check-circle {
      fill: var(--qw-merchant-green);
      stroke: var(--qw-merchant-green-border);
      stroke-width: 2;
    }

    .qw-merchant-success__check-path {
      fill: none;
      stroke: var(--qw-merchant-white);
      stroke-width: 6;
      stroke-linecap: round;
      stroke-linejoin: round;
      stroke-dasharray: 45;
      stroke-dashoffset: 45;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__check-path {
      animation:
        qwMerchantDrawCheck
        430ms
        ease
        610ms
        forwards;
    }

    .qw-merchant-success__title {
      margin: 0;
      color: var(--qw-merchant-text-on-dark);
      font-size: clamp(2rem, 7vw, 3.15rem);
      font-weight: 900;
      letter-spacing: -0.055em;
      line-height: 1;
      transform: translateY(14px);
      opacity: 0;
    }

    .qw-merchant-success__title span {
      display: inline-block;
      color: var(--qw-merchant-green-bright);
      text-shadow: 0 0 20px var(--qw-merchant-green-shadow);
    }

    .qw-merchant-success.is-visible .qw-merchant-success__title {
      animation:
        qwMerchantContentEnter
        580ms
        var(--qw-merchant-ease-out)
        600ms
        forwards;
    }

    .qw-merchant-success__validation-card {
      position: relative;
      width: 100%;
      max-width: 445px;
      margin-top: 30px;
      padding: 23px;
      overflow: hidden;
      border: 1px solid var(--qw-merchant-card-border);
      border-radius: var(--qw-merchant-card-radius);
      background:
        linear-gradient(
          145deg,
          var(--qw-merchant-card),
          var(--qw-merchant-card-soft)
        );
      box-shadow: 0 20px 45px var(--qw-merchant-card-shadow);
      transform: translateY(18px) scale(0.985);
      opacity: 0;
    }

    .qw-merchant-success__validation-card::before {
      position: absolute;
      top: -70px;
      left: 50%;
      width: 260px;
      height: 140px;
      border-radius: 50%;
      background:
        radial-gradient(
          circle,
          var(--qw-merchant-amber-soft),
          transparent 70%
        );
      content: "";
      transform: translateX(-50%);
      pointer-events: none;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__validation-card {
      animation:
        qwMerchantCardEnter
        650ms
        var(--qw-merchant-ease-out)
        760ms
        forwards;
    }

    .qw-merchant-success__validation-badge {
      position: relative;
      z-index: 1;
      display: inline-flex;
      min-height: 48px;
      align-items: center;
      justify-content: center;
      gap: 11px;
      padding: 11px 18px;
      border: 1px solid var(--qw-merchant-amber-border);
      border-radius: var(--qw-merchant-pill-radius);
      background: var(--qw-merchant-amber-soft);
      color: var(--qw-merchant-amber-dark);
      font-size: clamp(0.9rem, 3.5vw, 1rem);
      font-weight: 850;
      letter-spacing: -0.015em;
      line-height: 1.3;
    }

    .qw-merchant-success__validation-dot {
      position: relative;
      width: 10px;
      height: 10px;
      flex: 0 0 10px;
      border-radius: 50%;
      background: var(--qw-merchant-amber);
      box-shadow: 0 0 0 5px var(--qw-merchant-amber-soft);
    }

    .qw-merchant-success__validation-dot::after {
      position: absolute;
      inset: -5px;
      border: 1px solid var(--qw-merchant-amber-border);
      border-radius: 50%;
      content: "";
    }

    .qw-merchant-success.is-visible .qw-merchant-success__validation-dot::after {
      animation:
        qwMerchantValidationPulse
        1900ms
        ease-out
        1250ms
        infinite;
    }

    .qw-merchant-success__description {
      max-width: 465px;
      margin: 25px 0 0;
      color: var(--qw-merchant-text-on-dark-soft);
      font-size: clamp(0.96rem, 3.4vw, 1.06rem);
      font-weight: 550;
      line-height: 1.65;
      transform: translateY(12px);
      opacity: 0;
    }

    .qw-merchant-success.is-visible .qw-merchant-success__description {
      animation:
        qwMerchantContentEnter
        580ms
        var(--qw-merchant-ease-out)
        940ms
        forwards;
    }

    @keyframes qwMerchantPanelEnter {
      from {
        transform: translateY(22px) scale(0.975);
        opacity: 0;
      }
      to {
        transform: translateY(0) scale(1);
        opacity: 1;
      }
    }

    @keyframes qwMerchantAmbientEnter {
      from {
        transform: scale(0.88);
        opacity: 0;
      }
      to {
        transform: scale(1);
        opacity: 1;
      }
    }

    @keyframes qwMerchantCheckEnter {
      0% {
        transform: scale(0.55);
        opacity: 0;
      }
      65% {
        transform: scale(1.06);
        opacity: 1;
      }
      100% {
        transform: scale(1);
        opacity: 1;
      }
    }

    @keyframes qwMerchantCheckHalo {
      0% {
        transform: scale(0.72);
        opacity: 0;
      }
      35% {
        opacity: 0.85;
      }
      100% {
        transform: scale(1.28);
        opacity: 0;
      }
    }

    @keyframes qwMerchantDrawCheck {
      to {
        stroke-dashoffset: 0;
      }
    }

    @keyframes qwMerchantContentEnter {
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    @keyframes qwMerchantCardEnter {
      0% {
        transform: translateY(18px) scale(0.985);
        opacity: 0;
      }
      70% {
        transform: translateY(-2px) scale(1.003);
        opacity: 1;
      }
      100% {
        transform: translateY(0) scale(1);
        opacity: 1;
      }
    }

    @keyframes qwMerchantValidationPulse {
      0% {
        transform: scale(0.8);
        opacity: 0;
      }
      35% {
        opacity: 0.75;
      }
      100% {
        transform: scale(1.7);
        opacity: 0;
      }
    }

    @media (max-width: 620px) {
      .qw-merchant-success {
        align-items: end;
        padding: 10px;
      }

      .qw-merchant-success__panel {
        width: 100%;
        min-height: auto;
        border-radius: 25px;
      }

      .qw-merchant-success__content {
        min-height: auto;
        padding:
          48px
          clamp(19px, 6vw, 30px)
          max(34px, env(safe-area-inset-bottom));
      }

      .qw-merchant-success__close {
        top: 15px;
        right: 15px;
        width: 39px;
        height: 39px;
      }

      .qw-merchant-success__check-zone {
        width: 94px;
        height: 94px;
        margin-bottom: 19px;
      }

      .qw-merchant-success__check {
        width: 78px;
        height: 78px;
      }

      .qw-merchant-success__validation-card {
        margin-top: 24px;
        padding: 19px 15px;
      }

      .qw-merchant-success__validation-badge {
        width: 100%;
        padding: 12px 15px;
      }

      .qw-merchant-success__description {
        margin-top: 21px;
      }
    }

    @media (max-height: 620px) and (min-width: 621px) {
      .qw-merchant-success {
        align-items: start;
      }

      .qw-merchant-success__content {
        min-height: auto;
        padding-top: 40px;
        padding-bottom: 40px;
      }

      .qw-merchant-success__check-zone {
        margin-bottom: 14px;
        transform: scale(0.86);
      }

      .qw-merchant-success__validation-card {
        margin-top: 20px;
      }

      .qw-merchant-success__description {
        margin-top: 18px;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .qw-merchant-success,
      .qw-merchant-success *,
      .qw-merchant-success *::before,
      .qw-merchant-success *::after {
        scroll-behavior: auto !important;
        animation-duration: 1ms !important;
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
      }

      .qw-merchant-success__check-path {
        stroke-dashoffset: 0;
      }

      .qw-merchant-success__validation-dot::after {
        display: none;
      }
    }

/* ════════════════════════════════════════════════════════════════════════
   ÉCRAN « EMAIL CONFIRMÉ / COMPTE ACTIVÉ » (page pleine)
   Source : anims/qwestify-email-confirme-compte-active.html.
   RETIRÉ de l'original : les remises à zéro universelles de la page de démo.
   Aucun :root non plus. L'écran vit sur compte-active.html.
   ════════════════════════════════════════════════════════════════════════ */

    button,
    a {
      font: inherit;
    }

/* Variables de l'animation — RESTAURÉES et SCOPÉES.
   Elles vivaient sur :root dans la maquette : globales, donc en
   concurrence avec celles de l'app. Les scoper sous .qw-activation-page les rend
   locales à l'animation — aucune fuite, aucune collision.
   Meme cause, meme remede que pour l'overlay marchand. */
.qw-activation-page {

      /* =========================
         COULEURS QWESTIFY
         ========================= */

      --qw-bg: #070a09;
      --qw-bg-deep: #040605;
      --qw-bg-soft: #0b100e;

      --qw-white: #ffffff;
      --qw-text: #f6faf7;
      --qw-text-soft: rgba(246, 250, 247, 0.72);
      --qw-text-muted: rgba(246, 250, 247, 0.48);

      --qw-green: #24dc7a;
      --qw-green-bright: #44f596;
      --qw-green-deep: #0b8b52;
      --qw-green-dark: #075f3a;
      --qw-green-soft: rgba(36, 220, 122, 0.14);
      --qw-green-border: rgba(68, 245, 150, 0.28);
      --qw-green-halo: rgba(36, 220, 122, 0.28);

      --qw-orange: #ff6a1a;
      --qw-amber: #ffb323;
      --qw-orange-soft: rgba(255, 106, 26, 0.22);
      --qw-amber-soft: rgba(255, 179, 35, 0.16);

      --qw-teal: #0c5d50;
      --qw-teal-dark: #063b33;
      --qw-teal-halo: rgba(12, 93, 80, 0.32);

      --qw-panel: rgba(255, 255, 255, 0.035);
      --qw-panel-border: rgba(255, 255, 255, 0.08);
      --qw-focus: rgba(255, 179, 35, 0.58);

      /* =========================
         TYPOGRAPHIE / DIMENSIONS
         ========================= */

      --qw-font:
        Inter,
        ui-sans-serif,
        system-ui,
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        sans-serif;

      --qw-radius-large: 32px;
      --qw-radius-pill: 999px;

      --qw-content-width: 760px;

      --qw-shadow:
        0 36px 120px rgba(0, 0, 0, 0.48),
        0 0 0 1px rgba(255, 255, 255, 0.02);

      --qw-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
      --qw-ease-pop: cubic-bezier(0.22, 1.4, 0.36, 1);
}
    .qw-activation-page {
      position: relative;
      min-height: 100vh;
      isolation: isolate;
      overflow: hidden;
    }

    .qw-activation-page::before {
      position: absolute;
      inset: 0;
      z-index: -3;
      content: "";
      background:
        linear-gradient(
          rgba(255, 255, 255, 0.018) 1px,
          transparent 1px
        ),
        linear-gradient(
          90deg,
          rgba(255, 255, 255, 0.018) 1px,
          transparent 1px
        );
      background-size: 42px 42px;
      mask-image:
        radial-gradient(
          circle at center,
          black 0%,
          transparent 68%
        );
      -webkit-mask-image:
        radial-gradient(
          circle at center,
          black 0%,
          transparent 68%
        );
      opacity: 0.34;
    }

    .qw-activation-page__glow {
      position: absolute;
      z-index: -2;
      border-radius: 50%;
      pointer-events: none;
      filter: blur(16px);
      opacity: 0;
    }

    .qw-activation-page__glow--top {
      top: -18vw;
      left: 50%;
      width: min(80vw, 920px);
      aspect-ratio: 1;
      background:
        radial-gradient(
          circle,
          rgba(36, 220, 122, 0.18),
          transparent 68%
        );
      transform: translateX(-50%) scale(0.8);
    }

    .qw-activation-page__glow--left {
      left: -20vw;
      bottom: -28vw;
      width: min(70vw, 800px);
      aspect-ratio: 1;
      background:
        radial-gradient(
          circle,
          rgba(12, 93, 80, 0.24),
          transparent 70%
        );
      transform: scale(0.8);
    }

    .qw-activation-page__glow--right {
      right: -18vw;
      bottom: -26vw;
      width: min(62vw, 720px);
      aspect-ratio: 1;
      background:
        radial-gradient(
          circle,
          rgba(255, 153, 30, 0.13),
          transparent 70%
        );
      transform: scale(0.8);
    }

    .qw-activation-page.is-ready
    .qw-activation-page__glow {
      animation:
        qwGlowEnter
        1600ms
        ease
        forwards;
    }

    .qw-activation-page.is-ready
    .qw-activation-page__glow--left {
      animation-delay: 120ms;
    }

    .qw-activation-page.is-ready
    .qw-activation-page__glow--right {
      animation-delay: 220ms;
    }

    .qw-activation-page__shell {
      position: relative;
      display: grid;
      min-height: 100vh;
      place-items: center;
      padding:
        max(28px, env(safe-area-inset-top))
        max(20px, env(safe-area-inset-right))
        max(28px, env(safe-area-inset-bottom))
        max(20px, env(safe-area-inset-left));
    }

    .qw-activation-card {
      position: relative;
      width: min(100%, var(--qw-content-width));
      padding:
        clamp(34px, 6vw, 66px)
        clamp(22px, 7vw, 74px)
        clamp(36px, 6vw, 62px);
      overflow: hidden;
      border: 1px solid var(--qw-panel-border);
      border-radius: var(--qw-radius-large);
      background:
        linear-gradient(
          145deg,
          rgba(255, 255, 255, 0.055),
          rgba(255, 255, 255, 0.015) 38%,
          rgba(255, 255, 255, 0.01)
        ),
        rgba(7, 10, 9, 0.74);
      box-shadow: var(--qw-shadow);
      backdrop-filter: blur(18px);
      -webkit-backdrop-filter: blur(18px);
      text-align: center;
      transform: translateY(24px) scale(0.975);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-card {
      animation:
        qwCardEnter
        820ms
        var(--qw-ease-out)
        forwards;
    }

    .qw-activation-card::before {
      position: absolute;
      top: -160px;
      left: 50%;
      width: 420px;
      height: 250px;
      border-radius: 50%;
      content: "";
      background:
        radial-gradient(
          circle,
          rgba(36, 220, 122, 0.17),
          transparent 70%
        );
      transform: translateX(-50%);
      filter: blur(10px);
      pointer-events: none;
    }

    .qw-activation-card::after {
      position: absolute;
      inset: 0;
      border-radius: inherit;
      content: "";
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
      pointer-events: none;
    }

    /* =========================
       SCEAU D'ACTIVATION
       ========================= */

    .qw-activation-seal {
      position: relative;
      display: grid;
      width: clamp(132px, 22vw, 178px);
      aspect-ratio: 1;
      margin: 0 auto 28px;
      place-items: center;
    }

    .qw-activation-seal__orbit,
    .qw-activation-seal__orbit::before,
    .qw-activation-seal__orbit::after,
    .qw-activation-seal__halo,
    .qw-activation-seal__disc,
    .qw-activation-seal__ring {
      position: absolute;
      border-radius: 50%;
    }

    .qw-activation-seal__halo {
      inset: -20%;
      background:
        radial-gradient(
          circle,
          var(--qw-green-halo),
          transparent 66%
        );
      filter: blur(5px);
      transform: scale(0.5);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__halo {
      animation:
        qwSealHalo
        1300ms
        ease-out
        260ms
        forwards;
    }

    .qw-activation-seal__orbit {
      inset: 0;
      border: 1px solid rgba(68, 245, 150, 0.2);
      transform: rotate(-22deg) scale(0.72);
      opacity: 0;
    }

    .qw-activation-seal__orbit::before,
    .qw-activation-seal__orbit::after {
      width: 9px;
      height: 9px;
      content: "";
      background: var(--qw-green-bright);
      box-shadow:
        0 0 12px rgba(68, 245, 150, 0.8),
        0 0 24px rgba(68, 245, 150, 0.28);
    }

    .qw-activation-seal__orbit::before {
      top: 8%;
      left: 17%;
    }

    .qw-activation-seal__orbit::after {
      right: 10%;
      bottom: 20%;
      width: 7px;
      height: 7px;
      background: var(--qw-amber);
      box-shadow:
        0 0 12px rgba(255, 179, 35, 0.72),
        0 0 22px rgba(255, 179, 35, 0.22);
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__orbit {
      animation:
        qwOrbitEnter
        900ms
        var(--qw-ease-out)
        220ms
        forwards,
        qwOrbitRotate
        14s
        linear
        1200ms
        infinite;
    }

    .qw-activation-seal__ring {
      inset: 11%;
      border:
        1px solid
        var(--qw-green-border);
      background:
        linear-gradient(
          145deg,
          rgba(68, 245, 150, 0.13),
          rgba(36, 220, 122, 0.025)
        );
      box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.03),
        0 22px 54px rgba(0, 0, 0, 0.28);
      transform: scale(0.45) rotate(-12deg);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__ring {
      animation:
        qwSealRingEnter
        760ms
        var(--qw-ease-pop)
        180ms
        forwards;
    }

    .qw-activation-seal__disc {
      inset: 24%;
      display: grid;
      place-items: center;
      background:
        linear-gradient(
          145deg,
          var(--qw-green-bright),
          var(--qw-green)
        );
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.38),
        inset 0 -10px 20px rgba(5, 99, 58, 0.22),
        0 18px 38px rgba(36, 220, 122, 0.24);
      transform: scale(0.3);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__disc {
      animation:
        qwSealDiscEnter
        650ms
        var(--qw-ease-pop)
        360ms
        forwards;
    }

    .qw-activation-seal__check {
      width: 58%;
      overflow: visible;
    }

    .qw-activation-seal__check-path {
      fill: none;
      stroke: var(--qw-white);
      stroke-width: 7;
      stroke-linecap: round;
      stroke-linejoin: round;
      stroke-dasharray: 55;
      stroke-dashoffset: 55;
      filter:
        drop-shadow(
          0 2px 4px
          rgba(0, 0, 0, 0.16)
        );
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__check-path {
      animation:
        qwDrawCheck
        520ms
        ease
        780ms
        forwards;
    }

    .qw-activation-seal__shine {
      position: absolute;
      inset: 24%;
      overflow: hidden;
      border-radius: 50%;
      pointer-events: none;
    }

    .qw-activation-seal__shine::before {
      position: absolute;
      top: -25%;
      left: -90%;
      width: 50%;
      height: 150%;
      content: "";
      background:
        linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.5),
          transparent
        );
      transform: rotate(18deg);
    }

    .qw-activation-page.is-ready
    .qw-activation-seal__shine::before {
      animation:
        qwSealShine
        850ms
        ease
        1080ms
        both;
    }

    /* =========================
       CONTENU
       ========================= */

    .qw-activation-eyebrow {
      margin: 0 0 10px;
      color: var(--qw-green-bright);
      font-size: 0.76rem;
      font-weight: 850;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      transform: translateY(12px);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-eyebrow {
      animation:
        qwContentEnter
        560ms
        var(--qw-ease-out)
        820ms
        forwards;
    }

    .qw-activation-title {
      margin: 0;
      color: var(--qw-text);
      font-size: clamp(2.35rem, 7.6vw, 4.5rem);
      font-weight: 950;
      letter-spacing: -0.065em;
      line-height: 0.98;
      text-wrap: balance;
      transform: translateY(16px);
      opacity: 0;
    }

    .qw-activation-title span {
      display: inline-block;
      color: var(--qw-green-bright);
      text-shadow:
        0 0 26px rgba(68, 245, 150, 0.3);
    }

    .qw-activation-page.is-ready
    .qw-activation-title {
      animation:
        qwContentEnter
        650ms
        var(--qw-ease-out)
        900ms
        forwards;
    }

    .qw-activation-subtitle {
      margin: 18px 0 0;
      color: var(--qw-text-soft);
      font-size: clamp(1.1rem, 3.4vw, 1.45rem);
      font-weight: 700;
      letter-spacing: -0.025em;
      line-height: 1.45;
      transform: translateY(14px);
      opacity: 0;
    }

    .qw-activation-subtitle strong {
      color: var(--qw-white);
      font-weight: 850;
    }

    .qw-activation-page.is-ready
    .qw-activation-subtitle {
      animation:
        qwContentEnter
        620ms
        var(--qw-ease-out)
        1020ms
        forwards;
    }

    .qw-activation-actions {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-top: 34px;
      transform: translateY(16px);
      opacity: 0;
    }

    .qw-activation-page.is-ready
    .qw-activation-actions {
      animation:
        qwContentEnter
        650ms
        var(--qw-ease-out)
        1140ms
        forwards;
    }

    .qw-activation-button {
      position: relative;
      display: inline-flex;
      min-width: min(100%, 240px);
      min-height: 58px;
      align-items: center;
      justify-content: center;
      gap: 11px;
      overflow: hidden;
      padding: 0 28px;
      border: 0;
      border-radius: var(--qw-radius-pill);
      background:
        linear-gradient(
          100deg,
          var(--qw-orange),
          var(--qw-amber)
        );
      box-shadow:
        0 16px 36px var(--qw-orange-soft),
        inset 0 1px 0 rgba(255, 255, 255, 0.28);
      color: #241307;
      text-decoration: none;
      font-size: 1rem;
      font-weight: 950;
      letter-spacing: -0.02em;
      transition:
        transform 180ms ease,
        box-shadow 180ms ease,
        filter 180ms ease;
    }

    .qw-activation-button::before {
      position: absolute;
      top: -45%;
      left: -70%;
      width: 42%;
      height: 190%;
      content: "";
      background:
        linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.4),
          transparent
        );
      transform: rotate(18deg);
    }

    .qw-activation-page.is-ready
    .qw-activation-button::before {
      animation:
        qwButtonShine
        950ms
        ease
        1750ms
        both;
    }

    .qw-activation-button:hover {
      transform: translateY(-3px);
      filter: saturate(1.06) brightness(1.035);
      box-shadow:
        0 20px 44px rgba(255, 106, 26, 0.28),
        inset 0 1px 0 rgba(255, 255, 255, 0.32);
    }

    .qw-activation-button:active {
      transform: translateY(0) scale(0.985);
    }

    .qw-activation-button:focus-visible {
      outline: 3px solid var(--qw-focus);
      outline-offset: 5px;
    }

    .qw-activation-button svg {
      width: 20px;
      height: 20px;
      fill: none;
      stroke: currentColor;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
      transition: transform 180ms ease;
    }

    .qw-activation-button:hover svg {
      transform: translateX(3px);
    }

    .qw-activation-note {
      margin: 14px 0 0;
      color: var(--qw-text-muted);
      font-size: 0.9rem;
      font-weight: 650;
      line-height: 1.5;
    }

    /* =========================
       ANIMATIONS
       ========================= */

    @keyframes qwCardEnter {
      from {
        transform: translateY(24px) scale(0.975);
        opacity: 0;
      }

      to {
        transform: translateY(0) scale(1);
        opacity: 1;
      }
    }

    @keyframes qwGlowEnter {
      from {
        transform: translateX(-50%) scale(0.8);
        opacity: 0;
      }

      to {
        transform: translateX(-50%) scale(1);
        opacity: 1;
      }
    }

    .qw-activation-page__glow--left,
    .qw-activation-page__glow--right {
      transform: scale(0.8);
    }

    @keyframes qwSealHalo {
      0% {
        transform: scale(0.5);
        opacity: 0;
      }

      35% {
        opacity: 1;
      }

      100% {
        transform: scale(1.18);
        opacity: 0.58;
      }
    }

    @keyframes qwOrbitEnter {
      from {
        transform: rotate(-22deg) scale(0.72);
        opacity: 0;
      }

      to {
        transform: rotate(0deg) scale(1);
        opacity: 1;
      }
    }

    @keyframes qwOrbitRotate {
      from {
        transform: rotate(0deg) scale(1);
      }

      to {
        transform: rotate(360deg) scale(1);
      }
    }

    @keyframes qwSealRingEnter {
      0% {
        transform: scale(0.45) rotate(-12deg);
        opacity: 0;
      }

      66% {
        transform: scale(1.07) rotate(2deg);
        opacity: 1;
      }

      100% {
        transform: scale(1) rotate(0);
        opacity: 1;
      }
    }

    @keyframes qwSealDiscEnter {
      0% {
        transform: scale(0.3);
        opacity: 0;
      }

      65% {
        transform: scale(1.08);
        opacity: 1;
      }

      100% {
        transform: scale(1);
        opacity: 1;
      }
    }

    @keyframes qwDrawCheck {
      to {
        stroke-dashoffset: 0;
      }
    }

    @keyframes qwSealShine {
      0% {
        left: -90%;
        opacity: 0;
      }

      30% {
        opacity: 1;
      }

      100% {
        left: 140%;
        opacity: 0;
      }
    }

    @keyframes qwContentEnter {
      to {
        transform: translateY(0);
        opacity: 1;
      }
    }

    @keyframes qwButtonShine {
      0% {
        left: -70%;
        opacity: 0;
      }

      25% {
        opacity: 1;
      }

      100% {
        left: 140%;
        opacity: 0;
      }
    }

    /* =========================
       RESPONSIVE
       ========================= */

    @media (max-width: 640px) {
      .qw-activation-page__shell {
        align-items: end;
        padding: 10px;
      }

      .qw-activation-card {
        width: 100%;
        border-radius: 26px;
        padding:
          38px
          21px
          max(36px, env(safe-area-inset-bottom));
      }

      .qw-activation-seal {
        width: 138px;
        margin-bottom: 22px;
      }

      .qw-activation-title {
        max-width: 9ch;
        margin-inline: auto;
      }

      .qw-activation-subtitle {
        margin-top: 16px;
      }

      .qw-activation-actions {
        margin-top: 29px;
      }

      .qw-activation-button {
        width: 100%;
        min-height: 58px;
      }
    }

    @media (max-height: 680px) and (min-width: 641px) {
      .qw-activation-page__shell {
        align-items: start;
      }

      .qw-activation-card {
        margin-block: 18px;
        padding-top: 36px;
        padding-bottom: 36px;
      }

      .qw-activation-seal {
        width: 132px;
        margin-bottom: 18px;
      }

      .qw-activation-actions {
        margin-top: 24px;
      }
    }

    /* =========================
       MOUVEMENTS RÉDUITS
       ========================= */

    @media (prefers-reduced-motion: reduce) {
      *::after {
        scroll-behavior: auto !important;
        animation-duration: 1ms !important;
        animation-delay: 0ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
      }

      .qw-activation-seal__orbit {
        transform: none !important;
      }

      .qw-activation-seal__check-path {
        stroke-dashoffset: 0;
      }

      .qw-activation-button::before,
      .qw-activation-seal__shine::before {
        display: none;
      }
    }

/* ══════════════════════════════════════════════════════════════════════════════
   MENU JOUEUR AUTH-AWARE — visibilité pilotée par <html class="qw-auth-member">
   ──────────────────────────────────────────────────────────────────────────────
   Contrat : shared.js injecte #sideMenu et #dtDropdown, puis pose (ou retire) la
   classe .qw-auth-member sur <html>. Ce bloc fait TOUT le masquage — le JS
   n'écrit aucun style, le plancher inline reste intact.

   Le défaut est le VISITEUR, et c'est délibérément asymétrique : data-qw-auth
   ="member" part masqué et n'apparaît que si la classe est posée. Un JS mort,
   une session qui échoue ou un événement qui n'arrive jamais laissent donc le
   menu visiteur. L'inverse — un menu connecté qui s'affiche par défaut puis se
   corrige — est exactement le défaut qu'on répare ici, et il n'est plus
   atteignable : il n'existe aucun chemin qui montre « Se déconnecter » sans que
   la session ait été confirmée.

   On ne réutilise PAS body.is-logged-out (style.css §1927+) : ce crochet legacy
   détruit aussi .dt-sidebar et .app-nav, ce qui masquerait la navigation entière
   au lieu des seules entrées réservées.
   ══════════════════════════════════════════════════════════════════════════════ */

/* 1 · Défaut sûr — tout ce qui est réservé au membre est masqué. */
[data-qw-auth="member"] { display: none; }

/* 2 · Session confirmée — on rétablit le display natif de chaque type d'élément.
   Énumération explicite plutôt que `revert` : seuls ces six types portent
   l'attribut, et un display juste vaut mieux qu'un mot-clé au support inégal. */
html.qw-auth-member .side-link[data-qw-auth="member"],
html.qw-auth-member .dt-nav-item[data-qw-auth="member"],
html.qw-auth-member .dt-dd-item[data-qw-auth="member"],
html.qw-auth-member .dt-dd-header[data-qw-auth="member"] { display: flex; }
/* La barre basse empile icône et libellé : son display natif est flex COLONNE. */
html.qw-auth-member .bnav-btn[data-qw-auth="member"] { display: flex; flex-direction: column; }

/* Les onglets de la barre basse sont désormais de vrais <a> (navigation sans JS,
   href lisible par highlightActive) et non plus des <button onclick>. */
.bottom-nav .bnav-btn { text-decoration: none; }
html.qw-auth-member .dt-dd-sep[data-qw-auth="member"]    { display: block; }
html.qw-auth-member .btn-logout[data-qw-auth="member"]   { display: block; }

/* 3 · … et symétriquement, le visiteur disparaît. */
html.qw-auth-member [data-qw-auth="anon"] { display: none; }

/* 3bis · STAFF — marchand OU super-admin. Troisième état, même patron que
   « member » : masqué par défaut, révélé par une classe posée sur <html> après
   lecture des claims (qwestifyPeindreEntreeStaff). Le défaut sûr reste le plus
   discret : un JS mort ne montre PAS une entrée d'administration.
   Le rôle est EXCLUSIF, l'entrée n'a donc jamais deux destinations à proposer. */
[data-qw-auth="staff"] { display: none; }
html.qw-auth-staff .dt-dd-item[data-qw-auth="staff"] { display: flex; }
html.qw-auth-staff .dt-dd-sep[data-qw-auth="staff"]  { display: block; }
html.qw-auth-staff .dt-nav-item[data-qw-auth="staff"] { display: flex; }
/* Le tiroir burger a son propre habillage : `.side-link`, pas `.dt-dd-item`.
   Il lui faut donc sa propre règle de révélation — l'énumération explicite du
   bloc « member » juste au-dessus suit exactement le même principe. */
html.qw-auth-staff .side-link[data-qw-auth="staff"] { display: flex; }

/* 3bis-2 · ADMIN — super-admin SEUL (lot 3A, 2026-10-26) : l'UI Lab ne s'affiche plus
   aux marchands. Même patron que « staff » : masqué par défaut, révélé par la classe
   html.qw-auth-admin que pose qwestifyPeindreEntreeStaff (shared.js). */
[data-qw-auth="admin"] { display: none; }
html.qw-auth-admin .dt-nav-item[data-qw-auth="admin"] { display: flex; }
html.qw-auth-admin .side-link[data-qw-auth="admin"] { display: flex; }

/* 3ter · … et l'invite B2B s'efface pour eux. Défaut INVERSE de « staff » :
   « not-staff » part VISIBLE et ne se masque que pour un staff confirmé. C'est
   voulu — l'invite « Vous êtes un commerce ? » est un chemin de conversion, et
   un JS en panne doit la laisser au joueur, pas la lui retirer.
   ⚠️ `!important` ici, et NULLE PART AILLEURS dans ce bloc : le CTA de pied de
   page d'index.html porte un `style="display:inline-flex;…"` en attribut, qui
   bat toute feuille de style. Sans lui, ce troisième lien resterait affiché à un
   marchand — soit exactement l'entrée trompeuse qu'on retire. On ne DÉPLACE pas
   ce style inline vers le JS (cela le soustrairait au cliquet sans le supprimer,
   cf. §15) : on le neutralise là où il est. */
html.qw-auth-staff [data-qw-auth="not-staff"] { display: none !important; }

/* 4 · Les entrées du burger sont désormais de vrais <a> (navigation sans JS,
   accessibles au clavier) et non plus des <button onclick>. */
.side-menu .side-link { text-decoration: none; }

/* 5 · Couple « Se connecter / S'inscrire » du pied de la feuille burger.
   En colonne : la feuille fait 280px et ces libellés respirent mal côte à côte. */
/* Bloc d'authentification du tiroir #dtSidebar (= le menu burger sous 1280px). */
.qw-side-auth { padding: 10px 16px 0; flex-shrink: 0; }
.qw-menu-anon { display: flex; flex-direction: column; gap: 8px; }
.qw-menu-anon .btn-anon-login,
.qw-menu-anon .btn-anon-signup {
  display: block; width: 100%; text-align: center;
  text-decoration: none; padding: 11px 20px; border-radius: var(--radius-md);
}

/* 6 · Stats GM réservées au membre. gm_state() est `revoke ... from anon` : un
   visiteur ne peut pas avoir de série, il ne doit donc pas en voir l'emplacement.
   (« Total GM » n'est pas masqué ici mais SUPPRIMÉ du markup : aucune colonne ni
   champ de gm_state() ne fournit ce total — les 143 affichés depuis le premier
   commit étaient un chiffre de maquette, pas un compteur en attente.) */
html.qw-auth-member .rp-info-row[data-qw-auth="member"] { display: flex; }
html.qw-auth-member .gm-stats[data-qw-auth="member"]    { display: flex; }

/* 7 · Bouton « Jouer » des cartes d'Arcade (arcade.html et section d'accueil).
   La règle vit ICI, avec les six autres, et non dans le bloc Arcade : le point
    2 ci-dessus ÉNUMÈRE les types d'éléments qui portent l'attribut, et une
   énumération dispersée dans la feuille est une énumération qu'on croit
   complète. Le pendant visiteur (.arc-cta--anon) n'a besoin d'aucune règle :
   « anon » part VISIBLE et c'est le point 3 qui l'efface au membre. */
html.qw-auth-member .arc-cta[data-qw-auth="member"] { display: inline-flex; }

/* ══════════════════════════════════════════════════════════════════════════════
   ÉCRAN « BIENTÔT DISPONIBLE » — bientot.html
   ──────────────────────────────────────────────────────────────────────────────
   Une fonctionnalité annoncée dans la navigation mais pas encore construite n'est
   PAS une erreur : elle mérite un écran d'attente, pas le 404 réservé aux mauvaises
   URL. Cet écran réutilise la coquille standard (topbar, rail, pied) pour que le
   joueur reste chez lui.

   .qw-soon-app remplace le style="display:block" que les autres pages posent en
   dur sur #app (masqué par défaut, style.css:161) : le plancher de styles inline
   ne doit jamais remonter.
   ══════════════════════════════════════════════════════════════════════════════ */
#app.qw-soon-app { display: block; }

.qw-soon {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 14px;
  min-height: min(58vh, 460px);
  padding: 48px 24px;
  margin: 0 auto; max-width: 520px;
}
.qw-soon-emoji { font-size: 56px; line-height: 1; }
.qw-soon-title { font-size: 28px; font-weight: 800; color: var(--text); margin: 0; }
.qw-soon-lead  { font-size: 16px; color: var(--text2); margin: 0; max-width: 42ch; }
.qw-soon-sub   { font-size: 14px; color: var(--text3); margin: 0; }
.qw-soon-back {
  margin-top: 10px; display: inline-flex; align-items: center; gap: 8px;
  padding: 12px 22px; border-radius: var(--radius-md);
  background: var(--orange); color: #fff; text-decoration: none;
  font-family: 'Inter', sans-serif; font-size: 15px; font-weight: 600;
  border: none; cursor: pointer; transition: filter .2s var(--ease, ease);
}
.qw-soon-back:hover { filter: brightness(1.08); }

@media (max-width: 480px) {
  .qw-soon { padding: 32px 18px; min-height: 50vh; }
  .qw-soon-emoji { font-size: 46px; }
  .qw-soon-title { font-size: 23px; }
}

/* ── Responsive de bientot.html ──
   Même piège que le portefeuille, même remède. Sous 768px le framework masque
   .dt-main pour révéler un #mobileContent séparé ; cette page n'en a pas (zéro
   duplication), donc son .dt-main doit rester visible — sinon l'écran est VIDE
   sur téléphone : seules les barres de navigation s'affichent, le message et le
   bouton de retour disparaissent.

   ⚠️ Le sélecteur est préfixé #app à dessein : « #app.logged-in-layout .dt-main
   { display:none !important } » pèse (1,2,0) et bat une version en
   .logged-in-layout seule (0,3,0). Entre deux !important de même origine, c'est
   la spécificité qui tranche — sans ce préfixe, le :has() ne servirait à rien.
   Scopé par :has(.qw-soon-body) : aucune autre page n'est touchée. */
@media (max-width: 768px) {
  #app.logged-in-layout .dt-main:has(.qw-soon-body) {
    display: flex !important; flex-direction: column; margin-left: 0 !important;
  }
  .dt-body.qw-soon-body { display: block; padding: 8px 14px 96px; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   COLONNE CENTRALE — empêcher les grilles de déborder sous le panneau droit
   ──────────────────────────────────────────────────────────────────────────────
   Un élément de grille vaut `min-width:auto` par défaut, et une piste `1fr` est
   `minmax(auto, 1fr)` : la piste ne peut donc PAS descendre sous la largeur
   min-content de son contenu. Résultat, .dt-center (pourtant en min-width:0)
   mesurait 1110px de contenu pour 572 à 732px de place dès que le panneau
   d'activité était ouvert — les cartes filaient sous « Mon activité ».

   Le plancher venait des ITEMS, pas des conteneurs : on pose donc min-width:0
   sur les enfants, et AUCUNE définition de colonnes n'est retouchée. Les
   media queries qui font 3 → 2 → 1 colonne restent seules maîtres du nombre de
   colonnes ; on ne leur retire que le droit de veto du contenu.

   Mesuré : .dt-center passe de scrollWidth 1110 / clientWidth 732 à 732 / 732
   à 1440px, sans changer le nombre de colonnes (3 pour les quêtes, 2 pour les
   campagnes). Défaut PRÉEXISTANT — présent à l'identique avant l'ajout de la
   section « Dernières quêtes », qui n'a fait que s'y ajouter.
   ══════════════════════════════════════════════════════════════════════════════ */
.dt-two-lists > *,
.onboarding-grid > *,
.lc-grid > *,
.gptp-container > *,
.ob-card,
.ob-quest-body,
.ob-card-footer,
.ls-item,
.ls-info { min-width: 0; }

/* ══════════════════════════════════════════════════════════════════════════════
   QUÊTES TERMINÉES — cartes d'archive, pas des portes
   ──────────────────────────────────────────────────────────────────────────────
   Une quête dont l'échéance est passée reste techniquement ouvrable : le serveur
   ne refuse que sur le STATUT (complete_quest → quest_not_active), pas sur la
   date. Un joueur pouvait donc dérouler tout le tunnel pour s'entendre refuser
   l'XP à la dernière étape. On ferme la porte AVANT plutôt qu'après.

   Le href est retiré en JS (quest-lists.js) — sans quoi le lien reste focusable
   et s'ouvre à la touche Entrée. Le CSS ci-dessous ne fait que le DIRE à l'œil :
   la souris n'a plus rien à cliquer, la carte se lit comme une archive.

   Le badge passe par ::after sur le titre, patron déjà employé pour le cadenas
   des cartes campagne en mode visiteur (style.css:1770) : aucune modification
   de window.UI_buildQuestCard, qui sert toutes les autres sections.
   ══════════════════════════════════════════════════════════════════════════════ */
.qw-quests-ended .ob-card-link { pointer-events: none; cursor: default; }

.qw-quests-ended .ob-card {
  filter: grayscale(0.85);
  opacity: 0.55;
  transition: none;                 /* pas d'animation d'entrée : rien à survoler */
}
.qw-quests-ended .ob-card:hover { transform: none; box-shadow: none; }

.qw-quests-ended .ob-card-title::after {
  content: " · Terminée";
  color: var(--text3);
  font-weight: 700;
  white-space: nowrap;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LOT 1 — L'ACCUEIL RÉEL SUR TÉLÉPHONE (≤768px)

   Sous 768px l'accueil montrait #mobileContent : une maquette 100% statique
   (aucun .js ne la touche) dont les « campagnes » sont des offres inventées
   expirant en 2025. Pendant ce temps la vraie colonne .dt-center tournait déjà
   sur téléphone — campaign-grid.js / featured-quests.js / quest-lists.js
   remplissaient leurs grilles avec les données de la base, et setFeaturedFilter
   répondait correctement — mais .dt-main était display:none. On ne DUPLIQUE
   donc aucun filtre dans la maquette : on révèle le vrai contenu et on masque
   la maquette. Zéro JS touché.

   ⚠️ PORTÉE — style.css est partagé par TOUTES les pages. Un simple
   « .dt-main{display:flex} » révélerait la colonne desktop de campagne.html,
   profil.html, classement.html… On s'accroche donc à #dtCampFilterBlock,
   présent dans index.html et NULLE PART ailleurs (vérifié). Si cet id est un
   jour renommé, l'accueil mobile redevient silencieusement la maquette :
   renommer les deux ensemble.

   ⚠️ SPÉCIFICITÉ — même piège que le portefeuille (voir le bloc :has(.wal-body)
   plus haut) : « #app.logged-in-layout .dt-main{display:none!important} » (1-2-0)
   doit être dépassé. Le :has(#id) porte la nôtre à 2-2-0. Les inline styles de
   .dt-body / .dt-two-lists (index.html) imposent en plus !important sur les
   grilles.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {

  /* 1 — Révélation de la colonne réelle. La règle sœur qui masquait ici
     #mobileContent est partie avec la maquette. Les règles génériques du
     framework l'ont suivie : plus AUCUNE page ne possède #mobileContent. */
  #app.logged-in-layout .dt-main:has(#dtCampFilterBlock) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
  }

  /* 2 — Une seule colonne : la piste droite réservée au panneau « Mon activité »
     étranglait .dt-center à 239px sur 390 de large. Le panneau reste accessible
     par le tiroir ; il n'a pas de place en propre sur un téléphone. */
  .dt-main:has(#dtCampFilterBlock) .dt-body {
    grid-template-columns: 1fr !important;
    gap: 0 !important;
    padding: 14px 14px 24px !important;   /* + les 64px de #app (barre du bas) */
  }
  .dt-main:has(#dtCampFilterBlock) .dt-right  { display: none !important; }
  /* !important obligatoire : .dt-center porte lui aussi un gap:24px inline
     (index.html:975). Sans lui la règle est du code mort — la spécificité ne
     bat jamais un attribut style. */
  .dt-main:has(#dtCampFilterBlock) .dt-center { gap: 18px !important; }

  /* 3 — Carrousel : PRÉSENT sur téléphone, au doigt.
     Il avait été masqué faute de pouvoir s'en servir : ses flèches
     n'apparaissent qu'au survol, geste qui n'existe pas au doigt. On ne
     reconstruit rien — mêmes diapositives, mêmes points, même rotation
     automatique : on remplace seulement les flèches par le balayage (le
     geste est branché dans index.html, sur dtGo) et par les points, qui
     portaient déjà leur onclick.

     touch-action:pan-y est la pièce maîtresse : elle rend l'axe horizontal
     à notre gestionnaire et LAISSE le défilement vertical au navigateur.
     Sans elle, un balayage un peu oblique ferait défiler la page. */
  .dt-main:has(#dtCampFilterBlock) #dtAutoCarousel {
    touch-action: pan-y;
    overflow: hidden;            /* le geste reste dans le carrousel, jamais dans la page */
    border-radius: 16px;
  }
  /* Les flèches, elles, n'ont pas de sens sans survol. */
  .dt-main:has(#dtCampFilterBlock) .dt-arrow { display: none !important; }

  /* Points : cible de doigt, agrandie par un pseudo-élément sans grossir le
     rond visible (6px, 18px une fois actif).
     ⚠️ La largeur de cette cible doit rester ADOSSÉE AU PAS, jamais au 44px
     habituel : avec 6px de rond et 10px d'écart, le pas fait 16px — une cible
     de 44px chevauchait TROIS voisins et chaque tap atterrissait à côté.
     Mesuré : Playwright refusait le clic, « le point voisin intercepte ».
     Ici : 22px d'écart → 28px de pas → cible de 28px, jointive et sans
     recouvrement. La hauteur, elle, peut être généreuse : rien au-dessus ni
     en dessous ne dispute le geste. */
  .dt-main:has(#dtCampFilterBlock) #dtDots { gap: 22px; margin-top: 12px; }
  .dt-main:has(#dtCampFilterBlock) #dtDots .cdot { position: relative; }
  .dt-main:has(#dtCampFilterBlock) #dtDots .cdot::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 28px; height: 40px; transform: translate(-50%, -50%);
  }

  /* 4 — Barre de filtres : neuf pastilles ne tiennent pas sur 390px, elles
     défilent horizontalement (ascenseur masqué, on fait défiler au doigt). */
  .dt-main:has(#dtCampFilterBlock) .dt-filters {
    display: flex; align-items: center; gap: 8px;
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .dt-main:has(#dtCampFilterBlock) .dt-filters::-webkit-scrollbar { display: none; }
  .dt-main:has(#dtCampFilterBlock) .dt-fb {
    flex: 0 0 auto;
    padding: 9px 14px; border-radius: 20px;
    border: 1px solid var(--border2); background: transparent;
    color: var(--text2); font-size: 13px; font-weight: 600;
    cursor: pointer; white-space: nowrap; transition: all .2s;
  }
  .dt-main:has(#dtCampFilterBlock) .dt-fb.active {
    background: var(--orange); border-color: var(--orange); color: #fff;
  }

  /* 5 — En-têtes de bloc : recopie des définitions desktop (l.560-564), qui
     vivent dans @media(min-width:1024px) et n'existaient donc pas ici. */
  .dt-main:has(#dtCampFilterBlock) .dt-block    { display: flex; flex-direction: column; gap: 8px; }
  .dt-main:has(#dtCampFilterBlock) .dt-block-hd { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  /* Pas de font-size ici : l.1404 fixe déjà .dt-block-title à var(--text-base)
     pour le mobile. Le redéclarer à --text-md l'écraserait (spécificité bien
     supérieure) et remonterait les titres à 13px, contre l'échelle voulue. */
  .dt-main:has(#dtCampFilterBlock) .dt-block-title { font-weight: 800; letter-spacing: -.3px; }
  .dt-main:has(#dtCampFilterBlock) .dt-seeall {
    flex: 0 0 auto;
    font-size: 13px; font-weight: 600; color: var(--text3);
    background: none; border: none; cursor: pointer;
  }

  /* 6 — « Nouveau » / « Tendances » : deux colonnes de 109px illisibles, on
     empile. */
  .dt-main:has(#dtCampFilterBlock) .dt-two-lists { grid-template-columns: 1fr !important; }

  /* 7 — Pied de carte : sur une carte de 176px, le pied dispose de 134px et
     doit loger « Nouveau » (77px) + deux pastilles de 34px (76px) + la
     gouttière = 161px. Aucun des deux ne peut rétrécir sous son contenu (une
     image de 34px n'a pas de min-content plus petit, le badge non plus), et la
     rangée sortait donc de la carte.

     Un simple min-width:0 sur la rangée est un PIÈGE, mesuré : la rangée tombe
     à 0px de large et, en justify-content:flex-end, ses pastilles se posent
     HORS de la carte — jusqu'à 69px de décalage, par-dessus la colonne voisine.
     On empile plutôt le pied : le texte, puis les pastilles, chacun à sa
     taille pleine. Rien n'est rogné, rien ne sort. */
  /* ⚠️ RESTREINT AUX CARTES CAMPAGNE (lot c2). Ce remède visait le pied de la
     carte QUÊTE, qui logeait alors cinq enfants larges (« Nouveau »,
     « Sponsorisé », distance, statut, deux pastilles de 34px) et débordait.
     Le pied de la carte quête n'en porte plus que deux, compacts : compteur à
     gauche, badges à droite — 88px pour 146px disponibles, mesuré. L'empiler
     en colonne le casserait au lieu de le sauver. Les cartes campagne, elles,
     gardent le comportement d'origine. */
  .dt-main:has(#dtCampFilterBlock) .ob-campaign-card .ob-card-footer {
    flex-direction: column;
    /* nowrap OBLIGATOIRE : en direction column, le flex-wrap:wrap de la règle
       de base ferait wrapper sur l'axe transverse — donc en COLONNES. */
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: 10px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LA CAMPAGNE RÉELLE SUR TÉLÉPHONE (≤768px) — campagne.html

   Même remède que pour l'accueil, mais un mal plus grave : ici la maquette
   #mobileContent était RÉELLEMENT PEINTE. Le joueur tapait une campagne
   précise et son téléphone lui répondait par une offre Uber inventée et
   « 1 247 Participants » — pendant que les vraies données, chargées par
   initCampaign(), attendaient derrière un display:none. Mesuré : 4 textes
   fictifs peints à 375 comme à 390, visiteur comme membre.

   ⚠️ CROCHET — style.css est partagé. On s'accroche à #campaignQuestsTitle,
   présent dans campagne.html et NULLE PART ailleurs. Surtout PAS
   #campaignGrid : merchant-dashboard.html porte le même id, et la règle
   déborderait sur le dashboard B2B.

   ⚠️ SPÉCIFICITÉ — même piège que pour l'accueil et le portefeuille :
   « #app.logged-in-layout .dt-main{display:none!important} » (1-2-0) doit
   être dépassé ; le :has(#id) porte la nôtre à 2-2-0. Les styles inline de
   .dt-body (campagne.html:691) imposent en plus !important sur la grille.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {

  /* 1 — Bascule de coquille : la vraie campagne prend la place de la maquette. */
  #app.logged-in-layout .dt-main:has(#campaignQuestsTitle) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
  }

  /* 2 — Une seule colonne : la piste droite réservée au panneau d'activité
     n'a pas de place sur un téléphone (le tiroir la rend accessible). */
  .dt-main:has(#campaignQuestsTitle) .dt-body {
    grid-template-columns: 1fr !important;
    gap: 0 !important;
    padding: 14px 14px 24px !important;   /* + les 64px de #app (barre du bas) */
  }
  .dt-main:has(#campaignQuestsTitle) .dt-right  { display: none !important; }
  .dt-main:has(#campaignQuestsTitle) .dt-center { gap: 18px !important; }

  /* 3 — Rangée « Partager » : .campaign-hero-actions est en flex-shrink:0
     (l.2385) et débordait de 3 éléments à 375px. On l'autorise à passer à la
     ligne plutôt qu'à sortir de l'écran. */
  .dt-main:has(#campaignQuestsTitle) .campaign-hero-actions {
    flex-wrap: wrap;
    flex-shrink: 1;
    min-width: 0;
  }
  .dt-main:has(#campaignQuestsTitle) .share-container { max-width: 100%; }

  /* 4 — Pied de carte empilé : sur une carte étroite, le badge et les
     pastilles de récompense ne tiennent pas sur une ligne et la rangée
     sortait de la carte (démontré sur l'accueil). */
  /* ⚠️ RESTREINT AUX CARTES CAMPAGNE (lot c2) — même raison qu'à l'accueil :
     le pied de la carte quête ne porte plus que deux blocs compacts et tient
     largement sur une ligne. L'empiler en colonne le casserait. */
  .dt-main:has(#campaignQuestsTitle) .ob-campaign-card .ob-card-footer {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: 10px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════════
   RANGÉES DÉFILANTES DE L'ACCUEIL — .qw-rail
   ──────────────────────────────────────────────────────────────────────────────
   Quatre sections passent de la grille qui s'empile à UNE rangée qu'on balaie :
   « Autour de moi », « Pour commencer », « Dernières campagnes », « Dernières
   quêtes ». Les cartes ne changent NI de dessin NI de taille — seul le
   conteneur change de comportement. Zéro ligne de JS : les quatre renderers
   écrivent déjà `container.innerHTML = <liste de <a class="ob-card-link">>` et
   aucun d'eux ne sait comment son conteneur dispose ses enfants (vérifié :
   getBoundingClientRect / clientWidth / offsetWidth / scrollLeft n'existent
   nulle part dans le dépôt hors vendor).

   ⚠️ POURQUOI UNE CLASSE MODIFICATRICE, ET SURTOUT PAS .onboarding-grid / .lc-grid.
   Ces deux classes portent DOUZE conteneurs, pas quatre. On y trouve les vues
   « Afficher tout » (#lqGridEnCours, #lqGridTerminees, #lcGridEnCours,
   #lcGridTerminees) — dont la raison d'être est justement de tout montrer en
   grille, ce qu'un rail contredirait — et surtout merchant-dashboard.html, soit
   l'espace B2B du §8. La classe se pose donc sur les quatre conteneurs visés,
   nommément, dans index.html.

   ⚠️ CINQ PIÈGES, tous silencieux, tous mesurés avant d'écrire ce bloc :

   1. `min-width:0` est posé sur `.onboarding-grid > *` et `.lc-grid > *`
      (plus haut dans ce fichier). En grille c'est un remède ; en flex nowrap
      c'est l'inverse — flex-shrink vaut 1 par défaut, et sans plancher les
      cartes se compriment à l'infini au lieu de déborder : LE RAIL NE DÉFILE
      JAMAIS. D'où le `flex: 0 0 …` ci-dessous, qui remet un plancher.

   2. `.dt-block` est en flex colonne et n'a PAS de min-width:0. Un enfant flex
      nowrap a un min-content égal à la SOMME de ses items : sans min-width:0
      sur le rail, il pousse le bloc, qui pousse la page. Et comme
      `body{overflow-x:hidden}`, le débordement serait ROGNÉ, pas visible : on
      perdrait des cartes sans jamais voir de barre horizontale.

   3. Poser overflow-x:auto force le navigateur à calculer overflow-y en `auto`
      (et non `visible`). Le soulèvement au survol de .ob-card et son ombre
      seraient tranchés, avec un ascenseur vertical parasite dans chaque rail.
      D'où overflow-y:hidden explicite et un padding vertical qui loge l'ombre.

   4. `.ob-card-link{height:100%}` et `.ob-card{height:100%}` ne tiennent que
      par le `align-items:stretch` par défaut. Poser `flex-start` — réflexe
      fréquent sur un rail — ferait retomber les deux sur `auto` et les cartes
      cesseraient d'être de même hauteur. On garde stretch, explicitement.

   5. `.fq-empty{grid-column:1/-1}` est une propriété de GRILLE : elle devient
      inerte en flex. C'est la raison pour laquelle #nearbyPrompt — qui ne
      reçoit jamais de carte, seulement les messages de géolocalisation —
      reste HORS du rail. Seul #nearbyContainer le devient.

   ⚠️ FOND PERDU. La rangée récupère la marge de page (marge négative rendue en
   padding interne) pour deux raisons : une rangée qui s'arrête avant le bord de
   l'écran a l'air coupée, et c'est cet espace récupéré qui laisse dépasser la
   carte suivante. La première carte reste alignée sur le titre de section.

   ⚠️ LA TAILLE DES CARTES EST CELLE D'AUJOURD'HUI, AU PIXEL. La largeur n'est
   pas écrite en dur : elle se recalcule à partir de la largeur de CONTENU
   (100% du rail moins le fond perdu), exactement comme le faisait la grille.
   --qw-cartes = le nombre de cartes pleines visibles, et c'est le SEUL réglage
   à toucher pour arbitrer entre « taille exacte » et « liseré plus large » :
   une valeur entière rend la taille d'aujourd'hui, une valeur fractionnaire
   (2,2 par ex.) rétrécit les cartes d'autant et élargit le liseré.
   ══════════════════════════════════════════════════════════════════════════════ */
.qw-rail {
  --qw-bleed: 14px;          /* marge de page récupérée (mobile) */
  --qw-gap: 10px;            /* identique au gap de la grille d'aujourd'hui */
  /* Cartes visibles. La DÉCIMALE est le liseré : à 2, deux cartes remplissent
     l'écran au pixel près et la suivante ne dépasse que de 4px — invisible,
     donc personne ne devine qu'on peut balayer. À 2,2 elle dépasse de 36px et
     l'affordance existe, au prix de 10% de largeur. Arbitrage assumé : deux
     cartes occupent déjà 90% d'un écran de 375px, il fallait bien prendre la
     place du liseré quelque part. */
  --qw-cartes: 2.2;

  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;                                  /* piège 4 */
  gap: var(--qw-gap);
  min-width: 0;                                          /* piège 2 */

  margin-inline: calc(-1 * var(--qw-bleed));             /* fond perdu */
  padding-inline: var(--qw-bleed);
  padding-block: 4px 12px;                               /* loge l'ombre du survol */

  overflow-x: auto;
  overflow-y: hidden;                                    /* piège 3 */
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: var(--qw-bleed);          /* la carte s'aligne sur le titre */
  scrollbar-width: none;                                 /* Firefox */
}
.qw-rail::-webkit-scrollbar { display: none; }

/* Un rail sans carte ne doit pas exister : sinon son padding vertical laisse
   une bande de 16px sous le titre — mesuré sur « Autour de moi » quand la
   géolocalisation est refusée, cas où le bloc reste visible pour porter son
   message alors que le conteneur de cartes, lui, est vide. */
.qw-rail:empty { display: none; }

/* Le plancher qui rend le défilement possible (piège 1), et la largeur
   d'aujourd'hui : (largeur de contenu − les gaps) ÷ nombre de cartes.

   ⚠️ Le 100% N'INCLUT PAS le fond perdu. Un pourcentage de flex-basis se
   résout sur la boîte de CONTENU du conteneur, padding déduit — or le fond
   perdu EST ce padding. Le soustraire une seconde fois rétrécissait les cartes
   de 14px de chaque côté : mesuré 155px au lieu de 168,5px à 375px. Le piège
   est d'autant plus sournois que le résultat était joli — deux cartes et un
   large liseré — donc plausible. */
.qw-rail > .ob-card-link {
  flex: 0 0 calc(
    (100% - (var(--qw-cartes) - 1) * var(--qw-gap)) / var(--qw-cartes)
  );
  scroll-snap-align: start;
}

/* Les campagnes gardent leur carte pleine largeur : UNE par vue sur téléphone,
   comme aujourd'hui — la réduire de moitié pour en montrer deux serait un
   changement de dessin, pas une mise en rangée. La décimale joue le même rôle
   que ci-dessus : elle réserve le liseré de la suivante. */
.lc-grid.qw-rail { --qw-cartes: 1.12; }

@media (min-width: 600px) {
  .onboarding-grid.qw-rail { --qw-gap: 12px; }
  .lc-grid.qw-rail         { --qw-cartes: 2.15; --qw-gap: 14px; }
}
@media (min-width: 769px) {
  /* Sur large écran la piste a sa propre gouttière : le fond perdu y reprend
     la valeur du padding de .dt-body, pas celle du téléphone. */
  .qw-rail { --qw-bleed: 24px; }
}
@media (min-width: 1024px) {
  .onboarding-grid.qw-rail { --qw-cartes: 3.2; --qw-gap: 10px; }
  .lc-grid.qw-rail         { --qw-cartes: 2.15; --qw-gap: 16px; }
}

/* ── Conteneur des sections « Découvrez [marchand] » ───────────────────────────
   Il accueille N .dt-block générés par quest-lists.js. Il lui faut son propre
   rythme vertical : .dt-center est en flex-column avec gap:24px, et ce gap
   s'applique ENTRE ses enfants directs — donc autour de ce conteneur, pas entre
   les sections qu'il contient. Sans ces trois lignes, les sections seraient
   collées les unes aux autres.
   :empty — aucune campagne n'a de quête, ou le rendu n'a pas encore eu lieu :
   le conteneur disparaît. Un div vide de 0px consommerait quand même une
   gouttière de 24px, soit un trou inexpliqué sous « Dernières quêtes ». */
#dtCampQuestsHost {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
#dtCampQuestsHost:empty { display: none; }

/* ══════════════════════════════════════════════════════════════════════════════
   MENU DE L'AVATAR (mobile) — .qw-avmenu
   ──────────────────────────────────────────────────────────────────────────────
   Le clic sur la pastille ouvrait le panneau « Mon espace » ; il ouvre désormais
   ce menu. Il réutilise les entrées du menu déroulant BUREAU (.dt-dd-item, plus
   haut dans ce fichier) : mêmes classes, mêmes destinations, même déconnexion.
   Seule l'enveloppe est propre au mobile.

   ⚠️ position:fixed, et non absolute. La barre .app-nav est en overflow:hidden
   — un menu posé dedans serait tranché à 56px de haut. Il est donc injecté
   APRÈS elle (shared.js) et ancré au viewport : sous la barre, calé à droite,
   c'est-à-dire sous la pastille. Le fixed l'affranchit en prime de tout
   contexte d'empilement créé par un ancêtre.

   ⚠️ z-index au-dessus de .app-nav (100) mais SOUS les feuilles modales : ce
   menu est du chrome, pas une boîte de dialogue.

   Il ne s'affiche qu'ouvert (.open) ; sur bureau il n'existe pas à l'écran,
   la barre .app-nav étant elle-même masquée au-dessus de 768px — et le menu
   déroulant #dtDropdown y tient déjà ce rôle. */
.qw-avmenu {
  position: fixed;
  top: 60px;
  right: 10px;
  z-index: 120;
  min-width: 208px;
  padding: 6px;
  display: none;
  flex-direction: column;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, .45);
}
.qw-avmenu.open { display: flex; }

/* Au-dessus de 768px la barre mobile disparaît : le menu n'a plus lieu d'être,
   même resté ouvert avant un redimensionnement. */
@media (min-width: 769px) {
  .qw-avmenu { display: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   CHEVRON + PANNEAU DE STATISTIQUES — .qw-chevron / .qw-stats
   ──────────────────────────────────────────────────────────────────────────────
   Deuxième surface ouverte depuis l'en-tête mobile, voisine du menu avatar et
   bâtie dans le même vocabulaire : fond --bg2, bord --border, rayon 14, ombre
   portée franche. Elles ne s'ouvrent jamais ensemble (délégation, shared.js).

   ⚠️ L'en-tête est PLEIN à 375px en membre : 331px occupés sur 355 utiles, et
   `overflow:hidden`. Le chevron est donc tenu au strict nécessaire — 22px de
   large, aucune gouttière propre — et c'est le mot-symbole, seul élément
   élastique de la barre, qui rend la place.
   ══════════════════════════════════════════════════════════════════════════════ */
/* ⚠️ MASQUÉ PAR DÉFAUT, RÉVÉLÉ AU MEMBRE — et il faut le déclarer ici, pas se
   reposer sur le data-qw-auth="member" du bouton. La règle du §11
   (`[data-qw-auth="member"]{display:none}`, style.css:8890) pèse (0,1,0),
   exactement comme `.qw-chevron` — mais ce bloc vient APRÈS dans le fichier, et
   l'emportait donc. Mesuré : en visiteur sur index, l'avatar était bien masqué
   (0px) et le chevron restait à 22px, ouvrant un panneau plein de tirets.
   Le sens de la règle suit le §11 : défaut = visiteur, le membre est ADDITIF.
   Un CSS à moitié chargé ou une session en échec laissent le chevron caché,
   jamais l'inverse. */
.qw-chevron { display: none; }
html.qw-auth-member .qw-chevron {
  flex-shrink: 0;
  width: 22px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; margin-left: -2px;
  background: none; border: none;
  color: var(--text2); cursor: pointer;
  transition: color .2s ease;
}
.qw-chevron:hover { color: var(--text); }
/* Le pivot > ↔ <. Une rotation, pas un second dessin : deux SVG finiraient par
   diverger, et il n'y a ici qu'une seule vérité — le panneau est ouvert ou non. */
.qw-chevron-icone { transition: transform .25s var(--ease); }
.qw-chevron.is-open .qw-chevron-icone { transform: rotate(180deg); }

.qw-stats {
  position: fixed;
  top: 60px; right: 10px;
  z-index: 120;
  width: 268px; max-width: calc(100vw - 20px);
  padding: 12px;
  display: none; flex-direction: column; gap: 8px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, .45);
}
.qw-stats.open { display: flex; }
@media (min-width: 769px) { .qw-stats { display: none !important; } }

.qw-stats-ligne { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.qw-stats-label { font-size: 12px; font-weight: 600; color: var(--text2); }
.qw-stats-val   { font-size: 15px; font-weight: 800; color: var(--text); }
.qw-stats-sep   { height: 1px; background: var(--border); margin: 2px 0; }

/* ⚠️ Le niveau réutilise .lvl-badge — la classe que profile-sync écrit DÉJÀ en
   querySelectorAll, ce qui donne la valeur au panneau sans une ligne de JS de
   plus. Mais .lvl-badge (style.css:238) n'est pas un simple crochet : c'est la
   PASTILLE D'ANGLE de l'avatar, en position:absolute; bottom:-4px; right:-4px.
   Posée telle quelle dans le panneau, elle s'en échappait et allait se coller
   au bord de la page en carré orange — mesuré à right:-4px, top:352px.
   On la remet donc en flux ici, et on lui rend l'apparence des autres valeurs.
   Spécificité (0,2,0) : bat .lvl-badge, et passe après .is-avatar-loaded
   .lvl-badge, de même poids. */
.qw-stats .lvl-badge {
  position: static;
  background: none; border: none; padding: 0;
  font-size: 15px; font-weight: 800; color: var(--text);
  border-radius: 0;
}

/* Série GM : le nombre à GAUCHE, le bouton à DROITE.
   ⚠️ `flex-wrap: wrap` n'est pas de la précaution : le libellé du bouton CHANGE
   d'état en état — « ☀️ Valider » au repos, « ✓ GM validé aujourd'hui » une
   fois la série prise, et les deux mécanismes GM l'écrivent sans rien savoir de
   la place disponible. À 320px, le libellé long ne tient plus sur la ligne :
   sans repli, c'est « Série GM » qui se cassait en deux mots empilés. Avec, le
   bouton descend proprement sous le compteur. Mesuré : une seule ligne à 375
   dans les deux états, repli à 320 dans l'état validé seulement. */
.qw-stats-gm { flex-wrap: wrap; row-gap: 8px; }
.qw-stats-serie { display: flex; align-items: baseline; gap: 7px; min-width: 0; white-space: nowrap; }
.qw-stats-serie .qw-stats-val { color: var(--yellow); }
/* ⚠️ `width: auto` n'est pas redondant : le bouton porte .gm-btn, indispensable
   pour que les deux mécanismes d'état GM le trouvent — et .gm-btn (style.css:340)
   impose `width:100%`. Sans cette reprise, le bouton occupait TOUTE la ligne et
   écrasait la série à 0px de large (mesuré : bouton 242px, série 0px).
   Le commentaire de marqueMenuAvatar affirmait que .gm-btn ne portait aucun
   style — il a été corrigé avec ce lot. */
.qw-stats-gmbtn {
  flex-shrink: 0; width: auto;
  padding: 7px 12px; border-radius: 9px; border: none;
  background: linear-gradient(135deg, var(--orange), #FF8C55);
  color: #fff; font-family: 'Inter', sans-serif;
  font-size: 12px; font-weight: 700; cursor: pointer;
}
.qw-stats-gmbtn:hover { filter: brightness(1.06); }
/* .claimed est posée par les DEUX mécanismes d'état GM (gmSetDone dans
   shared.js, _gmSetAllBtnsDone sur index/campagne) — ils écrivent aussi des
   styles en ligne, qui l'emportent ; cette règle couvre les pages où seul le
   premier passe. */
.qw-stats-gmbtn.claimed { background: var(--bg3); color: var(--text3); cursor: default; filter: none; }

.qw-stats-titre {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .8px; color: var(--text3);
}
.qw-stats-liste { display: flex; flex-direction: column; gap: 6px; }
/* La liste est rendue par profile-sync (mêmes classes .acv2-act-* que la carte
   « Mon activité ») : elle hérite donc de son dessin sans une règle de plus.
   Seul l'entête interne, inutile dans un panneau qui a déjà son titre, est
   replié ici. */
.qw-stats-liste .acv2-act-head { display: none; }

/* Ex-styles inline du rendu d'activité (profile-sync), devenus des classes. */
.acv2-act-head {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .8px; color: var(--text3); margin-bottom: 6px;
}
.acv2-act-foot { text-align: center; padding: 8px 0 0; }
.acv2-act-foot a { color: var(--orange); text-decoration: none; font-size: 12px; font-weight: 600; }

/* ══════════════════════════════════════════════════════════════════════════════
   RECHERCHE GLOBALE — .qw-rech
   ──────────────────────────────────────────────────────────────────────────────
   Une seule feuille pour les deux affordances : le bouton loupe (≤768px) et le
   champ .dt-search (≥769px), qui ne coexistent jamais — la bascule est nette à
   768/769. position:fixed, donc aucune ascendance à respecter et aucun contexte
   d'empilement à craindre : la feuille est ajoutée au <body>.

   Elle couvre l'écran sur téléphone (la place manque pour autre chose) et se
   pose en panneau centré sur large écran, sous la barre.

   ⚠️ z-index au-dessus du chrome (.app-nav = 100, menu avatar = 120) : la
   recherche est une surface modale, elle passe devant. Le voile capte le clic
   extérieur sans écouteur supplémentaire.
   ══════════════════════════════════════════════════════════════════════════════ */
.qw-rech {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: none;
  flex-direction: column;
  background: var(--bg);
}
.qw-rech.open { display: flex; }
/* Le fond de page ne défile plus derrière la feuille. */
html.qw-rech-ouverte, html.qw-rech-ouverte body { overflow: hidden; }

.qw-rech-barre {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg2);
}
.qw-rech-barre input {
  flex: 1 1 auto; min-width: 0;
  height: 40px; padding: 0 14px;
  background: var(--bg3); color: var(--text);
  border: 1px solid var(--border2); border-radius: 10px;
  font-family: 'Inter', sans-serif; font-size: 15px;
}
.qw-rech-barre input:focus { outline: none; border-color: var(--orange); }
.qw-rech-fermer {
  flex-shrink: 0; width: 40px; height: 40px;
  background: var(--bg3); color: var(--text);
  border: 1px solid var(--border2); border-radius: 10px;
  font-size: 16px; cursor: pointer;
}

.qw-rech-corps { flex: 1 1 auto; overflow-y: auto; padding: 8px 8px 24px; }

.qw-rech-groupe {
  padding: 14px 10px 6px;
  font-size: 12px; font-weight: 800; letter-spacing: .6px;
  text-transform: uppercase; color: var(--text3);
}

.qw-rech-item {
  display: flex; align-items: center; gap: 12px;
  padding: 10px; border-radius: 12px;
  color: var(--text); text-decoration: none;
}
.qw-rech-item:hover { background: var(--bg2); }
/* Les joueurs n'ont pas de destination : ni curseur de lien, ni survol qui
   promette un clic. L'absence de page de profil public est une réalité du
   produit, pas un oubli — autant que l'interface le dise. */
.qw-rech-item.is-inerte { cursor: default; }
.qw-rech-item.is-inerte:hover { background: transparent; }

.qw-rech-vignette {
  flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px;
  background: var(--bg3); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; color: var(--text3);
}
.qw-rech-vignette img { width: 100%; height: 100%; object-fit: contain; }

.qw-rech-txt { display: flex; flex-direction: column; min-width: 0; }
.qw-rech-titre {
  font-size: 15px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qw-rech-sous { font-size: 13px; color: var(--text3); }

.qw-rech-vide { padding: 24px 12px; color: var(--text3); font-size: 14px; }

@media (min-width: 769px) {
  /* Panneau posé sous la barre, pas plein écran : sur large écran, occulter
     toute la page pour une liste de six lignes serait disproportionné. */
  .qw-rech {
    inset: 72px auto auto 50%;
    transform: translateX(-50%);
    width: min(640px, calc(100vw - 48px));
    max-height: min(70vh, 560px);
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, .5);
    overflow: hidden;
  }
  .qw-rech-barre { background: transparent; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   VITRINE — HAUT DE #landing (étape 1)   ·   barre + hero + parcours

   ⚠️ TOUT EST SCOPÉ SOUS #landing, ET CE N'EST PAS DE LA COQUETTERIE.
   `nav#landingNav` N'APPARTIENT PAS À index.html : partenaires.html et
   inscription-commerce.html portent le MÊME id et chargent la MÊME feuille.
   Une règle écrite `nav#landingNav { … }` repeindrait donc deux pages qui ne
   demandent rien. D'où `#landing #landingNav` partout — un ancêtre qu'elles
   n'ont pas.

   ⚠️ .vt-hero ET .vt-parcours SONT DES <section>, et `section{}` est redéfini
   à QUATRE reprises dans cette feuille (l.110 · l.1077 @min-1024 · l.1919
   @max-768 · l.2061 @max-400). À ≤768 px, `section{padding:52px 16px}` ne
   dégagerait que 52 px sous une barre FIXE de 60 px : le <h1> passerait
   dessous. Les règles ci-dessous reposent donc leur propre échelle. Une seule
   déclaration par élément suffit : une classe (0,1,0) bat un sélecteur
   d'élément (0,0,1) dans les quatre contextes — une media query n'ajoute
   aucune spécificité.

   TYPOGRAPHIE — deux voix, délibérément :
     · --vt-sf  = la voix du DISCOURS (titre, accroche, CTA, légendes).
       C'est la pile système d'Apple, déjà en service dans cette feuille pour
       les feuilles GM (« esthétique Apple », commit b1320c1). Écart assumé
       au « tout Inter » du §2, avec précédent dans le dépôt.
     · 'Inter' = la voix du PRODUIT (le pastille d'XP, le sur-titre). Ce sont
       des données, elles parlent comme l'application.

   COULEUR — un seul accent, --orange. Aucun violet (le --violet du vieux
   hero-glow2 est parti avec lui). Le vert n'apparaît QUE dans les captures :
   c'est la couleur du produit, pas celle de la page.
   ═══════════════════════════════════════════════════════════════════════ */

#landing {
  --vt-sf: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text',
           system-ui, 'Segoe UI', sans-serif;
  --vt-dalle: 236px;              /* largeur d'un écran du parcours */
  --vt-ecran: 358 / 700;          /* ratio EXACT des captures (px source) */
}

/* ── BARRE ─────────────────────────────────────────────────────────────────
   Fond translucide + flou : la barre laisse passer la page qui défile
   dessous, au lieu de la masquer d'un bandeau opaque. Le `!important` n'est
   pas un ajout gratuit — il RÉPOND à `nav#landingNav{background:var(--bg)
   !important}` (l.4085), qu'aucune spécificité ne peut battre. Deux
   déclarations : la première est le repli des moteurs sans color-mix. */
#landing #landingNav {
  background: var(--bg) !important;
  gap: 12px;
}

/* ⚠️ LA TRANSLUCIDITÉ EST SOUS @supports, ET LE « REPLI PAR DEUX DÉCLARATIONS »
   NE MARCHAIT PAS ICI. Ce bloc était écrit ainsi :
       background: var(--bg) !important;                              (repli)
       background: color-mix(in srgb, var(--bg) 72%, transparent) !important;
   Le motif « repli d'abord, valeur moderne ensuite » suppose que la seconde
   déclaration soit rejetée AU PARSING. Or elle contient var() : le moteur
   l'accepte toujours à l'analyse et écarte donc la première par la cascade ;
   l'échec ne survient qu'à la substitution, où la propriété devient « invalid
   at computed-value time » → unset → background-color: transparent.
   Mesuré : la barre fixe de 60 px perdait TOUT fond, et la page défilait
   visiblement dessous. @supports, lui, teste avant de servir.
   Le test porte AUSSI sur backdrop-filter : une barre translucide SANS flou
   serait pire qu'une barre opaque — on ne rend transparent que si l'on sait
   flouter derrière. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px)))
      and (background: color-mix(in srgb, red 50%, transparent)) {
  #landing #landingNav {
    background: color-mix(in srgb, var(--bg) 72%, transparent) !important;
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
  }
}

.vt-brand {
  display: flex;
  align-items: center;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  min-width: 0;
}
.vt-brand img {
  height: 21px;
  width: auto;
  display: block;
}
.vt-nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.vt-login {
  font-family: var(--vt-sf);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text);
  background: none;
  border: 0;
  padding: 8px 4px;
  cursor: pointer;
  white-space: nowrap;
  transition: color .2s var(--ease);
}
.vt-login:hover { color: var(--orange); }

/* ── HERO ──────────────────────────────────────────────────────────────── */
.vt-hero {
  padding: 116px 24px 0;
  max-width: 780px;
  margin: 0 auto;
  text-align: center;
}
.vt-title {
  font-family: var(--vt-sf);
  font-size: clamp(40px, 11vw, 78px);
  font-weight: 600;               /* Apple ne compose pas ses titres en 800 */
  line-height: 1.03;
  letter-spacing: -.038em;
  color: var(--text);
}
.vt-lede {
  font-family: var(--vt-sf);
  font-size: clamp(17px, 4.2vw, 21px);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: -.01em;
  color: var(--text2);
  max-width: 33ch;
  margin: 20px auto 0;
}

/* Le CTA : la seule surface pleine de la page. */
.vt-cta {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-top: 34px;
  padding: 13px 14px 13px 26px;
  border-radius: 999px;
  background: var(--orange);
  color: #fff;
  font-family: var(--vt-sf);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.012em;
  text-decoration: none;
  box-shadow: 0 10px 30px rgba(255, 107, 43, .30);
  transition: background .22s var(--ease), box-shadow .22s var(--ease);
}
.vt-cta:hover {
  background: var(--orange2);
  box-shadow: 0 14px 38px rgba(255, 107, 43, .40);
}
.vt-cta:active { box-shadow: 0 6px 18px rgba(255, 107, 43, .28); }
.vt-cta:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }

/* La pastille d'XP parle la langue du produit : Inter, comme les compteurs
   du jeu. Sa valeur est relue en base — jamais un nombre figé (cf. le
   « +50 XP » resté en dur dans l'en-tête de quete.html). */
.vt-cta-xp {
  font-family: 'Inter', sans-serif;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .02em;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .20);
  white-space: nowrap;
}

/* ── LE PARCOURS ───────────────────────────────────────────────────────────
   `max-width:none` et les paddings à zéro annulent le `section{max-width:
   1200px;margin:0 auto}` de la l.1077 : la piste doit pouvoir filer jusqu'aux
   deux bords, sinon le geste de défilement n'a pas l'air possible. */
.vt-parcours {
  position: relative;
  max-width: none;
  margin: 52px 0 0;
  padding: 0 0 76px;
  /* La lueur déborde VOLONTAIREMENT (135vw), et un élément absolu qui dépasse
     élargit le document : mesuré à 390 px de large, scrollWidth passait à 451 px
     et la PAGE défilait latéralement. `clip` et non `hidden` : hidden ferait de
     cette section un conteneur de défilement, alors qu'on veut seulement cesser
     de peindre au-delà du bord. La piste, elle, garde son propre overflow-x. */
  overflow-x: clip;
}

/* Une seule lueur sur toute la page, et elle est PLACÉE : elle sort de
   derrière les écrans, comme la lumière qu'ils émettent. L'ancien hero en
   avait deux, dont une violette, posées sans motif. */
.vt-lueur {
  position: absolute;
  left: 50%;
  top: 8px;
  width: min(820px, 135vw);
  height: 400px;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center, rgba(255, 107, 43, .17) 0%, transparent 68%);
  pointer-events: none;
}
[data-theme="light"] .vt-lueur {
  background: radial-gradient(ellipse at center, rgba(255, 107, 43, .13) 0%, transparent 68%);
}

.vt-eyebrow {
  position: relative;
  font-family: 'Inter', sans-serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text3);
  text-align: center;
  margin: 0 0 24px;
}

/* La piste. Le padding latéral en calc() centre la PREMIÈRE et la DERNIÈRE
   dalle : sans lui, elles se collent au bord et le rail paraît tronqué. */
.vt-strip {
  position: relative;
  display: flex;
  gap: 18px;
  list-style: none;
  margin: 0;
  padding: 4px calc(50% - var(--vt-dalle) / 2);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.vt-strip::-webkit-scrollbar { display: none; }
.vt-slide {
  flex: 0 0 var(--vt-dalle);
  scroll-snap-align: center;
}

/* Le châssis. Il suit le thème ; la dalle, non — voir le commentaire du
   markup : le tunnel est une scène sombre écrite en dur. */
.vt-phone {
  padding: 7px;
  border-radius: 34px;
  background: var(--bg2);
  border: 1px solid var(--border2);
  box-shadow: 0 26px 56px rgba(0, 0, 0, .38), 0 2px 6px rgba(0, 0, 0, .22);
}
[data-theme="light"] .vt-phone {
  box-shadow: 0 22px 48px rgba(14, 14, 20, .16), 0 2px 6px rgba(14, 14, 20, .07);
}
/* La dalle. width/height sont sur la balise (716×1400) : le ratio est réservé
   avant même que l'image n'arrive, donc aucun décalage de mise en page. Le fond
   #0C0C12 est la teinte réelle du bas du dégradé du tunnel — c'est ce que voit
   l'œil pendant qu'une dalle paresseuse descend, et ça ressemble à un écran
   éteint plutôt qu'à un trou blanc. */
.vt-screen {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--vt-ecran);
  object-fit: cover;
  object-position: top center;
  border-radius: 27px;
  background-color: #0C0C12;
}

.vt-caption {
  font-family: var(--vt-sf);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text2);
  text-align: center;
  margin: 16px 0 0;
}

/* Le rail reprend le vocabulaire des barres d'étape du jeu (.ls-seg l.1013 :
   5 px de haut, rayon 3, piste --bg3, accompli --orange). Ce n'est pas une
   citation décorative : le parcours EST une suite ordonnée d'étapes, et le
   visiteur retrouvera cette barre dans le tunnel. Les segments sont des
   boutons — une zone à défilement horizontal ne s'atteint pas au clavier. */
.vt-steps {
  position: relative;
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-top: 28px;
}
.vt-step {
  width: 26px;
  height: 5px;
  padding: 0;
  border: 0;
  border-radius: 3px;
  background: var(--bg3);
  cursor: pointer;
  transition: background .25s var(--ease), width .25s var(--ease);
}
.vt-step:hover { background: var(--border2); }
.vt-step[aria-current="true"] { background: var(--orange); width: 38px; }
.vt-step:focus-visible { outline: 2px solid var(--text); outline-offset: 4px; }

/* ── ENTRÉE EN SCÈNE ───────────────────────────────────────────────────────
   `backwards` et NON `both` : avec `both`, la dernière image du keyframe
   resterait appliquée et écraserait le :hover du CTA — un bouton qui ne
   réagit plus, pour une raison invisible dans la règle de survol. */
@media (prefers-reduced-motion: no-preference) {
  .vt-title, .vt-lede, .vt-cta { animation: vtEntree .75s var(--ease) backwards; }
  .vt-lede { animation-delay: .08s; }
  .vt-cta  { animation-delay: .16s; }
}
@keyframes vtEntree {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ── RESPONSIVE ────────────────────────────────────────────────────────────
   Deux contraintes distinctes : le mot-symbole fait 206×24 à taille native et
   doit tenir à côté de la bascule (52 px) et de « Connexion » ; et la dalle
   grandit avec la place disponible. */
@media (min-width: 700px) {
  #landing { --vt-dalle: 262px; }
  .vt-hero { padding: 148px 32px 0; }
  .vt-parcours { margin-top: 64px; }
}
@media (min-width: 1024px) {
  #landing { --vt-dalle: 292px; }
  .vt-hero { padding: 168px 40px 0; }
  .vt-strip { gap: 26px; }
  .vt-brand img { height: 24px; }
}
@media (max-width: 400px) {
  .vt-brand img { height: 17px; }
  .vt-hero { padding: 104px 20px 0; }
  .vt-cta { font-size: 16px; padding: 12px 12px 12px 22px; }
}
@media (max-width: 340px) {
  .vt-brand img { height: 14px; }
  #landing #landingNav { gap: 8px; }
  #landing { --vt-dalle: 214px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   BAS DE VITRINE — Temps 2
   ═══════════════════════════════════════════════════════════════════════════
   Le haut (Temps 1) avait posé une langue : pile SF pour le discours, Inter
   pour les données du produit, orange de marque, un seul accent par écran. Le
   bas la reprend, au lieu des trois vocabulaires qui coexistaient ici (.how-v2
   de 2026, .how sa jumelle jamais retirée, .qt-* homonyme des classes du
   tunnel — une collision de préfixe qui n'attendait qu'un `grep` malheureux).

   Tout est centré SAUF un bloc : #annonceurs. C'est délibéré et c'est le seul
   endroit où ce lot prend un risque de forme — voir son commentaire.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Ossature commune ──────────────────────────────────────────────────── */
.vt-sec {
  position: relative;
  max-width: 1100px;            /* CLAUDE.md §4 — contenu centré */
  margin: 0 auto;
  padding: 92px 24px 0;
  text-align: center;
}
.vt-sec-titre {
  font-family: var(--vt-sf);
  font-size: clamp(29px, 6.2vw, 46px);
  font-weight: 600;
  line-height: 1.09;
  letter-spacing: -.032em;
  color: var(--text);
  margin: 0;
  text-wrap: balance;
}
.vt-sec-lede {
  font-family: var(--vt-sf);
  font-size: clamp(16px, 3.8vw, 19px);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -.01em;
  color: var(--text2);
  max-width: 42ch;
  margin: 16px auto 0;
  text-wrap: pretty;
}
.vt-sec-lede--large { max-width: 56ch; }

/* L'eyebrow du Temps 1 est centré. Le bloc annonceurs est ferré à gauche :
   il lui faut la même étiquette, alignée sur son propre texte. */
.vt-eyebrow--gauche { text-align: left; }

/* ── 1 · Comment ça marche ─────────────────────────────────────────────────
   Trois temps, sans carte ni cadre : ce sont des étapes qu'on lit, pas des
   objets qu'on compare. Le numéro porte l'orange — c'est le seul accent de la
   section, et il marque une SUITE (le seul endroit de la page qui en soit une). */
.vt-etapes {
  display: grid;
  grid-template-columns: 1fr;
  gap: 44px;
  list-style: none;
  margin: 52px 0 0;
  padding: 0;
  text-align: left;
}
.vt-etape { position: relative; padding-top: 18px; }
/* Filet fin au-dessus de chaque temps : il découpe la suite sans l'encadrer. */
.vt-etape::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 38px;
  height: 2px;
  border-radius: 2px;
  background: var(--orange);
}
.vt-etape-num {
  display: block;
  font-family: 'Inter', sans-serif;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .16em;
  color: var(--orange);
  margin-bottom: 10px;
}
.vt-etape-titre {
  font-family: var(--vt-sf);
  font-size: clamp(20px, 4.6vw, 24px);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -.022em;
  color: var(--text);
  margin: 0;
}
.vt-etape-desc {
  font-family: var(--vt-sf);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text2);
  margin: 8px 0 0;
}

/* ── 2 · Types de quêtes ───────────────────────────────────────────────────
   Ceux-là SONT des objets qu'on compare : ils prennent la carte. Trois, pas
   cinq — les deux formats retirés n'existaient pas dans le produit. */
.vt-formats {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
  list-style: none;
  margin: 44px 0 0;
  padding: 0;
  text-align: left;
}
.vt-format {
  padding: 24px 22px;
  border-radius: 20px;
  background: var(--bg2);
  border: 1px solid var(--border);
  transition: border-color .25s var(--ease), transform .25s var(--ease);
}
@media (prefers-reduced-motion: no-preference) {
  .vt-format:hover { border-color: var(--border2); transform: translateY(-3px); }
}
.vt-format-icone {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 13px;
  margin-bottom: 16px;
}
.vt-format-icone svg { width: 21px; height: 21px; }
/* Les teintes sont celles des tokens, à 12 % — jamais une couleur inventée. */
.vt-format-icone--orange { background: rgba(255, 107, 43, .12); color: var(--orange); }
.vt-format-icone--vert   { background: rgba(34, 197, 94, .12);  color: var(--green); }
.vt-format-icone--violet { background: rgba(124, 58, 237, .14); color: var(--violet); }
.vt-format-nom {
  font-family: var(--vt-sf);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--text);
  margin: 0;
}
.vt-format-desc {
  font-family: var(--vt-sf);
  font-size: 15px;
  line-height: 1.5;
  color: var(--text2);
  margin: 6px 0 0;
}

/* ── 3 · Choisis ton défi ──────────────────────────────────────────────────
   La section garde sa mécanique ; seuls l'espacement des blocs peuplés en JS
   est repris ici, pour qu'ils respirent sous le nouveau titre. */
#landingQuests #landingDemoFeatured:not(:empty) { margin-top: 44px; }
#landingQuests #landingLockedGrid:not(:empty)   { margin-top: 18px; }
/* Les cartes sont injectées par UI_buildQuestCard / UI_buildCampaignCard, qui
   les dessinent ferrées à gauche — comme partout ailleurs sur le site. Le
   `text-align:center` de .vt-sec, lui, se transmet par HÉRITAGE et centrait
   titre, pastille et pied de la carte : la même carte n'avait pas la même tête
   sur la vitrine et sur campagne.html. On rend la main aux composants.
   `start` et non `left` : c'est la valeur que calcule la carte de référence. */
#landingQuests #landingDemoFeatured,
#landingQuests #landingLockedGrid { text-align: start; }

/* ── 4 · Le jeu ────────────────────────────────────────────────────────────
   Un panneau, deux colonnes en bureau : ce qu'on gagne à gauche, la flamme à
   droite. L'ancienne section « Séries quotidiennes » occupait un écran entier
   pour une maquette figée ; elle tient ici dans une ligne et une décoration. */
.vt-jeu {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  align-items: center;
  margin-top: 44px;
  padding: 30px 24px;
  border-radius: 24px;
  background: var(--bg2);
  border: 1px solid var(--border);
  text-align: left;
}
.vt-jeu-liste { list-style: none; margin: 0; padding: 0; display: grid; gap: 15px; }
.vt-jeu-item { display: flex; align-items: flex-start; gap: 13px; }
.vt-jeu-puce {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin-top: 9px;
  border-radius: 50%;
  background: var(--orange);
}
.vt-jeu-texte {
  font-family: var(--vt-sf);
  font-size: 16px;
  line-height: 1.45;
  color: var(--text2);
}
.vt-jeu-texte strong { color: var(--text); font-weight: 600; }

/* La flamme : DÉCORATION (aria-hidden côté markup). Sept jours qui illustrent
   la mécanique — la page ne prétend plus afficher une série réelle. */
.vt-flamme {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 22px 16px;
  border-radius: 18px;
  background: var(--bg3);
}
.vt-flamme-icone {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 62%, var(--orange) 0%, rgba(255, 107, 43, .18) 70%, transparent 72%);
}
.vt-flamme-jours { display: flex; gap: 7px; }
.vt-jour {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 27px;
  height: 27px;
  border-radius: 50%;
  font-family: 'Inter', sans-serif;
  font-size: 11px;
  font-weight: 700;
  color: var(--text3);
  background: var(--bg2);
  border: 1px solid var(--border);
}
.vt-jour.is-fait { background: var(--orange); border-color: var(--orange); color: #fff; }
.vt-jour.is-auj  { border-color: var(--orange); color: var(--orange); }

/* ── 6 · Commerçants & annonceurs ──────────────────────────────────────────
   LE bloc signature, et le seul écart de forme assumé du lot. Toute la page
   est centrée et tutoie ; celui-ci est ferré à gauche et vouvoie. Ce n'est pas
   une fantaisie : il ne s'adresse pas au même lecteur. Un commerçant qui fait
   défiler doit voir, sans lire, que ce passage lui revient. L'arête orange et
   l'alignement font ce travail — le reste de la section reste sobre. */
.vt-b2b {
  position: relative;
  margin-top: 8px;
  padding: 34px 26px 36px;
  border-radius: 24px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--orange);
  text-align: left;
}
.vt-b2b-titre { font-size: clamp(25px, 5.4vw, 38px); margin-top: 2px; }
.vt-b2b-lede  { margin: 14px 0 0; max-width: 48ch; }
.vt-b2b-liste {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  display: grid;
  gap: 12px;
}
.vt-b2b-item {
  position: relative;
  padding-left: 27px;
  font-family: var(--vt-sf);
  font-size: 16px;
  line-height: 1.45;
  color: var(--text2);
}
/* Coche dessinée en CSS : deux traits, aucun glyphe à charger, aucune image. */
.vt-b2b-item::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 6px;
  width: 6px;
  height: 11px;
  border-right: 2px solid var(--orange);
  border-bottom: 2px solid var(--orange);
  transform: rotate(42deg);
}
.vt-cta--b2b { margin-top: 30px; padding: 13px 26px; }

/* ── 7 · Clôture ───────────────────────────────────────────────────────── */
.vt-cloture { padding-bottom: 96px; }
.vt-cloture .vt-cta { margin-top: 30px; padding: 13px 26px; }

/* ── 8 · Pied de page ──────────────────────────────────────────────────────
   Le mot-symbole était un <div onclick> portant un style inline ; il devient
   un <button>, comme celui du haut de page (même raison qu'au Temps 1 : un
   <div> nu n'a pas de rôle qui porte un nom accessible). */
.vt-pied-logo {
  display: block;
  margin: 0 auto 16px;
  padding: 4px;
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
}
.vt-pied-logo img { height: 22px; width: auto; display: block; }
.vt-pied-logo:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; border-radius: 6px; }
.vt-pied-b2b { margin: 12px 0; }
.vt-pied-lien {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 700;
  color: var(--orange);
  text-decoration: none;
}
.vt-pied-lien:hover { text-decoration: underline; }

/* ── Respiration et colonnes selon la place ────────────────────────────── */
/* Le panneau annonceurs est ferré à gauche : sur un écran large, sa liste
   tenait sur une colonne et laissait la moitié droite du panneau vide, ce qui
   le faisait paraître inachevé plutôt que délibéré. Deux colonnes occupent la
   largeur sans toucher au reste de la composition. */
@media (min-width: 900px) {
  .vt-b2b-liste { grid-template-columns: 1fr 1fr; column-gap: 38px; }
}

@media (min-width: 700px) {
  .vt-sec        { padding: 116px 32px 0; }
  .vt-etapes     { grid-template-columns: repeat(3, 1fr); gap: 34px; margin-top: 60px; }
  .vt-formats    { grid-template-columns: repeat(3, 1fr); }
  .vt-jeu        { grid-template-columns: 1fr auto; gap: 40px; padding: 36px 34px; }
  .vt-b2b        { padding: 44px 40px 46px; }
  .vt-cloture    { padding-bottom: 116px; }
}
@media (min-width: 1024px) {
  .vt-sec     { padding: 132px 40px 0; }
  .vt-jeu     { padding: 44px 48px; }
  .vt-b2b     { padding: 52px 56px 54px; }
  .vt-cloture { padding-bottom: 132px; }
}
@media (max-width: 400px) {
  .vt-sec     { padding: 78px 20px 0; }
  .vt-format  { padding: 20px 18px; }
  .vt-jeu     { padding: 24px 18px; }
  .vt-b2b     { padding: 28px 20px 30px; }
  .vt-flamme-jours { gap: 5px; }
  .vt-jour    { width: 25px; height: 25px; }
}

/* Thème clair : seules les ombres portées, écrites en noir, ont besoin d'être
   rejouées — tout le reste passe par les tokens et suit déjà data-theme. */
[data-theme="light"] .vt-format:hover { box-shadow: 0 12px 30px rgba(14, 14, 20, .09); }

/* ═══════════════════════════════════════════════════════════════════════════
   RÉVÉLATION MOBILE — profil.html · parametres.html · classement.html
   ───────────────────────────────────────────────────────────────────────────
   Ces trois pages logent 100 % de leur contenu dans .dt-main. Or le cadre
   masque .dt-main sous 769px, DEUX FOIS et en `!important` : une fois sans
   portée (.dt-sidebar/.dt-main/.dt-topbar) et une fois en
   `#app.logged-in-layout .dt-main`, de spécificité 1-2-0. Résultat mesuré à
   375px avant ce bloc : .dt-main en display:none, boîte 0×0 — l'en-tête et la
   barre du bas s'affichaient, et entre les deux, le noir.

   ⚠️ ON NE TOUCHE PAS AUX RÈGLES DE MASQUAGE. Huit pages en dépendent, dont
   quete.html (§9). On passe DEVANT, comme les quatre crochets déjà en place
   (index `#dtCampFilterBlock`, portefeuille `.wal-body`, campagne
   `#campaignQuestsTitle`, bientôt `.qw-soon-body`) : `:has(#id)` porte la
   spécificité à 2-2-0, ce qui suffit à dépasser 1-2-0. Deux `!important` de
   même origine se départagent à la spécificité — c'est tout le mécanisme.

   ⚠️ LES ID SONT DES CROCHETS, PAS DES NOMS DE STYLE. Ils n'existent que pour
   ce bloc et ne servent à rien d'autre. Les renommer sans toucher ici revide
   la page sur mobile, sans erreur ni test rouge. Le commentaire posé à côté de
   chaque id dans le HTML dit la même chose, pour qu'on le lise des deux côtés.

   ⚠️ `!important` SUR LES GRILLES : classement.html écrit sa grille en style
   INLINE sur .dt-body et .dt-center (3fr 1fr). Un style inline l'emporte sur
   toute règle de feuille — sauf `!important`. Même contrainte que le crochet
   d'index, déjà documentée là-bas.

   Tout ce bloc vit sous 768px : le bureau (≥769px, et la bande 769-1023 comme
   le plein écran) n'est pas concerné, par construction.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {

  /* 1 — La révélation elle-même. */
  #app.logged-in-layout .dt-main:has(#prfMobileRoot),
  #app.logged-in-layout .dt-main:has(#setMobileRoot),
  #app.logged-in-layout .dt-main:has(#lbMobileRoot) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
    min-height: 0;
  }

  /* 2 — Une seule colonne, des marges de téléphone, et de quoi passer sous la
     barre du bas. `#app` porte déjà 64px de dégagement : on ajoute 28px de
     respiration, pas une seconde fois la hauteur de la barre. */
  .dt-main:has(#prfMobileRoot) .dt-body,
  .dt-main:has(#setMobileRoot) .dt-body,
  .dt-main:has(#lbMobileRoot)  .dt-body {
    display: block !important;
    grid-template-columns: 1fr !important;
    gap: 0 !important;
    padding: 14px 14px 28px !important;
    max-width: none; width: 100%; margin: 0; box-sizing: border-box;
  }
  .dt-main:has(#prfMobileRoot) .dt-center,
  .dt-main:has(#setMobileRoot) .dt-center,
  .dt-main:has(#lbMobileRoot)  .dt-center {
    display: flex !important;
    flex-direction: column;
    gap: 14px !important;
    min-width: 0;
    padding-bottom: 0 !important;
  }
  /* La colonne de droite est un complément de bureau : sur mobile elle
     doublerait la longueur de la page pour du secondaire. */
  .dt-main:has(#prfMobileRoot) .dt-right,
  .dt-main:has(#setMobileRoot) .dt-right,
  .dt-main:has(#lbMobileRoot)  .dt-right { display: none !important; }

  /* 3 — PROFIL : l'en-tête s'empile, le bouton d'action prend la largeur. */
  .dt-main:has(#prfMobileRoot) .prf-header {
    flex-direction: column; align-items: stretch; gap: 16px; padding: 18px;
  }
  .dt-main:has(#prfMobileRoot) .prf-avatar-zone { gap: 14px; }
  .dt-main:has(#prfMobileRoot) .prf-edit-btn { width: 100%; justify-content: center; }
  .dt-main:has(#prfMobileRoot) .prf-xp-wrap { max-width: none; }
  .dt-main:has(#prfMobileRoot) .prf-stat-card { padding: 14px; }

  /* 4 — CLASSEMENT : le titre respire moins large, les cartes suivent. */
  .dt-main:has(#lbMobileRoot) .lb-title { font-size: 24px; }
  /* ⚠️ `1fr` NE SUFFIT PAS À FAIRE RÉTRÉCIR UNE GRILLE. La page passe déjà en
     une colonne sous 900px, mais `1fr` vaut `minmax(auto, 1fr)` : le minimum
     reste le min-content de la piste. Mesuré à 320px, `.tu-section` réclamait
     330px, la piste s'y calait, et toute la page débordait de 24px pour un seul
     enfant. `minmax(0, 1fr)` retire ce plancher ; `min-width:0` fait le même
     travail sur les enfants, qui sont aussi des éléments de grille. */
  .dt-main:has(#lbMobileRoot) .lb { grid-template-columns: minmax(0, 1fr) !important; }
  .dt-main:has(#lbMobileRoot) .lb > * { min-width: 0; }
  .dt-main:has(#lbMobileRoot) .lg-ladder { padding: 18px 14px; gap: 18px; }
  /* L'escalier tient dans 347px, mais tout juste : on resserre les libellés
     plutôt que de laisser « Vous êtes ici » pousser la marche courante. */
  .dt-main:has(#lbMobileRoot) .lg-step-name { font-size: 10px; }
  .dt-main:has(#lbMobileRoot) .lg-step-tag  { font-size: 9px; line-height: 1.15; letter-spacing: 0; }
  .dt-main:has(#lbMobileRoot) .lg-ladder-top { gap: 10px; }
  .dt-main:has(#lbMobileRoot) .lg-ladder-season { font-size: 11px; text-align: right; }

  /* 5 — PARAMÈTRES : l'en-tête d'identité s'empile pour de bon.
     ⚠️ `width:100%` NE FAIT PAS PASSER UN ÉLÉMENT À LA LIGNE dans un conteneur
     flex : la règle de la page le posait bien, mais l'élément restait un item
     rétrécissable sur la MÊME ligne. Mesuré à 375px : le bouton gardait 147px,
     et `.set-hero-info` — qui n'a ni `flex` ni `min-width:0` — tombait à 23px.
     Le pseudo s'y coupait en plein milieu, et le bouton semblait le chevaucher.
     C'est `flex-basis:100%` qui force le retour à la ligne, pas la largeur. */
  .dt-main:has(#setMobileRoot) .set-hero { align-items: center; gap: 12px; }
  .dt-main:has(#setMobileRoot) .set-hero-info { flex: 1 1 100%; min-width: 0; }
  .dt-main:has(#setMobileRoot) .set-hero-pseudo,
  .dt-main:has(#setMobileRoot) .set-hero-name { overflow-wrap: anywhere; }
  .dt-main:has(#setMobileRoot) .set-change-photo { flex: 0 0 100%; margin-top: 4px; }
  /* Cibles tactiles : les rangées de formulaire respirent et le bouton
     d'enregistrement fait la largeur, comme le prévoit déjà la page. */
  .dt-main:has(#setMobileRoot) .set-field { padding: 14px; gap: 8px; }
  .dt-main:has(#setMobileRoot) .set-save-btn { min-height: 42px; }
  .dt-main:has(#setMobileRoot) .set-section-title { padding-left: 14px; padding-right: 14px; }
}

/* Les libellés de tuile KPI tiennent sur UNE ligne jusqu'en bas de gamme.
   « SÉRIE ACTUELLE » débordait à 320px : mesuré, il lui manquait quelques
   pixels — d'où la réduction, appliquée seulement là où elle est nécessaire. */
@media (max-width: 360px) {
  .dt-main:has(#prfMobileRoot) .prf-stat-label { font-size: 10px; letter-spacing: .2px; }
  .dt-main:has(#prfMobileRoot) .prf-stat-val   { font-size: 20px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   HERO CAMPAGNE — version premium (pages de CAMPAGNE uniquement)
   ───────────────────────────────────────────────────────────────────────────
   Le logo de la marque, NET, sur la couleur de la campagne. Remplace la
   bannière assombrie du Lot 2, qui passait toute image sous
   `brightness(.42) saturate(.72)` : sur une marque dont le visuel EST son
   logo, on n'obtenait qu'un rectangle terne.

   ⚠️ SCOPÉ PAR `.campaign-hero--campaign`, posée par renderHero quand le slug
   désigne une CAMPAGNE. campagne.html sert AUSSI les pages de MARCHAND, qui
   partagent ce markup mais pas les données : pas de pastille d'état
   (merchants.start_date est du texte libre que Date() ne lit pas), participants
   souvent à zéro, titre qui retombe sur le nom. Sans la classe, elles gardent
   le hero d'avant, inchangé. Même patron que le crochet :has(#campaignQuestsTitle)
   plus haut dans ce fichier.

   ⚠️ Les frères (#campaignPeriod, .campaign-info-row) sont atteints par `~` :
   ils sont hors de .campaign-hero dans le DOM, la classe ne peut pas les
   couvrir en descendance.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1 — Le fond : la teinte de campagne, montée en intensité, plus une lueur
   derrière le logo. `--hero-tint` est déjà résolue plus haut (accent ou repli
   neutre #222226), on ne refait pas ce travail. */
.campaign-hero--campaign {
  min-height: 316px;
  padding: 20px 26px 24px;
  background-image:
    radial-gradient(108% 128% at 84% 22%,
      color-mix(in srgb, var(--hero-tint) 70%, transparent) 0%,
      color-mix(in srgb, var(--hero-tint) 24%, transparent) 44%,
      transparent 68%),
    linear-gradient(108deg,
      color-mix(in srgb, var(--hero-tint) 54%, #06060B) 0%,
      color-mix(in srgb, var(--hero-tint) 90%, #000000) 30%,
      color-mix(in srgb, var(--hero-tint) 46%, #0B0B11) 62%,
      #0B0B11 100%);
}

/* 2 — Plus de bannière assombrie. `content:none` retire le pseudo-élément
   ENTIER : ni image, ni masque, ni filtre à neutraliser un par un. Le
   `--header-bg-img` n'est de toute façon plus posé par le JS sur ce chemin. */
.campaign-hero--campaign::before { content: none; }

/* 3 — LE LOGO, NET. C'est le sujet du hero : il monte à 148px et reçoit une
   ombre portée qui le décolle du fond. Aucun filtre d'assombrissement. */
/* ⚠️ `flex:1` SUR LE CORPS. Le hero est une colonne flex de 316px de haut ;
   sans cela le corps gardait sa hauteur naturelle et laissait 103px de vide
   mesuré sous le titre — un hero qui a l'air inachevé. Il occupe désormais
   l'espace libre et centre son contenu. */
.campaign-hero--campaign .campaign-hero-body {
  flex: 1;
  align-items: center;
}
.campaign-hero--campaign .campaign-hero-right { align-items: center; }
/* ⚠️ UNE TUILE, PAS UNE IMAGE NUE. Les logos passent par le profil « contain
   400×400 » du pipeline (§14) et arrivent très souvent sur FOND BLANC opaque —
   posés tels quels sur la couleur de campagne, ils donnaient un rectangle blanc
   à angles vifs. Le coin arrondi, le liseré et l'ombre en font un objet voulu.
   Un logo à fond transparent y gagne aussi : la tuile lui sert de socle clair,
   comme la pièce du carrousel. */
.campaign-hero--campaign .hero-logo-overlay {
  max-height: 152px;
  max-width: 232px;
  width: auto;
  padding: 14px;
  box-sizing: border-box;
  background: #FFFFFF;
  border-radius: 20px;
  box-shadow:
    0 18px 40px rgba(0, 0, 0, .42),
    0 0 0 1px rgba(255, 255, 255, .10);
  filter: none;   /* l'ombre est portée par la tuile, pas par le glyphe */
  object-fit: contain;
}

/* 4 — LE TITRE, ET SA PROPRE PROTECTION.
   ⚠️ L'assombrissement retiré au point 2 servait AUSSI à garder le titre
   lisible sur les visuels clairs. On ne le supprime donc pas sans rien : le
   texte reçoit son propre voile, local et doux, qui suit sa colonne au lieu
   d'éteindre toute l'image. Un dégradé de FOND sur la colonne — pas un
   pseudo-élément en z-index négatif, qui se battrait avec le `z-index:2` que
   `.campaign-hero > *` pose déjà. */
/* La protection du titre. ⚠️ Elle doit se sentir sans SE VOIR — et deux
   tentatives ont échoué là-dessus avant celle-ci : un dégradé RADIAL posé sur
   la colonne dessinait une tache derrière un titre court (« Kyu ») ; le même
   en LINÉAIRE, mesuré à l'écran, restait une bande horizontale nettement
   lisible. Le défaut n'était pas l'opacité, c'était le PRINCIPE : tout fond
   posé sur `.campaign-hero-left` s'arrête au bord de la colonne, et un bord se
   voit.
   La protection est donc portée par le FOND DU HERO lui-même (point 1) : son
   dégradé 108deg part d'un coin haut-gauche assombri (`tint 54%, #06060B`) et
   remonte à la teinte pleine au stop 30 %. Elle couvre toute la hauteur, n'a
   aucun bord propre, et vaut pour N'IMPORTE QUELLE teinte — `color_theme` est
   une donnée libre, elle peut être claire. Le `text-shadow` du titre en est le
   second filet. */
.campaign-hero--campaign .campaign-hero-left {
  padding: 6px 26px 8px 0;
}
.campaign-hero--campaign .campaign-hero-title {
  font-size: 40px;
  line-height: 1.06;
  letter-spacing: -1.1px;
  color: #fff;
  text-shadow: 0 2px 20px rgba(0, 0, 0, .5);
}
/* ⚠️ La `color` a été RETIRÉE d'ici. Elle valait rgba(255,255,255,.62) et,
   plus spécifique (0-2-0 contre 0-1-0), elle aurait battu le blanc à .92 du
   voile ci-dessus : le hero premium serait resté avec l'ancien contraste. Seul
   l'interlettrage lui reste propre. */
.campaign-hero--campaign .campaign-hero-label {
  letter-spacing: 1.4px;
}
/* Sans description (15 campagnes sur 17), le titre doit tenir seul : c'est le
   cas nominal, pas l'exception. Avec, la ligne reste discrète sous lui. */
.campaign-hero--campaign .campaign-hero-sub {
  color: rgba(255, 255, 255, .70);
  font-size: 14px;
  max-width: 46ch;
}

/* 5 — LES BOUTONS. Plus légers : un seul trait, un fond à peine posé, la même
   hauteur pour les trois. Le bouton « Règles » cesse d'être une pilule pleine
   qui tirait l'œil avant le titre. */
.campaign-hero--campaign .campaign-hero-actions .ob-btn-blur,
.campaign-hero--campaign .campaign-hero-actions .campaign-btn-rules,
.campaign-hero--campaign .campaign-hero-actions .campaign-btn-share {
  height: 30px;
  padding: 0 13px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(12, 12, 18, .34);
  color: rgba(255, 255, 255, .92);
  font-size: 12px;
  font-weight: 600;
  box-shadow: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.campaign-hero--campaign .campaign-hero-actions .ob-btn-blur:hover,
.campaign-hero--campaign .campaign-hero-actions .campaign-btn-rules:hover,
.campaign-hero--campaign .campaign-hero-actions .campaign-btn-share:hover {
  background: rgba(255, 255, 255, .16);
  border-color: rgba(255, 255, 255, .28);
}

/* 6 — LES CARTES DE STATISTIQUES : trois cartes DÉTACHÉES du hero, dont la
   Période. Le bandeau pleine largeur d'origine se replie — son contenu a
   déménagé. */
.campaign-hero--campaign ~ .campaign-period { display: none !important; }
.campaign-info-block[hidden] { display: none; }   /* `display:flex` battrait [hidden] */

/* Rythme (bureau) : hero → cartes 20 · entre cartes 16 · cartes → titre 40.
   Le parent .dt-center sépare déjà ses enfants par un `gap` de 24px (style
   inline de campagne.html) et le titre porte son propre margin-top de 8px :
   les marges ci-dessous n'en sont que la DIFFÉRENCE, écrite en calc pour que
   la cible reste lisible.
   ⚠️ LA CAUSE DES CARTES COLLÉES : la règle de base `.campaign-info-row`
   impose `margin-top: -22px !important`, héritée du temps où les stats
   formaient une bande ACCROCHÉE au hero. 24 − 22 = 2px au bureau ; sur
   téléphone le gap tombe à 18px, d'où −4px : les cartes glissaient sous le
   hero (position:relative + z-index:1, il peint par-dessus) et leurs coins
   hauts disparaissaient. Les pages de MARCHAND s'appuient encore sur cette
   règle : on la contre ici, sous la classe de campagne, sans la toucher — et
   son `!important` impose le nôtre, sur cette seule propriété. */
.campaign-hero--campaign ~ .campaign-info-row {
  gap: 16px;
  margin-top: calc(20px - 24px) !important;
  margin-bottom: calc(40px - 24px - 8px);
}
/* La Période porte deux dates longues ET la pastille : elle reçoit plus de
   largeur. 1.8 est MESURÉ, pas choisi : la plus longue période réelle
   (« 20 juillet 2026 → 6 septembre 2026 » + « En cours ») demande 397px de
   carte. flex-grow ne partage que l'espace laissé APRÈS les 32px de padding
   de chaque carte : à 1280px, 1.6 donnait 399px — à 1px du pli — et 1.8 en
   donne 423.
   flex-grow plutôt qu'une grille 1fr 1fr 1.8fr : sans dates, le bloc reste
   [hidden] et les deux autres se partagent la rangée, sans colonne vide. */
.campaign-hero--campaign ~ .campaign-info-row #campaignPeriodBlock { flex-grow: 1.8; }
/* ⚠️ NI `background` NI `border` ICI. Un premier jet en posait — un dégradé
   teinté et un liseré — et les DEUX étaient du code mort, battus par un verrou
   `!important` sur `.campaign-info-block`. Ce verrou a été retiré le 2026-10-14
   (il figeait aussi la carte en sombre dans le thème CLAIR) ; le fond vit
   désormais dans la règle de base (sombre) et dans le bloc « THÈME CLAIR » plus
   bas. Le gris neutre reste voulu : il sépare mieux la bande du hero coloré. */
.campaign-hero--campaign ~ .campaign-info-row .campaign-info-block {
  border-radius: 14px;
  padding: 13px 16px;
}
.campaign-hero--campaign ~ .campaign-info-row .campaign-info-label {
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  /* --text2 et non --text3, dans les DEUX thèmes. Mesuré : --text3 donnait
     2,4:1 sur la carte sombre (#5A5A7A sur #222226) et 2,55:1 sur la carte
     claire (var(--bg3)). --text2 : 5,68:1 en sombre, 5,58:1 en clair. La
     discrétion vient de la taille (10px) et des capitales, pas du gris. */
  color: var(--text2);
}
.campaign-hero--campaign ~ .campaign-info-row .campaign-info-value {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}
/* La pastille d'état, à droite de la période. Mêmes couleurs que les cartes :
   c'est le même vocabulaire d'état dans toute l'application. */
.campaign-info-pill {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .12);
  color: var(--text2);
}
.campaign-info-pill[hidden] { display: none; }
.campaign-info-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* ⚠️ Noms RELEVÉS dans _ui_computeStatut (shared.js), pas devinés : la fonction
   renvoie ob-status-active / -soon / -inactive. Des classes inventées auraient
   donné une pastille grise, sans erreur ni test rouge. */
.campaign-info-pill.ob-status-active   { color: var(--green);  border-color: rgba(34,197,94,.34);   background: rgba(34,197,94,.10); }
.campaign-info-pill.ob-status-soon     { color: var(--yellow); border-color: rgba(245,158,11,.34);  background: rgba(245,158,11,.10); }
.campaign-info-pill.ob-status-inactive { color: var(--text2);  border-color: rgba(255,255,255,.12); background: rgba(255,255,255,.05); }
/* « Terminé » en --text2 : --text3 y donnait 2,08:1 en sombre (mesuré). */

/* 6b — THÈME CLAIR de la zone sous le hero — pages de CAMPAGNE et de MARCHAND.
   Les cartes deviennent des surfaces claires de la même famille que la boîte de
   progression juste au-dessus (.cp-progress : var(--bg3) + liseré var(--border)).
   Le liseré est un `inset box-shadow` et non une `border` : la carte garde sa
   géométrie au pixel près entre les deux thèmes.
   Libellés, icônes et pastille « Terminé » sont en --text2 dans les DEUX thèmes
   (règles de base) : il n'y a rien à surcharger ici pour eux. La valeur reste
   var(--text). */
[data-theme="light"] .campaign-info-block {
  background: var(--bg3);
  box-shadow: inset 0 0 0 1px var(--border);
}
/* Pastilles d'état sur fond clair. Vert et jaune purs y tombent sous 2:1 : même
   correctif que .cp-progress-check et .acv2-val-gm (teinte mêlée à var(--text)).
   « Terminé » : le blanc translucide du sombre disparaît sur fond clair — seuls
   son liseré et son fond changent, sa couleur est déjà --text2. */
[data-theme="light"] .campaign-info-pill.ob-status-active   { color: color-mix(in srgb, var(--green) 45%, var(--text)); }
[data-theme="light"] .campaign-info-pill.ob-status-soon     { color: color-mix(in srgb, var(--yellow) 45%, var(--text)); }
[data-theme="light"] .campaign-info-pill.ob-status-inactive { border-color: var(--border2); background: var(--bg2); }
/* Page de MARCHAND — « Partager » sur le HERO, qui reste sombre dans les deux
   thèmes. Il héritait de var(--text2), dont la valeur claire (#5A5A7A) tombe à
   2,2:1 sur ce fond. En clair, var(--text3) porte EXACTEMENT la valeur sombre de
   --text2 (#9898B8) : le bouton s'affiche donc à l'identique dans les deux
   thèmes. (0,2,0) : la règle premium du hero de campagne (0,3,0) garde la main. */
[data-theme="light"] .campaign-btn-share { color: var(--text3); }

/* 7 — TÉLÉPHONE. Le logo passe sous le titre, centré : à 360px il n'y a pas de
   moitié droite à lui donner. Les cartes passent sur DEUX colonnes —
   Participants et Quêtes côte à côte, la Période seule en dessous : trois
   cartes côte à côte y seraient illisibles. */
@media (max-width: 768px) {
  .campaign-hero--campaign {
    min-height: 0;
    padding: 16px 16px 20px;
  }
  .campaign-hero--campaign .campaign-hero-body {
    flex-direction: column;
    align-items: stretch;
    gap: 16px;
  }
  /* La rangée haute s'empile : le label sur sa ligne, les trois boutons sur la
     leur, alignés à GAUCHE. ⚠️ Mesuré à 390px : en `space-between` les boutons
     se replient à deux puis un, et « Partager » retombait seul à droite,
     décalé sous les deux autres — une rangée cassée, pas une rangée pliée.
     Empilés et alignés à gauche, les trois tiennent sur UNE ligne dès 360px
     (rangée mesurée à 280px). En dessous — 320px, mesuré — ils se replient,
     mais par la gauche et sans trou : un pli, pas une cassure. */
  .campaign-hero--campaign .campaign-hero-top {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .campaign-hero--campaign .campaign-hero-actions { justify-content: flex-start; }
  .campaign-hero--campaign .campaign-hero-left {
    padding: 0;
  }
  .campaign-hero--campaign .campaign-hero-title { font-size: 27px; letter-spacing: -.6px; }
  .campaign-hero--campaign .campaign-hero-right { justify-content: center; }
  .campaign-hero--campaign .hero-logo-overlay { max-height: 112px; max-width: 78%; }
  /* Rythme (téléphone) : hero → cartes 16 · entre cartes 12 · cartes → titre
     32. Même calcul qu'au bureau, sur le gap de 18px que .dt-center prend
     sous 768px (crochet :has(#campaignQuestsTitle)). minmax(0, 1fr) : deux
     colonnes ÉGALES, quel que soit le texte de chacune. */
  .campaign-hero--campaign ~ .campaign-info-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
    margin-top: calc(16px - 18px) !important;
    margin-bottom: calc(32px - 18px - 8px);
  }
  .campaign-hero--campaign ~ .campaign-info-row #campaignPeriodBlock { grid-column: 1 / -1; }
  /* Quand une carte ne loge plus tout sur une ligne, l'élément de FIN passe
     dessous au lieu de déborder ou de couper : le texte d'une demi-carte
     (compteur à quatre chiffres, « +X »), la pastille de la Période. C'est le
     contenu qui décide, pas une largeur d'écran.
     ⚠️ Pour la Période, c'est ce qui garde la date ENTIÈRE : sans ce pli, la
     pastille lui volait sa largeur et la coupure tombait au milieu d'une date
     (« 29 juin 2026 → 8 » / « septembre 2026 », mesuré à 360px).
     `text-wrap: balance` a été essayé : il choisit la même coupure — c'est la
     plus équilibrée. */
  .campaign-hero--campaign ~ .campaign-info-row .campaign-info-inner {
    flex-wrap: wrap;
    row-gap: 8px;
  }
}
/* Téléphone étroit : les deux demi-cartes deviennent des TUILES — pastille en
   haut, libellé et valeur dessous. Mesuré à 360px : une demi-carte offre
   128px de contenu, et « 4 Participants » avec ses quatre avatars en demande
   157. Côte à côte, il faudrait ramener la grappe de 54 à 25px — plus un
   rétrécissement, une disparition. Les DEUX cartes s'empilent, pas seulement
   celle qui déborde : côte à côte, deux dessins différents. */
@media (max-width: 430px) {
  .campaign-hero--campaign ~ .campaign-info-row .campaign-info-block:not(#campaignPeriodBlock) .campaign-info-inner {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* 8 — LIEN « ‹ ANNONCEUR » AU-DESSUS DU HERO. N'existe que sur une page de
   CAMPAGNE : _campPoserRetourAnnonceur (campagne.html) le crée — une page de
   marchand n'en a aucune trace dans son DOM. Même dessin que « ← Tous les
   annonceurs » d'annonceur.html (.anr-retour) : texte secondaire, plein au
   survol, anneau orange au clavier.
   Hauteur 44px : la cible tactile minimale. Le texte n'en occupe que 17, d'où
   la marge basse NÉGATIVE — elle reprend le gap de .dt-center (24px, 18px
   sous 768px) pour ne laisser que 6px entre la boîte et le hero : l'anneau de
   focus (2px, décalé de 2px) y tient sans passer sous le hero, qui peint
   par-dessus (position:relative + z-index:1). Le hero et tout ce qui le suit
   gardent leurs écarts : seul l'espace AU-DESSUS du hero change.
   Nom long : une ligne, coupé en « … » ; le chevron, lui, ne rétrécit pas. */
.campaign-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-height: 44px;
  margin-bottom: calc(6px - 24px);
  font-size: 14px;
  font-weight: 600;
  color: var(--text2);
  text-decoration: none;
}
.campaign-back:hover { color: var(--text); }
.campaign-back:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; border-radius: 4px; }
.campaign-back-chevron { flex-shrink: 0; font-size: 18px; line-height: 1; }
.campaign-back-nom { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 768px) {
  .campaign-back { margin-bottom: calc(6px - 18px); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   HERO MARCHAND — deux correctifs de MISE EN PAGE (pages de marchand seules)
   ───────────────────────────────────────────────────────────────────────────
   campagne.html sert deux pages : campagne (hero premium, bloc ci-dessus) et
   MARCHAND, qui a gardé l'ancien hero. Cet ancien hero porte deux défauts de
   mise en page, mesurés, vivants sur les 17 pages de marchand — les mêmes que
   le hero de campagne portait avant d'être repris. On les corrige ici, et RIEN
   d'autre : pas de tuile à logo net, pas de bande de statistiques, pas de
   dégradé d'accent, pas de pastille d'état. La page de marchand n'a pas les
   données qui les rendraient honnêtes (`merchants.start_date` est du texte
   libre, participants souvent à zéro, titre qui retombe sur le nom).

   ⚠️ SCOPÉ PAR `.campaign-hero--merchant`, posée par renderHero quand le slug
   NE désigne PAS une campagne. Le hero premium ne voit aucune de ces règles.
   ═══════════════════════════════════════════════════════════════════════════ */

/* A — LE VIDE SOUS LE TITRE — mais SEULEMENT SANS BANNIÈRE.
   ⚠️ CORRECTION D'UNE MESURE FAUSSE. Le relevé initial « ~185px de vide sous le
   titre, sur les 17 pages de marchand » comptait tout l'espace entre le bas du
   titre et le bas du hero. Or sur une page à bannière — 14 marchands sur 17 —
   cet espace n'est PAS vide : le pseudo-élément l'occupe (mesuré à 375px,
   bannière y=126→300, toute la largeur). Le vide n'existe que sur les 3 pages
   SANS bannière, où le hero est un rectangle noir de 300px sous un titre.
   Appliquer le remplissage partout faisait pire que le mal : le titre centré
   tombait DANS la bannière (mesuré à 375px : titre y=177→200, bannière à partir
   de y=126). D'où l'exclusion `:not(.campaign-hero--banniere)`.
   ⚠️ On REMPLIT, on ne rétrécit pas. Le `min-height: 300px` du hero de base est
   délibéré et documenté plus haut dans ce fichier : la bannière a besoin de
   cette hauteur pour exister (« à 220, l'image n'avait pas la place »). Baisser
   la hauteur guérirait le vide en cassant la bannière. */
.campaign-hero--merchant:not(.campaign-hero--banniere) .campaign-hero-body {
  flex: 1;
  align-items: center;
}

/* B — LA RANGÉE HAUTE QUI SE CASSE SUR TÉLÉPHONE.
   Mesuré à 375px, en coordonnées relatives au hero : les boutons se replient
   sur DEUX bandes (y=20 et y=63) et la rangée haute passe à 75px, si bien que
   le label « CAMPAGNE » (y=49→67) se retrouve coincé ENTRE les deux bandes.
   Ce n'est pas une collision de texte — le label est à x=24→93, le premier
   bouton commence à x=146 — c'est un entrelacement vertical : le label ne
   s'aligne plus sur aucune rangée et la rangée haute paraît cassée.
   Empilés, le label prend sa ligne et les boutons la leur, alignés à gauche. */
@media (max-width: 768px) {
  .campaign-hero--merchant .campaign-hero-top {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .campaign-hero--merchant .campaign-hero-actions { justify-content: flex-start; }
}

/* C — LE RYTHME SOUS LE HERO : le même que la page de campagne.
   Bureau : hero → 1er bloc 20 · entre blocs (bandeau de dates, cartes) 16 ·
   dernier bloc → titre 40. Téléphone : 16 · 12 · 32.
   ⚠️ Même cause que sur la page de campagne (cf. bloc « HERO CAMPAGNE », 6) :
   `.campaign-period` et `.campaign-info-row` portent tous deux
   `margin-top: -22px`, écrit pour annuler le gap de 24px de .dt-center. Au
   bureau il restait 2px ; sous 768px le gap tombe à 18px, d'où −4px — le
   bandeau glissait sous le hero (position:relative + z-index:1, il peint
   par-dessus) et ses coins hauts disparaissaient. Les marges ci-dessous sont
   la cible MOINS ce gap, et moins les 8px de margin-top du titre ; l'!important
   ne contre que celui de la règle de base `.campaign-info-row`.
   Les cartes suivent le hero quand le bandeau est absent (renderHero le pose
   [hidden] sans dates), le bandeau sinon : d'où la règle « bandeau visible +
   cartes », qui donne l'écart ENTRE blocs au lieu de l'écart au hero.
   Espacement SEUL : ni disposition, ni contenu, ni colonnes ne changent. */
.campaign-hero--merchant ~ .campaign-period {
  margin-top: calc(20px - 24px);
  margin-bottom: 0;
}
.campaign-hero--merchant ~ .campaign-info-row {
  gap: 16px;
  margin-top: calc(20px - 24px) !important;
  margin-bottom: calc(40px - 24px - 8px);
}
.campaign-hero--merchant ~ .campaign-period:not([hidden]) + .campaign-info-row {
  margin-top: calc(16px - 24px) !important;
}
@media (max-width: 768px) {
  .campaign-hero--merchant ~ .campaign-period { margin-top: calc(16px - 18px); }
  .campaign-hero--merchant ~ .campaign-info-row {
    gap: 12px;
    margin-top: calc(16px - 18px) !important;
    margin-bottom: calc(32px - 18px - 8px);
  }
  .campaign-hero--merchant ~ .campaign-period:not([hidden]) + .campaign-info-row {
    margin-top: calc(12px - 18px) !important;
  }
}


/* ── Ligne « Mode sombre / clair » du menu de compte mobile ─────────────────
   ⚠️ AUCUNE RÈGLE DE display ICI, à dessein. L'entrée est un `.dt-dd-item`
   porteur de `data-qw-auth="member"` : elle est déjà couverte, à la lettre, par
   `html.qw-auth-member .dt-dd-item[data-qw-auth="member"]` du bloc de gating.
   En ajouter une ici serait le piège de cascade de ce fichier —
   `[data-qw-auth="member"]{display:none}` et `.dt-dd-item{display:flex}` ont la
   MÊME spécificité (0-1-0) et ne se départagent que par l'ordre source ; une
   règle de plus, mal placée, masquerait ou révélerait au hasard du fichier.
   Il ne reste donc ici que de la mise en place. */
.qw-ligne-theme { justify-content: flex-start; }

/* ⚠️ `.dt-theme-btn` N'A AUCUNE DÉFINITION DE BASE — mesuré, pas supposé.
   Elle n'existe que dans deux media queries, `min-width:1024px` (l.1216) et
   `769-1023px` (l.3057), aux déclarations identiques. Sous 769px la classe ne
   peint donc RIEN : posée telle quelle dans ce menu mobile, elle rendait un
   span de 0×0 — un commutateur invisible dans une ligne par ailleurs
   fonctionnelle. C'est le défaut silencieux habituel : rien ne casse, l'écran
   est seulement faux.
   On fournit ici la moitié mobile qui manquait, aux MÊMES métriques (44×24,
   pastille de 16, translation de 20) pour que l'objet soit le même des deux
   côtés de la bascule. Les règles bureau ne sont pas touchées. */
.qw-ligne-theme .dt-theme-btn {
  margin-left: auto;          /* le commutateur à droite, le libellé à gauche */
  width: 44px;
  height: 24px;
  flex-shrink: 0;
  position: relative;
  background: var(--bg3);
  border: 1px solid var(--border2);
  border-radius: 12px;
}
.qw-ligne-theme .dt-theme-btn::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text2);
  transition: transform .3s;
}
[data-theme="light"] .qw-ligne-theme .dt-theme-btn::after {
  transform: translateX(20px);
  background: var(--orange);
}

/* ══════════════════════════════════════════════════════════════════════════
   CLS — RÉSERVATION DE HAUTEUR DES BLOCS REMPLIS EN ASYNCHRONE
   ──────────────────────────────────────────────────────────────────────────
   Lighthouse désignait #dtGptpBlock (0,287) et #fqBlock comme coupables. La
   MESURE corrige la moitié de ce diagnostic :

     #dtGptpBlock fait 450px avant ET après le rendu (229px en bureau). Il est
     100 % statique — 38 éléments en dur, aucun JS n'écrit dedans. Il ne se
     remplit pas : il est POUSSÉ. Lighthouse impute le glissement à l'élément
     qui BOUGE, pas à celui qui le provoque, et comme c'est le plus grand bloc
     visible, il récolte le plus gros score. Lui poser un min-height n'aurait
     eu aucun effet.

   Les vrais moteurs sont les trois blocs situés AU-DESSUS de lui, mesurés à
   375px puis 1440px :
     #dtNearbyBlock   57 → 208 / 188   (+151 / +131)
     #fqBlock         29 → 313 / 345   (+284 / +316)
     #dtDealsBlock    55 →   0 /   0   (s'efface : −55 / −57)
   Soit ~380px de déplacement infligés à tout ce qui suit.

   D'où deux traitements distincts, selon que le bloc se REMPLIT ou s'EFFACE :
   réservation ci-dessous pour les deux premiers ; pour le troisième, voir
   featured-quests.js — réserver la place d'un bloc qui va disparaître ne ferait
   que déplacer le problème.

   ⚠️ Les valeurs sont les hauteurs PEUPLÉES mesurées, pas des marges de
   confort : trop haut laisserait un blanc sous le contenu, trop bas laisserait
   un résidu de glissement. Elles sont stables parce que ces sections sont des
   RANGÉES qui défilent (.qw-rail) : le nombre de cartes change leur largeur
   utile, jamais leur hauteur. */
/* `.dt-block` pose `display:flex` ; un style d'auteur l'emporte sur la feuille
   du navigateur, donc l'attribut `hidden` seul ne masquerait rien. Cette règle
   le rend effectif — c'est elle qui permet à #dtDealsBlock de naître fermé. */
.dt-block[hidden] { display: none !important; }

#fqBlock       { min-height: 313px; }
#dtNearbyBlock { min-height: 208px; }

/* ⚠️ LE CARROUSEL EST LA CAUSE RESTANTE, ET IL N'ÉTAIT PAS DANS LA LISTE.
   Une fois les deux blocs ci-dessus stabilisés, la mesure montre que #fqBlock
   ne GRANDIT plus (313px avant comme après) mais BOUGE encore : il est poussé
   par le carrousel, qui naît à 0 et se remplit à 404px (422 en bureau) — au-
   dessus de toute la colonne, donc il déplace TOUT ce qui suit.
   C'est la même mécanique que celle qui faisait accuser #dtGptpBlock : la
   victime la plus visible récolte le score, la cause est en amont. La corriger
   ici évite de la voir resurgir sous un autre nom au prochain rapport.
   404/422 = la hauteur d'une diapositive (.l3cc-card, 420px fixes) plus sa
   bordure ; 6px pour la rangée de points. */
#dtAutoCarousel { min-height: 404px; }
#dtDots         { min-height: 6px; }

/* ⚠️⚠️ ET LE CARROUSEL N'EST PAS AFFICHÉ AU VISITEUR — la réservation ci-dessus
   ne le protège donc PAS sur l'écran que Lighthouse mesure.
   En coquille riche visiteur (§11), `#app.anon-rich #dtAutoCarousel` passe en
   `display:none` et c'est `#dtDemoFeatured` qui prend sa place. Or `min-height`
   sur un élément en `display:none` ne réserve RIEN : il ne participe pas à la
   mise en page. On protégeait donc un bloc absent pendant que son remplaçant,
   lui, naissait à 0 et sautait à ~476px — poussant `.dt-filters`,
   `#dtNearbyBlock`, `#fqBlock`, `#dtGptpBlock` et tout le reste de la colonne
   jusqu'au pied de page.
   C'est la troisième fois que le même piège se referme dans ce fichier : les
   blocs qui ENCAISSENT le score ne sont pas ceux qui le CAUSENT. Mesurer
   l'écran réellement servi, pas celui qu'on a sous la main — la première passe
   avait sondé la page en membre, où ce bloc n'existe pas.

   Valeurs = MINIMUM de la hauteur peuplée sur la plage de largeurs, relevé au
   pas de 20px. Le minimum, et pas la moyenne : sous-réserver ne laisse qu'un
   reliquat de glissement, sur-réserver laisserait un blanc sous la carte.
     · < 769px : la bannière est en `aspect-ratio:16/9` non plafonné, la carte
       suit la largeur (400 à 499px selon le repli du titre) → 400px.
     · ≥ 769px : la colonne fait déjà 721px, la bannière plafonne à son
       `max-height:300px` et la carte se fige à 476,6px → 476px. */
#dtDemoFeatured { min-height: 400px; }

@media (min-width: 769px) {
  #fqBlock        { min-height: 345px; }
  #dtNearbyBlock  { min-height: 188px; }
  #dtAutoCarousel { min-height: 422px; }
  #dtDemoFeatured { min-height: 476px; }

/* ── LE MOT-SYMBOLE : UNE BOÎTE, PAS UNE HAUTEUR ─────────────────────────────
   Second volet du même chantier, et celui que la première passe avait MANQUÉ.
   Le logo portait `height:22px; width:auto` : la hauteur était donnée, la
   largeur non. Un SVG distant n'ayant pas de taille connue avant son arrivée,
   la boîte naissait à 0 de large, puis s'ouvrait d'un coup à 189px — et
   poussait ce qui l'entoure. Lighthouse le nommait sous `unsized-images` ;
   la sonde `PerformanceObserver` de la première passe ne l'a jamais vu parce
   qu'elle mesurait la page en VISITEUR, où ce pied de page vit dans `#app`,
   masqué. La leçon est là : mesurer l'écran que l'utilisateur voit, pas celui
   qu'on a sous la main.

   La largeur est désormais portée par les ATTRIBUTS `width`/`height` de la
   balise, pas par la feuille de style : l'analyseur HTML les lit AVANT tout
   chargement, et réserve 189×22 dès la construction du DOM. Une règle CSS ne
   suffirait pas ici — elle arrive avec la feuille, et surtout `width:auto` la
   renverrait à l'attente de l'image.
   189×22 = la taille rendue mesurée (le viewBox 206×24 à hauteur 22 donne
   188,83 ; l'écart de 0,17px n'est pas observable).
   Cette règle ne fait donc QUE reprendre le `display:block` de l'ancien style
   inline, qui a disparu avec lui. */
.qw-logo-mark { display: block; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   ARCADE — cartes de jeu (arcade.html ET la section d'accueil d'index.html)
   ──────────────────────────────────────────────────────────────────────────────
   UNE seule définition pour DEUX pages. Les cartes d'arcade.html et celles de la
   section « Arcade » de l'accueil sont le même objet : les décrire deux fois,
   c'est signer deux versions qui divergeront (c'est l'histoire du chrome, §15).

   ⚠️ AUCUN FICHIER IMAGE. Les visuels sont des dégradés purs : deux cartes ne
   valent pas deux requêtes réseau de plus sur le chemin d'un téléphone, et un
   .webp de plus, c'est un asset à recadrer, à peser et à faire vivre. Tout est
   ici, dans le cache de la feuille déjà chargée.

   ⚠️ CES CLASSES NE DÉPENDENT PAS DE `.dt-block`. C'est délibéré : `.dt-block`
   et consorts vivent dans `@media(min-width:1024px)` (style.css ~1360) et
   n'existent PAS entre 0 et 1023px — l'accueil s'en tire parce que son bloc de
   révélation mobile les redéclare pour lui seul. Une carte d'arcade qui en
   hériterait serait donc nue sur un téléphone, c'est-à-dire sur 89 % du public.
   ══════════════════════════════════════════════════════════════════════════════ */

/* Remplace le style="display:block" que les autres pages posent en dur sur #app
   (masqué par défaut, style.css:302) : le plancher de styles inline ne remonte
   jamais. Même patron que .qw-soon-app de bientot.html. */
#app.arc-app { display: block; }

.arc-page { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.arc-title { font-size: 26px; font-weight: 800; letter-spacing: -.4px; color: var(--text); margin: 0; }
.arc-lead  { font-size: 14px; color: var(--text2); line-height: 1.6; margin: 0 0 14px; max-width: 52ch; }

/* `minmax(0, 1fr)` et non `1fr` : une piste `1fr` vaut `minmax(auto, 1fr)` et
   garde donc pour plancher le min-content de son contenu — c'est ce plancher-là
   qui fait déborder une grille à 320px, jamais le nombre de colonnes. Même
   correctif que le bloc « RÉVÉLATION MOBILE » plus haut. */
.arc-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* téléphone : une seule colonne */
  gap: 14px;
  min-width: 0;
  /* ⚠️ LE `section` NU DE LA FEUILLE DÉBORDE ICI — MESURÉ, à 1280px.
     style.css:126 pose `section { padding: 72px 20px }`, et sa variante
     ≥1024px (l.1237) y ajoute `max-width: 1200px; margin: 0 auto`. Ce sont des
     règles écrites pour les sections de la VITRINE (.vt-*), mais elles portent
     sur le TYPE d'élément : toute <section> du dépôt les reçoit. Mesuré sur
     arcade.html : la grille héritait de 96px de padding vertical et 48px
     d'horizontal, ce qui creusait un trou de 96px sous le chapeau et rentrait
     les cartes de 48px — pour une grille qui n'a rien demandé.
     On remet donc les trois propriétés à zéro. `.arc-grid` pèse 0-1-0 contre
     0-0-1 : la remise à zéro gagne à toutes les largeurs, sans !important. */
  padding: 0;
  max-width: none;
  margin: 0;
}
@media (min-width: 620px) {
  .arc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Trois jeux depuis le lot « Entrepôt » : au-delà de 1040px la sidebar laisse
   la place à trois colonnes, et deux cartes + une orpheline cessent d'être le
   rendu par défaut. En dessous, on garde deux colonnes : trois cartes à 700px
   donneraient des vignettes illisibles. */
@media (min-width: 1040px) {
  .arc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.arc-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color .22s var(--ease), transform .22s var(--ease);
}
.arc-card:hover { border-color: var(--border2); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) {
  .arc-card, .arc-card:hover { transition: none; transform: none; }
}

/* ── Le visuel : un dégradé, pas une image ──────────────────────────────────
   `aspect-ratio` plutôt qu'une hauteur fixe : la boîte est connue avant toute
   peinture, donc rien ne glisse (anti-CLS), et elle suit la largeur de la carte
   d'un téléphone à un écran large sans media query. */
.arc-visual {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}

/* VAISSEAU — un couloir qui fuit vers le haut, un vaisseau en bas.
   ::before = les barreaux du couloir (la vitesse) ; ::after = le vaisseau. */
.arc-visual--vaisseau {
  background:
    radial-gradient(120% 86% at 50% 108%, rgba(255, 107, 43, .34), transparent 64%),
    linear-gradient(180deg, var(--bg3) 0%, var(--bg2) 100%);
}
.arc-visual--vaisseau::before {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    180deg,
    rgba(255, 255, 255, .11) 0 2px,
    transparent 2px 22px
  );
  /* Le couloir se referme vers le haut : le masque fait le travail de la
     perspective, sans une seule forme à dessiner. */
  -webkit-mask-image: linear-gradient(180deg, transparent 4%, #000 58%, transparent 100%);
          mask-image: linear-gradient(180deg, transparent 4%, #000 58%, transparent 100%);
  clip-path: polygon(38% 0%, 62% 0%, 100% 100%, 0% 100%);
}
.arc-visual--vaisseau::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 14%;
  width: 22%;
  aspect-ratio: 1 / 1.25;
  transform: translateX(-50%);
  background: linear-gradient(180deg, var(--orange2), var(--orange));
  clip-path: polygon(50% 0%, 100% 100%, 50% 74%, 0% 100%);
}

/* CAPTURE — la créature en haut, la balle qui monte vers elle. */
.arc-visual--capture {
  background:
    radial-gradient(64% 64% at 50% 34%, rgba(124, 58, 237, .42), transparent 72%),
    linear-gradient(180deg, var(--bg3) 0%, var(--bg2) 100%);
}
.arc-visual--capture::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 16%;
  width: 34%;
  aspect-ratio: 1;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, var(--violet2), var(--violet) 62%, rgba(124, 58, 237, .25) 100%);
  box-shadow: 0 0 0 6px rgba(124, 58, 237, .16), 0 0 0 13px rgba(124, 58, 237, .07);
}
.arc-visual--capture::after {
  content: "";
  position: absolute;
  left: 22%;
  bottom: 13%;
  width: 13%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #FFF 2%, var(--orange2) 26%, var(--orange) 100%);
}

/* CIRCUIT — un bout de plateau vu de face : trois pièces, la ligne qui les
   relie, et un anneau au sol. AUCUN fichier image, comme ses trois voisines.
   L'anneau interrompu en bas à droite est la case marquée : la mécanique qui
   distingue ce jeu des trois autres, et ce que la carte doit annoncer.

   ⚠️ Les trois pièces sont des DISQUES, pas les cinq silhouettes du jeu
   (cercle, losange, triangle, hexagone, carré). Un dégradé radial ne sait
   faire qu'un disque, et les deux pseudo-éléments disponibles servent déjà à
   la ligne et à l'anneau. C'est sans conséquence ici : la vignette est
   `aria-hidden`, purement décorative, et n'a rien à faire distinguer — les
   formes sont le dispositif d'ACCESSIBILITÉ du plateau, où elles portent une
   information. Le dire plutôt que de laisser croire que la carte reprend la
   grammaire du jeu ; elle en reprend les couleurs et la composition. */
.arc-visual--circuit {
  background:
    /* la pièce orange (cercle) */
    radial-gradient(circle at 30% 34%, var(--orange2) 0 9%, transparent 9.5%),
    /* la pièce verte (cercle, plus petite : la forme exacte se lit dans le jeu) */
    radial-gradient(circle at 63% 30%, var(--green) 0 8%, transparent 8.5%),
    /* la pièce violette */
    radial-gradient(circle at 47% 62%, var(--violet2) 0 8.5%, transparent 9%),
    /* la trame des cases */
    repeating-linear-gradient(90deg, rgba(255,255,255,.035) 0 1px, transparent 1px 16.6%),
    repeating-linear-gradient(180deg, rgba(255,255,255,.035) 0 1px, transparent 1px 25%),
    /* vignette, pour que la rangée se lise comme quatre jeux */
    radial-gradient(86% 86% at 48% 44%, transparent 34%, rgba(10, 10, 18, .66) 100%),
    radial-gradient(70% 70% at 48% 40%, rgba(124, 58, 237, .20), transparent 72%),
    linear-gradient(180deg, #14141F 0%, #0C0C14 100%);
}
/* La ligne de liaison : elle passe DERRIÈRE les pièces dans le jeu, donc elle
   est ici sous elles en z-index — même grammaire visuelle. */
.arc-visual--circuit::before {
  content: "";
  position: absolute;
  left: 30%;
  top: 33%;
  width: 34%;
  height: 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, .62);
  transform: rotate(-4deg);
  transform-origin: left center;
}
/* La case marquée : anneau blanc en traits interrompus, AU SOL. */
.arc-visual--circuit::after {
  content: "";
  position: absolute;
  right: 21%;
  bottom: 22%;
  width: 15%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 3px dashed rgba(242, 242, 250, .8);
}

/* ENTREPÔT — le plancher de l'entrepôt vu du dessus, une caisse, une marque.
   Toujours AUCUN fichier image : des lames en dégradés répétés, un madrier
   sombre en bas, ::before = la caisse (biseautée), ::after = la marque.
   Les valeurs suivent celles du jeu — plancher chaud, charpente sombre — pour
   que la carte annonce vraiment ce qu'on va ouvrir. */
.arc-visual--entrepot {
  background:
    /* les joints entre lames */
    repeating-linear-gradient(180deg,
      rgba(20, 12, 5, .38) 0 2px, transparent 2px 25%),
    /* le fil du bois */
    repeating-linear-gradient(180deg,
      rgba(255, 220, 180, .04) 0 1px, transparent 1px 7px),
    /* Vignette : les bords rejoignent le fond de la page, comme sur les deux
       vignettes voisines. Sans elle la carte « Entrepôt » est un rectangle
       clair dans une rangée sombre, et c'est elle qu'on voit en premier — la
       rangée doit se lire comme trois jeux, pas comme un jeu et deux autres. */
    radial-gradient(86% 86% at 46% 42%, transparent 34%, rgba(14, 14, 20, .62) 100%),
    /* la lumière chaude au centre, notre orange */
    radial-gradient(72% 72% at 46% 42%, rgba(255, 107, 43, .22), transparent 70%),
    linear-gradient(180deg, #7A5130 0%, #533520 100%);
}
.arc-visual--entrepot::before {
  content: "";
  position: absolute;
  left: 24%;
  top: 30%;
  width: 21%;
  aspect-ratio: 1;
  border-radius: 4px;
  background: linear-gradient(160deg, var(--orange2) 0%, #A8622A 62%);
  /* Le biseau tient en deux ombres portées internes : clair en haut-gauche,
     sombre en bas-droite. Même logique que la texture du jeu. */
  box-shadow:
    inset 2px 2px 0 rgba(255, 205, 150, .5),
    inset -2px -2px 0 rgba(70, 38, 12, .65),
    3px 4px 7px rgba(0, 0, 0, .42);
}
.arc-visual--entrepot::after {
  content: "";
  position: absolute;
  right: 27%;
  top: 40%;
  width: 12%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 3px solid var(--violet2);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .28);
}

/* QWESTI JET — le vaisseau (le Titan, trapu, épaulières, dos acier) en bas,
   ses trois traits de tir qui montent, la courbe bleutée de la Terre et
   quelques étoiles. ::before = les tirs ; ::after = le vaisseau. Des
   dégradés, aucune image, comme les quatre vignettes voisines. */
.arc-visual--qwesti-jet {
  background:
    /* quelques étoiles */
    radial-gradient(circle at 17% 22%, rgba(255, 255, 255, .7) 0 1px, transparent 1.6px),
    radial-gradient(circle at 81% 15%, rgba(255, 255, 255, .55) 0 1px, transparent 1.6px),
    radial-gradient(circle at 69% 44%, rgba(255, 255, 255, .45) 0 1px, transparent 1.6px),
    radial-gradient(circle at 27% 61%, rgba(255, 255, 255, .4) 0 1px, transparent 1.6px),
    /* la Terre, en bas à droite : un disque sombre, un liseré d'atmosphère */
    radial-gradient(120% 92% at 94% 152%, rgba(30, 58, 110, .55) 0 58%, rgba(92, 225, 255, .30) 60.5%, transparent 64%),
    /* la chaleur des tuyères, sous le vaisseau */
    radial-gradient(34% 34% at 50% 100%, rgba(255, 107, 43, .30), transparent 72%),
    linear-gradient(180deg, #0B0D1C 0%, var(--bg2) 100%);
}
.arc-visual--qwesti-jet::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 6%;
  width: 24%;
  height: 50%;
  transform: translateX(-50%);
  /* trois traits : le central plus épais, blanc au cœur */
  background: linear-gradient(90deg,
    transparent 0 9%, #DDF8FF 9% 17%, transparent 17% 44%,
    #FFFFFF 44% 56%, transparent 56% 83%, #DDF8FF 83% 91%, transparent 91%);
  /* en rafales : des traits, pas des rails */
  -webkit-mask-image: repeating-linear-gradient(180deg, #000 0 24%, transparent 24% 36%);
          mask-image: repeating-linear-gradient(180deg, #000 0 24%, transparent 24% 36%);
  filter: drop-shadow(0 0 4px rgba(92, 225, 255, .9));
}
.arc-visual--qwesti-jet::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 11%;
  width: 27%;
  aspect-ratio: 1 / .92;
  transform: translateX(-50%);
  /* le dos acier, sur la coque orange */
  background:
    linear-gradient(90deg, transparent 41%, rgba(38, 42, 54, .9) 41% 59%, transparent 59%),
    linear-gradient(180deg, var(--orange2), var(--orange));
  clip-path: polygon(50% 0%, 60% 5%, 64% 24%, 91% 31%, 100% 57%, 92% 73%, 66% 70%, 62% 89%, 56% 100%,
    44% 100%, 38% 89%, 34% 70%, 8% 73%, 0% 57%, 9% 31%, 36% 24%, 40% 5%);
}

/* ── Le texte et l'appel à l'action ───────────────────────────────────────── */
.arc-card-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 16px 16px;
  min-width: 0;
  flex: 1 1 auto;
}
.arc-name { font-size: 16px; font-weight: 800; letter-spacing: -.2px; color: var(--text); margin: 0; }
.arc-desc {
  font-size: 13px; line-height: 1.55; color: var(--text2); margin: 0 0 10px;
  overflow-wrap: anywhere;   /* un mot long ne fait pas déborder une carte de 292px */
}

/* `margin-top:auto` : les deux cartes n'ont pas la même longueur de texte, et
   sans lui les deux boutons se retrouvent à des hauteurs différentes. */
.arc-cta {
  margin-top: auto;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;              /* cible tactile : la règle des 44px, au doigt */
  padding: 0 20px;
  border-radius: var(--radius-md);
  background: var(--orange);
  color: #fff;
  font-family: 'Inter', sans-serif;
  font-size: 14px;
  font-weight: 700;
  text-decoration: none;
  border: none;
  cursor: pointer;
  transition: filter .2s var(--ease);
}
/* ⚠️ PIÈGE DE SPÉCIFICITÉ — MESURÉ, pas déduit. Le défaut sûr du bloc « MENU
   JOUEUR AUTH-AWARE » est `[data-qw-auth="member"] { display: none }`, qui pèse
   0-1-0 — exactement comme `.arc-cta`. Entre deux règles de même poids c'est la
   DERNIÈRE qui gagne, et ce bloc-ci vient après : le `display:inline-flex`
   ci-dessus ROUVRAIT le bouton réservé au membre. Mesuré à 390px : les DEUX
   boutons s'affichaient, « Jouer » posé au-dessus de « Se connecter pour jouer ».
   Et rien ne le disait — pas d'erreur, pas de test rouge, juste un écran faux.
   On repose donc le défaut sûr à 0-2-0. La révélation (0-3-0, point 7 du bloc
   auth) passe toujours devant, l'asymétrie voulue est rétablie : sans session
   confirmée, c'est le bouton VISITEUR qu'on voit. */
.arc-cta[data-qw-auth="member"] { display: none; }

.arc-cta:hover { filter: brightness(1.08); }
/* La variante visiteur est plus discrète : elle ne promet pas une partie, elle
   demande un compte. Elle ne doit pas se faire passer pour le bouton « Jouer ». */
.arc-cta--anon { background: var(--bg3); color: var(--text); border: 1px solid var(--border2); }

/* ── RÉVÉLATION MOBILE d'arcade.html ───────────────────────────────────────
   Même piège que bientot.html et le portefeuille, même remède. Sous 768px le
   cadre masque .dt-main en `!important` avec une spécificité de 1-2-0 ; sans ce
   bloc, arcade.html n'afficherait sur un téléphone que ses deux barres de
   navigation, et RIEN entre les deux. Aucune erreur, aucun test rouge : un
   écran simplement vide — sur 89 % du public.
   `:has(.arc-body-mount)` porte la spécificité à 2-2-0, ce qui suffit. La classe
   est un CROCHET, pas un nom de style : elle n'existe que pour ce bloc. La
   renommer d'un seul côté revide la page, en silence. */
@media (max-width: 768px) {
  #app.logged-in-layout .dt-main:has(.arc-body-mount) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
  }
  .dt-body.arc-body-mount {
    display: block;
    padding: 14px 14px 28px;   /* + les 64px de dégagement que #app pose déjà */
  }
  .arc-title { font-size: 22px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PAGE ANNONCEURS — annonceurs.html / annonceurs.js (lot 4)
   Règles NEUVES et propres à cette page : chacune est accrochée à une classe
   .ann-* ou au crochet .ann-body-mount. Aucune règle existante n'est modifiée.

   ⚠️ LES DEUX THÈMES. Les diapositives de l'accueil écrivent du blanc en dur
   (.l3cc-title) sur un dégradé posé en ligne par carousel.js. Ici, AUCUN texte
   en dur : titres et noms en var(--text) sur var(--bg2) — sombre sur clair,
   clair sur sombre. Seul fond fixe : la PLAQUE BLANCHE des logos, sur laquelle
   les logos des marques sont dessinés pour être lus.

   ⚠️ TÉLÉPHONE. Sous 768px le cadre masque .dt-main en !important (1-2-0).
   Le sélecteur de révélation commence par #app (2-2-0) : sans lui, la page
   serait VIDE sur téléphone, sans erreur ni test rouge.
   ═══════════════════════════════════════════════════════════════════════════ */
#app.ann-app { display: block; }

/* Titre de page lisible par les lecteurs d'écran et les moteurs, invisible à
   l'écran (la page s'ouvre sur son carrousel). */
.ann-sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Colonne de contenu, centrée, 1100px au plus (claude.md §4). */
.ann-page {
  display: flex; flex-direction: column; gap: 18px;
  width: 100%; max-width: 1100px; margin: 0 auto; min-width: 0;
}
.ann-hero-mount:empty { display: none; }   /* aucun annonceur mis en avant : aucun cadre */

/* Les <section> nues reçoivent le rembourrage des sections de la vitrine
   (style.css:126 et sa variante ≥1024px) : remis à zéro, comme .arc-grid. */
.ann-hero, .ann-section { padding: 0; max-width: none; margin: 0; min-width: 0; }

/* ── Carrousel ──────────────────────────────────────────────────────────────
   .dt-carousel apporte l'affichage des diapositives (.ac-slide / .active) ;
   son cadre n'existe qu'au-dessus de 1024px : on le pose ici pour toutes les
   largeurs. overflow:hidden garde le geste de balayage dans le cadre. */
.ann-carousel {
  position: relative; overflow: hidden; border-radius: 20px;
  border: 1px solid var(--border2);
  touch-action: pan-y;             /* l'axe horizontal au balayage, le vertical à la page */
}
.ann-slide {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  height: 320px; padding: 24px;
  text-decoration: none; color: var(--text);
  background:
    radial-gradient(ellipse 60% 58% at 50% 38%, color-mix(in srgb, var(--orange) 20%, transparent) 0%, transparent 72%),
    var(--bg2);
}
/* Thème clair : l'ombre sombre de .dt-carousel (≥1024px) fait une tache grise
   autour du cadre sur le fond clair ; ici elle est adoucie. */
[data-theme="light"] .ann-carousel { box-shadow: 0 6px 22px rgba(0,0,0,.08); }
.ann-slide:focus-visible { outline: 2px solid var(--orange); outline-offset: -4px; }
.ann-socle { position: relative; display: flex; align-items: center; justify-content: center; }
.ann-socle::after {                /* ombre portée : le logo POSE sur la page */
  content: ''; position: absolute; left: 50%; bottom: -12px;
  width: 78%; height: 18px; transform: translateX(-50%); border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.30), transparent 70%);
  pointer-events: none;
}
.ann-slide-nom {
  max-width: 100%; margin: 4px 0 0;
  font-size: clamp(20px, 2.4vw, 28px); font-weight: 800; letter-spacing: -.4px; line-height: 1.15;
  text-align: center; color: var(--text); overflow-wrap: anywhere;
}
/* Flèches : même dessin que l'accueil, mais aux couleurs du THÈME (celles de
   l'accueil sont écrites en dur, blanc sur disque sombre). Visibles au survol
   ou au focus clavier. */
.ann-carousel .dt-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 10;
  width: 40px; height: 40px; padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--border2); color: var(--text);
  font-size: 20px; line-height: 1; cursor: pointer;
  opacity: 0; transition: opacity .25s, background .2s;
}
.ann-carousel .dt-arrow-l { left: 14px; }
.ann-carousel .dt-arrow-r { right: 14px; }
.ann-carousel .dt-arrow:hover { background: var(--bg3); }
.ann-carousel:hover .dt-arrow, .ann-carousel .dt-arrow:focus-visible { opacity: 1; }
/* Points : .cdot sert tel quel ; ce sont des <button>, sans leur habillage natif. */
.ann-dots .cdot { border: 0; padding: 0; font: inherit; -webkit-appearance: none; appearance: none; }
#annDots.l3-pause .cdot.active { animation-play-state: paused; }   /* survol : le remplissage s'arrête avec la rotation */
/* Thème clair : le remplissage BLANC du point actif disparaît sur le fond
   clair de la page ; ici il se remplit d'orange. */
[data-theme="light"] .ann-dots .cdot.active { background-image: linear-gradient(to right, var(--orange), var(--orange)); }

/* ── Logos : une plaque blanche, ronde, commune au carrousel et aux cartes ── */
.ann-plaque {
  position: relative; z-index: 1;
  width: 132px; height: 132px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  background: #fff; border: 1px solid var(--border2);
  box-shadow: 0 10px 28px rgba(0,0,0,.22);
}
.ann-logo { display: block; width: 72%; height: 72%; object-fit: contain; }
.ann-initiale { font-size: 48px; font-weight: 800; line-height: 1; color: var(--orange); }

/* ── Puces ──────────────────────────────────────────────────────────────────
   Au-dessus de 768px, .dt-filters / .dt-fb existants s'appliquent tels quels.
   Les puces inertes gardent le dessin normal (aria-disabled, aucun écouteur). */
.ann-filters .dt-fb { font-family: inherit; }

/* ── Grille ─────────────────────────────────────────────────────────────── */
.ann-section-title { margin: 6px 0 12px; font-size: 18px; font-weight: 800; letter-spacing: -.3px; color: var(--text); }
a.ann-card { text-decoration: none; color: inherit; }
a.ann-card:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.ann-visual {
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(ellipse 55% 62% at 50% 50%, color-mix(in srgb, var(--orange) 16%, transparent) 0%, transparent 74%),
    var(--bg3);
}
.ann-visual .ann-plaque { width: 96px; height: 96px; }
.ann-visual .ann-initiale { font-size: 36px; }
.ann-card .arc-name { overflow-wrap: anywhere; }
.ann-card .ann-type { align-self: flex-start; }
.ann-slide .ann-type { align-self: center; }
/* État vide ou erreur : une phrase, sur toute la largeur de la grille. */
.ann-etat {
  grid-column: 1 / -1; margin: 0; padding: 28px 16px; text-align: center;
  font-size: 14px; line-height: 1.5; color: var(--text2);
  background: var(--bg2); border: 1px dashed var(--border2); border-radius: var(--radius-lg);
}
.ann-etat--erreur { color: var(--text); border-style: solid; border-color: color-mix(in srgb, var(--red) 45%, transparent); }

/* ── Pastille de statut de campagne (cartes ET diapositives) ─────────────────
   Rendue par annonceurs.js (statut()), trois états d'UNE règle (classer) :
   --en-cours (vert), --bientot (ambre), --rien (gris discret, point creux).
   Couleurs reprises des pastilles d'état de la page de campagne
   (.campaign-info-pill, passées en AA dans les deux thèmes) : vert et jaune
   purs en sombre ; en clair, mêlés à var(--text) — purs, ils y tombent sous
   2:1. « Rien en ce moment » : --text2 dans les deux thèmes.
   .ann-pastilles porte la pastille ET le type ; à 360px elles passent l'une
   sous l'autre au besoin, jamais coupées (nowrap). --reserve garde la hauteur
   de la rangée pendant la lecture des campagnes : rien ne saute à l'arrivée. */
.ann-pastilles { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
.ann-pastilles:empty { display: none; }
.ann-pastilles--reserve, .ann-pastilles--reserve:empty { display: flex; min-height: 24px; }
.ann-slide .ann-pastilles { justify-content: center; }
.ann-statut {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border: 1px solid transparent; border-radius: 9999px;
  font-size: 11px; font-weight: 700; line-height: 14px; white-space: nowrap;
}
.ann-statut-point { flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.ann-statut--en-cours { color: var(--green);  border-color: rgba(34,197,94,.34);  background: rgba(34,197,94,.10); }
.ann-statut--bientot  { color: var(--yellow); border-color: rgba(245,158,11,.34); background: rgba(245,158,11,.10); }
.ann-statut--rien     { color: var(--text2);  border-color: var(--border2);       background: transparent; }
.ann-statut--rien .ann-statut-point { background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }
[data-theme="light"] .ann-statut--en-cours { color: color-mix(in srgb, var(--green) 45%, var(--text)); }
[data-theme="light"] .ann-statut--bientot  { color: color-mix(in srgb, var(--yellow) 45%, var(--text)); }

/* ── Téléphone (≤768px) ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
  #app.logged-in-layout .dt-main:has(.ann-body-mount) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
  }
  .dt-body.ann-body-mount {
    display: block;
    padding: 14px 14px 28px;   /* + les 64px de dégagement que #app pose déjà */
  }
  .ann-page { gap: 16px; }
  .ann-slide { height: 264px; padding: 18px; }
  .ann-plaque { width: 108px; height: 108px; }
  .ann-initiale { font-size: 40px; }
  .ann-carousel .dt-arrow { display: none; }      /* pas de survol au doigt : le balayage les remplace */
  /* Points : cible de doigt adossée au pas (même réglage que l'accueil). */
  .ann-dots { gap: 22px; margin-top: 12px; }
  .ann-dots .cdot { position: relative; }
  .ann-dots .cdot::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 28px; height: 40px; transform: translate(-50%, -50%);
  }
  /* Puces : les définitions de .dt-filters / .dt-fb n'existent pas sous 769px.
     Recopie de l'accueil : une rangée qui défile au doigt, ascenseur masqué. */
  .ann-filters {
    display: flex; align-items: center; gap: 8px;
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    padding-bottom: 2px;
  }
  .ann-filters::-webkit-scrollbar { display: none; }
  .ann-filters .dt-fb {
    flex: 0 0 auto;
    padding: 9px 14px; border-radius: 20px;
    border: 1px solid var(--border2); background: transparent;
    color: var(--text2); font-size: 13px; font-weight: 600;
    cursor: pointer; white-space: nowrap; transition: all .2s;
  }
  .ann-filters .dt-fb.active { background: var(--orange); border-color: var(--orange); color: #fff; }
  .ann-section-title { font-size: 16px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PAGE ANNONCEUR — annonceur.html / annonceur.js (lot P3)
   La fiche d'UN annonceur. Règles NEUVES, accrochées à .anr-* ou au crochet
   .anr-body-mount ; aucune règle existante n'est modifiée. La bannière reprend
   les classes de la page Annonceurs (.ann-socle, .ann-plaque, .ann-logo,
   .ann-initiale, .ann-type, .ann-etat) : même famille visuelle, sans rotation.

   ⚠️ LES DEUX THÈMES. Aucun texte en dur : var(--text) / var(--text2) sur
   var(--bg2) / var(--bg3). Les pastilles « Ouvert / Fermé » s'assombrissent au
   thème clair par color-mix avec --text (procédé de .dt-nav-badge-soon).

   ⚠️ TÉLÉPHONE. Sous 768px le cadre masque .dt-main en !important (1-2-0) : le
   sélecteur de révélation commence par #app (2-2-0), comme pour .ann-body-mount.
   ═══════════════════════════════════════════════════════════════════════════ */
#app.anr-app { display: block; }

.anr-page {
  display: flex; flex-direction: column; gap: 16px;
  width: 100%; max-width: 1100px; margin: 0 auto; min-width: 0;
}
.anr-contenu { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
.anr-retour {
  align-self: flex-start;
  font-size: 14px; font-weight: 600; color: var(--text2); text-decoration: none;
}
.anr-retour:hover { color: var(--text); }
.anr-retour:focus-visible { outline: 2px solid var(--orange); outline-offset: 3px; border-radius: 4px; }

/* Les <section> nues reçoivent le rembourrage des sections de la vitrine :
   remis à zéro, comme .ann-section. */
.anr-banniere, .anr-section { padding: 0; max-width: none; margin: 0; min-width: 0; }

/* ── Bannière ───────────────────────────────────────────────────────────── */
.anr-banniere {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 32px;
  padding: 32px; border-radius: 20px; border: 1px solid var(--border2);
  background:
    radial-gradient(ellipse 46% 80% at 14% 50%, color-mix(in srgb, var(--orange) 18%, transparent) 0%, transparent 72%),
    var(--bg2);
}
[data-theme="light"] .anr-banniere { box-shadow: 0 6px 22px rgba(0,0,0,.08); }
.anr-banniere .ann-socle { padding: 6px 6px 14px; }
.anr-identite { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; min-width: 0; }
.anr-nom {
  margin: 0; font-size: clamp(24px, 3vw, 34px); font-weight: 800; letter-spacing: -.5px; line-height: 1.12;
  color: var(--text); overflow-wrap: anywhere;
}
/* Type d'annonceur : .ob-category-pill écrit var(--violet) sur un voile violet,
   2,8:1 au thème sombre et 4,3:1 au clair (mesuré). Ici, sur la bannière
   seulement, le violet est mêlé à --text : plus clair au sombre, plus foncé au
   clair (procédé de .dt-nav-badge-soon). La pastille des autres pages est intacte. */
.anr-banniere .ann-type { color: color-mix(in srgb, var(--violet) 55%, var(--text)); }
.anr-sous-titre { margin: 0; font-size: 15px; font-weight: 600; color: var(--text2); overflow-wrap: anywhere; }
.anr-description {
  margin: 2px 0 0; max-width: 68ch;
  font-size: 15px; line-height: 1.6; color: var(--text);
  white-space: pre-line;           /* sauts de ligne de la saisie, texte échappé */
  overflow-wrap: anywhere;
}
.anr-contacts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; max-width: 100%; }
.anr-contact {
  display: inline-flex; align-items: center; gap: 8px; max-width: 100%; min-width: 0;
  padding: 8px 14px; border-radius: 999px;
  border: 1px solid var(--border2); background: var(--bg3);
  color: var(--text); font-size: 14px; font-weight: 600; text-decoration: none;
  overflow-wrap: anywhere; transition: border-color .2s, background .2s;
}
.anr-contact svg { flex-shrink: 0; color: var(--orange); }
.anr-contact:hover { border-color: var(--orange); }
.anr-contact:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }

/* ── Points de vente ────────────────────────────────────────────────────── */
.anr-lieux {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px;
  align-items: start;              /* une carte sans horaires ne s'étire pas à la hauteur de sa voisine */
}
.anr-lieu {
  display: flex; flex-direction: column; gap: 10px; min-width: 0;
  padding: 16px; border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--bg2);
}
.anr-lieu-nom { margin: 0; font-size: 16px; font-weight: 800; color: var(--text); overflow-wrap: anywhere; }
.anr-lieu-adresse { margin: 0; font-size: 14px; line-height: 1.5; color: var(--text2); overflow-wrap: anywhere; }
.anr-lieu-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.anr-statut {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; border-radius: 999px; font-size: 13px; font-weight: 700;
}
.anr-statut-point { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.anr-statut.is-ouvert { color: var(--green); background: color-mix(in srgb, var(--green) 14%, transparent); }
.anr-statut.is-ferme  { color: var(--text2); background: var(--bg3); }
[data-theme="light"] .anr-statut.is-ouvert { color: color-mix(in srgb, var(--green) 55%, var(--text)); }
.anr-itineraire {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border2);
  color: var(--text); font-size: 13px; font-weight: 700; text-decoration: none;
}
.anr-itineraire svg { color: var(--orange); }
.anr-itineraire:hover { border-color: var(--orange); }
.anr-itineraire:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
/* Semaine : <details>, ouvert au bureau, replié au téléphone (décidé au rendu). */
.anr-semaine { border-top: 1px solid var(--border); padding-top: 8px; }
.anr-semaine-titre {
  cursor: pointer; font-size: 13px; font-weight: 700; color: var(--text2); padding: 2px 0;
}
.anr-semaine-titre:hover { color: var(--text); }
.anr-semaine-titre:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; border-radius: 4px; }
.anr-semaine-table { width: 100%; margin-top: 8px; border-collapse: collapse; font-size: 13px; }
.anr-semaine-table th, .anr-semaine-table td { padding: 5px 8px; text-align: left; vertical-align: top; }
.anr-semaine-table th { width: 40%; font-weight: 600; color: var(--text2); }
.anr-semaine-table td { color: var(--text); overflow-wrap: anywhere; }
.anr-semaine-table tr.is-aujourdhui th, .anr-semaine-table tr.is-aujourdhui td { font-weight: 800; color: var(--text); background: var(--bg3); }
.anr-semaine-table tr.is-aujourdhui th { box-shadow: inset 3px 0 0 0 var(--orange); border-radius: 6px 0 0 6px; }
.anr-semaine-table tr.is-aujourdhui td { border-radius: 0 6px 6px 0; }

/* ── Campagnes ──────────────────────────────────────────────────────────── */
.anr-camp-groupe { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.anr-camp-groupe + .anr-camp-groupe { margin-top: 18px; }
.anr-camp-titre { margin: 0; font-size: 15px; font-weight: 800; color: var(--text2); letter-spacing: -.1px; }
/* Terminées : la BANNIÈRE pâlit, jamais le texte (contraste intact). */
.anr-camp-terminees .card-banner-container { filter: grayscale(.7); }

/* ── Téléphone (≤768px) ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
  #app.logged-in-layout .dt-main:has(.anr-body-mount) {
    display: flex !important;
    flex-direction: column;
    margin-left: 0 !important;
  }
  .dt-body.anr-body-mount {
    display: block;
    padding: 14px 14px 28px;   /* + les 64px de dégagement que #app pose déjà */
  }
  .anr-banniere {
    grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 18px;
    padding: 24px 16px; text-align: center;
    background:
      radial-gradient(ellipse 70% 42% at 50% 22%, color-mix(in srgb, var(--orange) 18%, transparent) 0%, transparent 72%),
      var(--bg2);
  }
  .anr-identite { align-items: center; }
  .anr-contacts { justify-content: center; }
  .anr-banniere .ann-plaque { width: 108px; height: 108px; }
  .anr-banniere .ann-initiale { font-size: 40px; }
  .anr-lieu { padding: 14px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PANNEAU « MON ACTIVITÉ » — annonceurs.html / annonceur.html / arcade.html
   La flèche ❮ (#sidebarToggle) pose body.sidebar-open, qui révèle .dt-right.
   Ces trois pages n'avaient PAS de .dt-right : la flèche tournait sans rien
   ouvrir. Elles portent maintenant la même carte que classement.html, mais
   SANS ses styles en ligne : ils deviennent les classes ci-dessous, bornées à
   ces trois pages.
   ═══════════════════════════════════════════════════════════════════════════ */
#app.ann-app .acv2-bar-fill, #app.anr-app .acv2-bar-fill, #app.arc-app .acv2-bar-fill { width: 0; }   /* profile-sync pose la largeur réelle */
#app.ann-app .acv2-val-gm, #app.anr-app .acv2-val-gm, #app.arc-app .acv2-val-gm { color: var(--yellow); }
[data-theme="light"] #app.ann-app .acv2-val-gm, [data-theme="light"] #app.anr-app .acv2-val-gm, [data-theme="light"] #app.arc-app .acv2-val-gm { color: color-mix(in srgb, var(--yellow) 45%, var(--text)); }
#app.ann-app .acv2-gm-zone, #app.anr-app .acv2-gm-zone, #app.arc-app .acv2-gm-zone { padding: 10px 16px 14px; border-top: 1px solid var(--border); }
#app.ann-app .acv2-gm-btn, #app.anr-app .acv2-gm-btn, #app.arc-app .acv2-gm-btn {
  width: 100%; padding: 10px; border: none; border-radius: 10px;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  background: linear-gradient(135deg, var(--orange), var(--orange2)); color: #fff;
  font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 800; cursor: pointer; transition: all .2s;
}
#app.ann-app .acv2-gm-btn:focus-visible, #app.anr-app .acv2-gm-btn:focus-visible, #app.arc-app .acv2-gm-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
#app.ann-app .acv2-voir, #app.anr-app .acv2-voir, #app.arc-app .acv2-voir { text-align: center; padding: 6px 0; }
#app.ann-app .acv2-voir a, #app.anr-app .acv2-voir a, #app.arc-app .acv2-voir a { color: var(--orange); text-decoration: none; font-size: 12px; font-weight: 600; }
/* Téléphone : ces pages RÉVÈLENT .dt-main sous 768px (les autres le masquent).
   Un body.sidebar-open mémorisé depuis le bureau y poserait la carte au milieu
   de la colonne : elle reste cachée, comme partout ailleurs au téléphone. */
@media (max-width: 768px) {
  #app.ann-app .dt-right, #app.anr-app .dt-right, #app.arc-app .dt-right { display: none !important; }
}

/* ── Filtres de la page Annonceurs ───────────────────────────────────────────
   « Favoris » est affiché mais pas encore actif (aria-disabled, sans écouteur) :
   même dessin que les autres puces, curseur « indisponible », info-bulle « Bientôt ». */
.ann-filters .dt-fb[aria-disabled="true"] { cursor: not-allowed; }
/* Lien « Voir toute l'activité » du panneau : l'orange seul tombe sous 4,5:1
   au thème clair ; mêlé à --text comme les autres textes de ce lot. */
[data-theme="light"] #app.ann-app .acv2-voir a, [data-theme="light"] #app.anr-app .acv2-voir a, [data-theme="light"] #app.arc-app .acv2-voir a { color: color-mix(in srgb, var(--orange) 62%, var(--text)); }

/* ══════════════════════════════════════════════════════════════════════════
   LOT « XP DE FIN DE CAMPAGNE » — progression de campagne (page + carte) et
   écran « Campagne terminée » du tunnel.
   Les segments RÉUTILISENT .ob-progress-dashes / .ob-dash / .ob-dash-done des
   cartes de quête (même vert --green, même gris) : un seul vocabulaire de
   progression dans l'application. Le gain promis/obtenu reprend le jaune des
   badges d'XP (.ob-xp-badge). Rendu : UI_buildCampaignProgress (shared.js).
   ══════════════════════════════════════════════════════════════════════════ */

/* 1 — PAGE DE CAMPAGNE · rythme. La section s'insère entre le hero et les
   cartes de statistiques, dont l'écart était réglé au pixel (§6 plus haut) :
   bureau hero → progression 20 · progression → cartes 16 (l'écart ENTRE
   cartes) ; téléphone 16 · 12. Mêmes calculs « cible − gap de .dt-center ».
   ⚠️ Le `!important` du second sélecteur n'est pas une facilité : la règle
   qu'il contre (`.campaign-hero--campaign ~ .campaign-info-row`) en porte un,
   hérité de la règle de base. Spécificité supérieure + !important = c'est la
   nôtre qui s'applique, et SEULEMENT quand la progression est affichée. */
.campaign-progress[hidden] { display: none; }
.campaign-hero--campaign ~ .campaign-progress { margin-top: calc(20px - 24px); }
.campaign-hero--campaign ~ .campaign-progress:not([hidden]) + .campaign-info-row {
  margin-top: calc(16px - 24px) !important;
}
/* Mise en page « linear » : elle a sa propre barre en pourcentage. Sur une page
   de campagne, la progression par segments la remplace — jamais les deux. */
.campaign-progress:not([hidden]) ~ #qst-progress-block { display: none; }

.cp-progress {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 16px;
  border-radius: 14px;
  background: var(--bg3);
  border: 1px solid var(--border);
}
.cp-progress-tete {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
}
.cp-progress-titre {
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--text);
}
.cp-progress-check { color: var(--green); font-weight: 800; }
.cp-progress-compte {
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--text2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Une rangée, jamais deux : flex:1 (hérité de .ob-dash) + min-width:0 répartit
   la largeur sans débordement, quel que soit le nombre de quêtes. Au-delà de 8
   segments l'écart se resserre pour que chacun reste lisible. */
.cp-progress-barre { gap: 6px; }
.cp-progress-barre .ob-dash { height: 8px; border-radius: 4px; min-width: 0; }
.cp-progress-barre:has(> .ob-dash:nth-child(9)) { gap: 4px; }
.cp-progress-xp {
  margin: 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text2);
}
.cp-progress-xp strong { font-weight: 800; color: var(--yellow); }
.cp-progress.is-complete { border-color: color-mix(in srgb, var(--green) 34%, transparent); }
.cp-progress.is-complete .cp-progress-titre { color: var(--green); }
.cp-progress-xp.is-obtenu strong { color: var(--green); }
/* Terminée : « 2 quêtes requises complétées » (snapshot), puis l'XP reçue. La
   mention d'une quête ajoutée depuis reste DISCRÈTE : c'est une information,
   pas un état — la campagne demeure terminée. */
.cp-progress-detail { margin: 0; font-size: 13px; line-height: 1.4; color: var(--text2); }
/* --text2 et non --text3 : mesuré, --text3 sur --bg3 donne 2,55:1 — sous les
   4,5:1 exigés pour un texte de 12px. La discrétion vient de la taille. */
.cp-progress-note { margin: 0; font-size: 12px; line-height: 1.4; color: var(--text2); }
.cp-progress .cp-short { display: none; }

/* Thème clair : le gris des traits est un blanc translucide, invisible sur un
   fond clair ; le jaune seul tombe sous 4,5:1 (même correctif que .acv2-val-gm). */
[data-theme="light"] .cp-progress .ob-dash:not(.ob-dash-done) { background-color: color-mix(in srgb, var(--text) 16%, transparent); }
[data-theme="light"] .cp-progress-xp strong { color: color-mix(in srgb, var(--yellow) 45%, var(--text)); }
/* Même correctif pour le VERT de l'état terminé : seul sur fond clair, le titre
   « ✓ Campagne terminée » mesurait 1,92:1. */
[data-theme="light"] .cp-progress.is-complete .cp-progress-titre,
[data-theme="light"] .cp-progress-check,
[data-theme="light"] .cp-progress-xp.is-obtenu strong { color: color-mix(in srgb, var(--green) 45%, var(--text)); }

@media (max-width: 768px) {
  .campaign-hero--campaign ~ .campaign-progress { margin-top: calc(16px - 18px); }
  .campaign-hero--campaign ~ .campaign-progress:not([hidden]) + .campaign-info-row {
    margin-top: calc(12px - 18px) !important;
  }
  .cp-progress { gap: 8px; padding: 12px 14px; }
  .cp-progress .cp-long { display: none; }
  .cp-progress .cp-short { display: inline; }
  .cp-progress-barre { gap: 4px; }
  .cp-progress-barre .ob-dash { height: 6px; }
  .cp-progress-barre:has(> .ob-dash:nth-child(9)) { gap: 2px; }
}

/* Rattrapage constaté à l'ouverture de la page : un halo, une fois. */
@media (prefers-reduced-motion: no-preference) {
  .cp-progress.is-celebration { animation: cpCelebration 1400ms cubic-bezier(.16, 1, .3, 1) both; }
  .cp-progress.is-celebration .ob-dash-done {
    transform-origin: left center;
    animation: cpSegmentIn 560ms cubic-bezier(.16, 1, .3, 1) 120ms both;
  }
}
@keyframes cpCelebration {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent); }
  to   { box-shadow: 0 0 0 16px color-mix(in srgb, var(--green) 0%, transparent); }
}
@keyframes cpSegmentIn {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* 2 — CARTE DE CAMPAGNE · rangée compacte, juste au-dessus du pied, dans le
   dégradé sombre du bas de bannière. Blanc + ombre portée : même traitement
   que le titre de la carte (.ob-campaign-card .ob-card-title), seul moyen de
   rester lisible sur une image quelconque. */
.ob-camp-progress {
  position: relative;
  z-index: 10;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 16px 12px;
}
.ob-camp-progress-tete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
  font-size: 11px;
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .85);
}
.ob-camp-progress-etat { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.ob-camp-progress-check { color: var(--green); }
.ob-camp-progress-xp.ob-xp-badge { font-size: 11px; padding: 2px 8px; text-shadow: none; }
.ob-camp-progress-barre { gap: 3px; }
.ob-camp-progress-barre .ob-dash { height: 4px; min-width: 0; background-color: rgba(255, 255, 255, .28); }
.ob-camp-progress-barre .ob-dash.ob-dash-done { background-color: var(--green); }
.ob-camp-progress-barre:has(> .ob-dash:nth-child(9)) { gap: 2px; }

/* 3 — TUNNEL · écran « Campagne terminée » (qt-phase-campagne-template).
   Même scène que la phase 3 (.qt-reward-stage), sceau doré et segments en
   plus. Les barres d'XP rejouent les MÊMES keyframes que la phase 3 ; elles
   sont réécrites ici parce que la phase 3 les accroche par adjacence à
   .reward-subtitle, que la rangée de segments interrompt. */
.qt-campagne-kicker {
  margin: 0 0 var(--qt-s1);
  color: var(--yellow);
  font-size: 0.75rem;
  font-weight: 850;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.reward-icon-container--campagne {
  border-color: color-mix(in srgb, var(--yellow) 44%, var(--qt-text) 10%);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--qt-text) 14%, transparent),
    0 24px 70px color-mix(in srgb, var(--qt-overlay-bg) 60%, transparent),
    0 0 0 7px color-mix(in srgb, var(--yellow) 12%, transparent),
    0 0 60px color-mix(in srgb, var(--yellow) 26%, transparent);
}
.reward-icon-container--campagne::before {
  content: "★";
  background: linear-gradient(145deg,
    color-mix(in srgb, var(--yellow) 78%, var(--qt-accent) 22%),
    color-mix(in srgb, var(--qt-accent) 55%, var(--yellow) 45%));
}
.qt-campagne-segments {
  width: min(560px, 100%);
  margin-top: var(--qt-s3);
  gap: 6px;
}
.qt-campagne-segments .ob-dash { height: 10px; border-radius: 5px; min-width: 0; }
.qt-campagne-segments:has(> .ob-dash:nth-child(9)) { gap: 3px; }
.qt-reward-stage--campagne .qt-xp-engine + .qt-xp-engine .qt-bar-fill {
  background:
    radial-gradient(circle at 18% 50%, color-mix(in srgb, var(--qt-text) 18%, transparent), transparent 24%),
    linear-gradient(90deg, var(--qt-accent), color-mix(in srgb, var(--qt-accent) 56%, var(--qt-success) 44%), var(--qt-success));
}
@media (prefers-reduced-motion: no-preference) {
  .qt-campagne-kicker {
    opacity: 0;
    animation: qtFlowContentIn 520ms cubic-bezier(0.16, 1, 0.3, 1) 160ms forwards;
  }
  .qt-campagne-segments .ob-dash {
    transform-origin: left center;
    animation: qtCampagneSegment 460ms cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(380ms + var(--qt-i, 0) * 90ms);
  }
  .qt-reward-stage--campagne .qt-xp-engine .qt-bar-fill {
    animation: qtQuestXpFill 1150ms cubic-bezier(0.16, 1, 0.3, 1) 760ms both;
  }
  .qt-reward-stage--campagne .qt-xp-engine + .qt-xp-engine .qt-bar-fill {
    animation: qtPlayerXpGain 1050ms cubic-bezier(0.16, 1, 0.3, 1) 980ms both;
  }
}
@media (prefers-reduced-motion: reduce) {
  .qt-reward-stage--campagne .qt-xp-engine .qt-bar-fill { transform: scaleX(var(--qt-xp-to, 1)); }
}
@keyframes qtCampagneSegment {
  from { opacity: 0.4; transform: scaleX(0); }
  to   { opacity: 1;   transform: scaleX(1); }
}

/* ══════════════════════════════════════════════════════════════════════════
   SUPPRESSION DE COMPTE & DÉPART D'UN COMMERCE — LES ÉCRANS (lot 2b)
   Feuille de confirmation (QwFeuille, shared.js), zone « Quitter Qwestify » et
   mode départ du portail annonceur, bandeau des bons d'un commerce qui part,
   page compte-supprime.html. Conçus à 375×667, mesurés à 320, 360, 390 et sur
   bureau (claude.md §18) : cibles ≥ 44 px, champs ≥ 16 px, zones sûres, dvh.
   ⚠️ Rouge pour le texte : #EF4444 sur fond clair ne fait que 3,8:1. Le texte
   reste donc en --text sur un fond teinté, et le bouton plein prend un rouge
   assombri (≈ 5,9:1 avec du blanc) dérivé de --red, jamais une valeur en dur.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── La feuille : plein écran sur téléphone, fenêtre centrée dès 769 px ── */
html.qw-feuille-ouverte, html.qw-feuille-ouverte body { overflow: hidden; }
.qw-feuille {
  position: fixed; inset: 0; z-index: 10000;
  height: 100vh; height: 100dvh;
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  background: var(--bg); color: var(--text);
  font-family: 'Inter', sans-serif;
}
.qw-feuille-panneau {
  box-sizing: border-box;
  width: 100%; max-width: 560px; min-height: 100%;
  margin: 0 auto;
  padding:
    max(20px, env(safe-area-inset-top, 0px))
    max(16px, env(safe-area-inset-right, 0px))
    max(24px, env(safe-area-inset-bottom, 0px))
    max(16px, env(safe-area-inset-left, 0px));
  display: flex; flex-direction: column; gap: 16px;
}
.qw-feuille-titre {
  margin: 8px 0 0;
  font-size: 24px; line-height: 1.25; font-weight: 800; letter-spacing: -.3px;
  color: var(--text);
}
.qw-feuille-titre:focus:not(:focus-visible) { outline: none; }
.qw-feuille-corps { display: flex; flex-direction: column; gap: 12px; }
.qw-feuille-ligne {
  margin: 0;
  font-size: 16px; line-height: 1.5; color: var(--text);
  overflow-wrap: break-word;
}
.qw-feuille-ligne strong { font-weight: 800; }

.qw-feuille-champ { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.qw-feuille-champ[hidden] { display: none !important; }
.qw-feuille-label { font-size: 15px; font-weight: 700; color: var(--text); }
.qw-feuille-input {
  box-sizing: border-box; width: 100%; min-height: 52px;
  padding: 12px 14px;
  font: 700 18px/1.2 'Inter', sans-serif; letter-spacing: 2px;   /* ≥ 16 px : Safari iOS ne zoome pas */
  color: var(--text); background: var(--bg2);
  border: 2px solid var(--border2); border-radius: 12px;
}
.qw-feuille-input:focus { outline: none; border-color: var(--orange); }
.qw-feuille-input:disabled { opacity: .6; }

.qw-feuille-message {
  border-radius: 12px; padding: 12px 14px;
  font-size: 15px; line-height: 1.5; color: var(--text);
}
.qw-feuille-message[hidden] { display: none !important; }
.qw-feuille-message p { margin: 0; overflow-wrap: break-word; }
.qw-feuille-message--erreur {
  background: color-mix(in srgb, var(--red) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
}
.qw-feuille-message--info { background: var(--bg2); border: 1px solid var(--border2); }
.qw-feuille-lien {
  display: inline-flex; align-items: center; min-height: 44px; margin-top: 4px;
  font-weight: 800; color: var(--orange); text-decoration: underline;
}
[data-theme="light"] .qw-feuille-lien { color: color-mix(in srgb, var(--orange), black 35%); }

.qw-feuille-actions {
  display: flex; flex-direction: column; gap: 10px;
  margin-top: auto; padding-top: 8px;
}
/* Bouton partagé : la feuille, la zone « Quitter Qwestify », le bandeau de départ. */
.qw-feuille-btn {
  box-sizing: border-box; width: 100%; min-height: 52px;
  padding: 12px 18px;
  border-radius: 14px; border: 2px solid transparent;
  font: 800 16px/1.2 'Inter', sans-serif;
  cursor: pointer; touch-action: manipulation;
}
.qw-feuille-btn[hidden] { display: none !important; }
.qw-feuille-btn--danger    { background: color-mix(in srgb, var(--red), black 22%); color: #fff; }
.qw-feuille-btn--principal { background: var(--violet); color: #fff; }
.qw-feuille-btn--secondaire { background: transparent; color: var(--text); border-color: var(--border2); }
.qw-feuille-btn:disabled { opacity: .45; cursor: not-allowed; }
.qw-feuille-btn:focus-visible,
.qw-feuille-lien:focus-visible { outline: 3px solid var(--orange); outline-offset: 2px; }

@media (min-width: 769px) {
  .qw-feuille {
    display: flex; padding: 32px 16px;
    background: color-mix(in srgb, var(--bg) 82%, transparent);
  }
  /* margin:auto et non align-items:center : une fenêtre plus haute que l'écran
     déborderait vers le HAUT, hors d'atteinte du défilement. */
  .qw-feuille-panneau {
    margin: auto; min-height: 0;
    padding: 28px 28px 24px;
    background: var(--bg2); border: 1px solid var(--border2); border-radius: 20px;
    box-shadow: var(--shadow);
  }
  .qw-feuille-actions { flex-direction: row-reverse; justify-content: flex-start; margin-top: 8px; }
  .qw-feuille-btn { width: auto; min-width: 168px; }
}

/* ── Portail annonceur : « Quitter Qwestify » ── */
.md-quitter { border-color: color-mix(in srgb, var(--red) 35%, var(--border)); }
.md-quitter-intro { margin: 0 0 8px; font-size: 15px; font-weight: 700; color: var(--text); }
.md-quitter-liste { margin: 0 0 16px; padding-left: 22px; font-size: 15px; line-height: 1.6; color: var(--text); overflow-wrap: break-word; }
.md-quitter-etapes { margin-bottom: 18px; }
.md-quitter-etapes p { margin: 0 0 10px; font-size: 15px; line-height: 1.5; color: var(--text2); overflow-wrap: break-word; }
.md-quitter-etapes strong { color: var(--text); }
.md-quitter .qw-feuille-btn { width: auto; }
@media (max-width: 620px) {
  .md-quitter .qw-feuille-btn { width: 100%; }
}

/* ── Portail annonceur : mode départ ── */
.md-depart {
  margin: 0 0 18px; padding: 16px 18px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--yellow) 12%, var(--bg2));
  border: 1px solid color-mix(in srgb, var(--yellow) 50%, transparent);
}
.md-depart-titre { margin: 0 0 8px; font-size: 17px; font-weight: 800; line-height: 1.35; color: var(--text); overflow-wrap: break-word; }
.md-depart-ligne { margin: 0 0 6px; font-size: 15px; line-height: 1.5; color: var(--text); }
.md-depart .qw-feuille-btn { width: auto; margin-top: 10px; }
@media (max-width: 620px) {
  .md-depart .qw-feuille-btn { width: 100%; }
}
/* Les onglets hors « Valider un bon » : présents, lisibles, inactifs. */
.md-tab:disabled { opacity: .4; cursor: not-allowed; }

/* ── Portail annonceur : création fermée (lot 3A) ──
   L'interrupteur de l'admin (Marchands). Fermé : le bandeau #mdFerme, et tout ce
   qui porte data-creation disparaît (la base refuse déjà : QWE01). body + attribut :
   plus spécifique que les règles du portail (.md-section-head, .md-add-bons…). */
.md-ferme {
  margin: 0 0 18px; padding: 16px 18px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--violet) 10%, var(--bg2));
  border: 1px solid color-mix(in srgb, var(--violet) 45%, transparent);
}
.md-ferme-titre { margin: 0 0 8px; font-size: 17px; font-weight: 800; line-height: 1.35; color: var(--text); overflow-wrap: break-word; }
.md-ferme-ligne { margin: 0 0 6px; font-size: 15px; line-height: 1.5; color: var(--text); overflow-wrap: break-word; }
.md-ferme-lien, .md-facturation-lien {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--violet); font-weight: 700; overflow-wrap: anywhere;
}
body.md-creation-fermee [data-creation] { display: none; }

/* ── Portail annonceur : statistiques (lot 3A) — de vrais chiffres, sans graphique inventé ── */
.md-stats-etat { margin: 0 0 14px; font-size: 15px; line-height: 1.5; color: var(--text2); overflow-wrap: break-word; }
/* .kpi-grid { display: grid } (portail) battait l'attribut hidden : les tuiles vides restaient visibles. */
.kpi-grid[hidden] { display: none; }
.kpi-num.md-kpi-orange { color: var(--orange); }
.kpi-num.md-kpi-vert   { color: var(--green); }
.kpi-num.md-kpi-violet { color: var(--violet); }
#statsKpi .kpi-sub { color: var(--text3); }
.md-stats-bloc { margin: 0 0 22px; }
.md-stats-titre { margin: 0 0 10px; font-size: 15px; font-weight: 800; color: var(--text); }
.md-stats-t { width: 100%; border-collapse: collapse; font-size: 14px; }
.md-stats-t th, .md-stats-t td { padding: 10px 8px; text-align: left; border-bottom: 1px solid var(--border); }
.md-stats-t th { font-size: 12px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--text3); }
.md-stats-t td { color: var(--text); overflow-wrap: anywhere; }
@media (max-width: 620px) {
  /* Une ligne = une carte : l'en-tête de colonne passe devant chaque valeur. */
  .md-stats-t thead { display: none; }
  .md-stats-t tr { display: block; padding: 8px 0; border-bottom: 1px solid var(--border); }
  .md-stats-t td { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; border-bottom: 0; text-align: right; }
  .md-stats-t td::before { content: attr(data-th); flex: 0 0 auto; color: var(--text3); font-weight: 600; text-align: left; }
}

/* ── Portail annonceur : facturation (lot 3A) — la situation réelle, web seulement ── */
.md-facturation-ligne { margin: 0 0 10px; font-size: 15px; line-height: 1.55; color: var(--text); overflow-wrap: break-word; }
.md-facturation-ligne:last-child { margin-bottom: 0; }

/* ── Récompenses : un bon dont le commerce quitte Qwestify ── */
.wal-coupon-depart {
  position: relative; z-index: 1;
  margin: -2px 0 0; padding: 9px 11px;
  border-radius: 10px;
  font-size: 13px; font-weight: 700; line-height: 1.4; color: var(--text);
  background: color-mix(in srgb, var(--yellow) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--yellow) 50%, transparent);
  overflow-wrap: break-word;
}

/* ── compte-supprime.html ──
   ⚠️ La carte est une <section> : les règles globales `section` (padding de
   72 px, puis max-width et marge auto sur bureau) l'écraseraient. Chaque
   propriété qu'elles posent est donc reprise ici, par classe. */
.qw-adieu-page {
  box-sizing: border-box;
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  padding:
    max(16px, env(safe-area-inset-top, 0px))
    max(16px, env(safe-area-inset-right, 0px))
    max(16px, env(safe-area-inset-bottom, 0px))
    max(16px, env(safe-area-inset-left, 0px));
}
.qw-adieu-entete { display: flex; justify-content: center; padding: 8px 0; }
.qw-adieu-logo { display: block; width: 40px; height: 40px; border-radius: 10px; }
.qw-adieu { flex: 1 0 auto; display: flex; align-items: center; justify-content: center; padding: 24px 0; }
.qw-adieu-carte {
  box-sizing: border-box; width: 100%; max-width: 460px; margin: 0;
  padding: 32px 22px;
  text-align: center;
  background: var(--bg2); border: 1px solid var(--border2); border-radius: 20px;
}
.qw-adieu-titre { margin: 0 0 12px; font-size: 26px; line-height: 1.25; font-weight: 800; color: var(--text); }
.qw-adieu-texte { margin: 0 0 24px; font-size: 16px; line-height: 1.55; color: var(--text2); }
/* Le bouton est un .btn-primary (couleur, texte, coins, survol : ceux du site).
   Ici, seulement ce qu'il faut à un <a> pour se tenir comme un bouton : bloc
   centré, sans soulignement, cible de 44 px au moins. Ne JAMAIS y remettre une
   couleur : le bouton cesserait d'être celui du site. */
.qw-adieu-btn {
  box-sizing: border-box; min-height: 44px;
  display: flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.qw-adieu-btn:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
.qw-adieu-pied { padding: 8px 0; text-align: center; font-size: 13px; color: var(--text2); }

/* ══════════════════════════════════════════════════════════════════════════════
   DOCUMENTS LÉGAUX (phase 2c) — page publique « Supprimer mon compte » et lien
   « Versions précédentes »
   ──────────────────────────────────────────────────────────────────────────────
   supprimer-mon-compte.html est GÉNÉRÉE par scripts/build_legal.py depuis
   legal/supprimer-mon-compte.md, avec la feuille des documents légaux (le bloc
   <style> de legal.html, repris tel quel). Ici, seulement ce que cette page ajoute :
   un en-tête (logo + thème), des boutons, un pied. Publique, sans connexion :
   mobile d'abord (§18), zones sûres de l'iPhone, cibles de 44 px au moins.
   ⚠️ Le bloc <style> de la page vient APRÈS style.css : à spécificité égale, il
   gagne. D'où « .doc-view.doc-page-contenu » pour reprendre les marges.
   ══════════════════════════════════════════════════════════════════════════════ */
.doc-page { background: var(--bg); min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
/* legal.html — la barre du haut (« Retour » + thème). Le bouton « Retour » n'avait AUCUNE règle
   (paquet 1, 28-10) : un bouton brut du navigateur, cadre blanc et flèche de 45×69 px. La barre fait
   56 px et colle en haut, comme l'attendent les onglets (.doc-tabs, feuille de la page : top:56px) —
   sans elle, le texte défilait dans le vide au-dessus des onglets. Cible de 44 px (§18). */
.doc-nav { position: sticky; top: 0; z-index: 51; display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 56px; padding: 0 12px; background: var(--bg); border-bottom: 1px solid var(--border); }
.doc-nav .nav-back { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 12px 0 6px; border: 0; border-radius: 8px; background: none; color: var(--text2); font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; transition: color .2s; }
.doc-nav .nav-back svg { width: 18px; height: 18px; flex-shrink: 0; }
.doc-nav .nav-back:hover { color: var(--text); }
.doc-nav .nav-back:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.doc-page-entete {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: max(8px, env(safe-area-inset-top, 0px)) max(16px, env(safe-area-inset-right, 0px)) 8px max(16px, env(safe-area-inset-left, 0px));
  border-bottom: 1px solid var(--border);
}
.doc-page-accueil { display: inline-flex; align-items: center; min-height: 44px; }
/* L'interrupteur du site mesure 52 × 28 : sa zone touchable est agrandie à 44 px
   de haut, sans changer son dessin (le ::after est la pastille). */
.doc-page-theme { position: relative; }
.doc-page-theme::before { content: ''; position: absolute; top: -8px; bottom: -8px; left: -4px; right: -4px; }
.doc-view.doc-page-contenu {
  flex: 1 0 auto; box-sizing: border-box; width: 100%;
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
}
/* ⚠️ Ses sections sont de vraies <section> (§6), et la vitrine a posé des règles
   GLOBALES sur cette balise (padding de 44 à 96 px, max-width 1200, marge auto —
   même piège que #campaignProgress, claude.md §16). Neutralisées ici : la page
   garde le rythme de legal.html (.doc-section, 28 px entre deux sections). */
.doc-page section.doc-section { padding: 0; margin: 0 0 28px; max-width: none; }
.doc-actions { margin: 14px 0 10px; }
/* Bouton principal = .btn-primary du site (couleur, texte, coins, survol) ; ici,
   seulement ce qu'il faut à un <a> pour se tenir comme un bouton. */
.doc-btn {
  box-sizing: border-box; min-height: 48px;
  display: flex; align-items: center; justify-content: center;
  text-align: center; text-decoration: none;
}
.doc-btn--secondaire {
  width: 100%; padding: 12px 20px;
  background: transparent; color: var(--text);
  border: 1px solid var(--border2); border-radius: var(--radius-md);
  font-size: var(--text-base); font-weight: 600;
}
.doc-btn--secondaire:hover { background: var(--bg3); }
.doc-btn:focus-visible, .doc-versions:focus-visible, .doc-page-pied a:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; }
@media (min-width: 769px) { .doc-btn { max-width: 380px; } }
.doc-versions {
  display: inline-flex; align-items: center; min-height: 44px; margin-top: 2px;
  font-size: 12px; font-weight: 600; color: var(--text2);
  text-decoration: underline; text-underline-offset: 3px;
}
.doc-page-pied {
  padding: 12px max(16px, env(safe-area-inset-right, 0px)) max(16px, env(safe-area-inset-bottom, 0px)) max(16px, env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--border);
  text-align: center; font-size: 13px; color: var(--text3);
}
.doc-page-pied a { display: inline-flex; align-items: center; min-height: 44px; }

/* ══════════════════════════════════════════════════════════════════════════
   APP NATIVE (Capacitor, étape 8) — claude.md §18 r. 9, lot 3A
   html.qw-app-native n'est posée que par window.qwAppNative() (shared.js), seul test
   de l'app. Aucun écran de l'app ne mène à la page des tarifs : ses liens sont retirés,
   où qu'ils soient. ⚠️ !important, à dessein : le pied de page d'index.html porte un
   style="display:inline-flex" en attribut, qui battrait toute règle sans lui (cf. 3ter).
   ══════════════════════════════════════════════════════════════════════════ */
html.qw-app-native a[href^="partenaires.html"] { display: none !important; }

/* ══════════════════════════════════════════════════════════════════════════
   CARTES : PHOTO OU LOGO, ET « ✓ TERMINÉE » (paquet cartes, décision du 29-10-2026)
   ──────────────────────────────────────────────────────────────────────────
   shared.js classe chaque image de carte (qwNatureImage) et pose sur la carte
   .qw-img-photo ou .qw-img-logo (+ .qw-img-alpha si l'image a de la transparence,
   .qw-img-absente si elle n'arrive pas). Choix de Jonathan :
     · quête,    LOGO  → le logo ENTIER sur une copie floue de lui-même ;
     · campagne, LOGO  → le logo en petit à côté du titre, sur une couleur unie tirée de
                         l'image (--qw-carte-couleur, assombrie pour le texte blanc) ;
     · les deux, PHOTO → la couverture, centrée, jamais éclaircie, un dégradé sombre
                         derrière chaque texte.
   Le gris des cartes verrouillées du visiteur (#app.anon-rich …) reste plus fort que ces
   règles : il est inchangé.
   ══════════════════════════════════════════════════════════════════════════ */

/* Tant que l'image n'est pas classée, elle ne s'affiche pas : jamais de saut d'un
   traitement à l'autre. (Une image qui n'arrive pas reçoit « absente ».) */
img[data-qw-image]:not([data-qw-nature]) { opacity: 0; }

/* ── Campagne ─────────────────────────────────────────────────────────────── */
/* L'image est une <img> (ses pixels se lisent), en couverture centrée — la place et le
   rôle de l'ancien fond CSS de .card-banner-container. */
.card-banner-img {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover; object-position: center;
}
.ob-camp-titre-ligne { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ob-camp-titre-ligne > .ob-card-title { margin: 0; min-width: 0; }
.ob-camp-logo {
  display: none; flex: 0 0 48px; width: 48px; height: 48px;
  align-items: center; justify-content: center; overflow: hidden;
  border-radius: 12px; background: #0E0E14; box-shadow: 0 4px 12px rgba(0, 0, 0, .35);
}
.ob-camp-logo-img { display: block; width: 100%; height: 100%; object-fit: contain; padding: 5px; box-sizing: border-box; }
/* Un logo transparent (texte noir : Canton, Ville) se lit sur une plaque blanche. */
.qw-img-alpha .ob-camp-logo { background: #FFFFFF; }

/* PHOTO : ni éclaircie ni changement au survol ; un dégradé sous les textes du haut
   (commerce, titre) et du bas (progression, dates), le milieu laissé à la photo. */
.ob-campaign-card.qw-img-photo .card-banner-container,
.ob-card.ob-campaign-card.qw-img-photo:hover .card-banner-container { filter: none; }
.ob-campaign-card.qw-img-photo .card-banner-container::after {
  background: linear-gradient(180deg,
    rgba(8, 8, 14, .86) 0%, rgba(8, 8, 14, .55) 22%, rgba(8, 8, 14, 0) 36%,
    rgba(8, 8, 14, 0) 42%, rgba(8, 8, 14, .78) 54%, rgba(8, 8, 14, .93) 64%, rgba(8, 8, 14, .95) 100%);
}
/* LOGO : la grande image s'efface ; couleur unie tirée de l'image, petit logo au titre. */
.ob-campaign-card.qw-img-logo .card-banner-container,
.ob-card.ob-campaign-card.qw-img-logo:hover .card-banner-container {
  filter: none; background: var(--qw-carte-couleur, var(--bg3));
}
.ob-campaign-card.qw-img-logo .card-banner-img { display: none; }
.ob-campaign-card.qw-img-logo .card-banner-container::after {
  background:
    radial-gradient(120% 90% at 88% 0%, rgba(255, 255, 255, .12), transparent 60%),
    linear-gradient(180deg, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, .28) 100%);
}
.ob-campaign-card.qw-img-logo .ob-camp-logo { display: flex; }
/* Image absente : le repli de toujours (dégradé de marque), sans logo. */
.ob-campaign-card.qw-img-absente .card-banner-img,
.ob-campaign-card.qw-img-absente .ob-camp-logo { display: none; }
.ob-campaign-card.qw-img-absente .card-banner-container {
  background: linear-gradient(135deg, #0E0E14 0%, rgba(255, 107, 43, .12) 55%, #1C1C28 100%);
}
/* Téléphone : les textes de la carte de campagne sont EN BAS (≤ 480 px) — le dégradé suit. */
@media (max-width: 480px) {
  .ob-campaign-card.qw-img-photo .card-banner-container::after {
    background: linear-gradient(180deg,
      rgba(8, 8, 14, .45) 0%, rgba(8, 8, 14, 0) 14%, rgba(8, 8, 14, 0) 24%,
      rgba(8, 8, 14, .72) 40%, rgba(8, 8, 14, .93) 54%, rgba(8, 8, 14, .95) 100%);
  }
}

/* ── Quête ────────────────────────────────────────────────────────────────── */
.qc-plinth-flou { display: none; }
/* PHOTO : la couverture d'aujourd'hui, voile plus franc sous la banderole et le cœur. */
.ob-quest-card.qw-img-photo .qc-plinth:has(.ob-quest-icon-img)::after {
  background: linear-gradient(180deg,
    rgba(8, 8, 14, .62) 0%, rgba(8, 8, 14, 0) 38%, rgba(8, 8, 14, 0) 64%, rgba(8, 8, 14, .5) 100%);
}
/* LOGO : entier (contain), sur sa copie floue ; un logo transparent sur un fond clair. */
.ob-quest-card.qw-img-logo .qc-plinth-flou {
  display: block; position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%;
  object-fit: cover; filter: blur(14px) saturate(1.25) brightness(.55); transform: scale(1.3);
}
.ob-quest-card.qw-img-logo.qw-img-alpha .qc-plinth { background: #ECECF2; }
.ob-quest-card.qw-img-logo.qw-img-alpha .qc-plinth-flou { filter: blur(14px) saturate(1.15); opacity: .55; }
.ob-quest-card.qw-img-logo .qc-plinth:has(.ob-quest-icon-img) .ob-quest-icon-img {
  object-fit: contain; padding: 12px; box-sizing: border-box;
}
.ob-quest-card.qw-img-logo .qc-plinth:has(.ob-quest-icon-img)::after {
  background: linear-gradient(180deg, rgba(8, 8, 14, .45) 0%, rgba(8, 8, 14, 0) 34%);
}

/* ── « ✓ Terminée » — à côté de « Live », sur les deux cartes ─────────────── */
.ob-done-pill {
  display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
  border-radius: 9999px; background: var(--green); color: #06240F;
  font-size: 11px; font-weight: 800; line-height: 1; padding: 4px 10px; white-space: nowrap;
}
.ob-done-check { font-weight: 900; }

/* ── Compteur de participants des cartes de quête ─────────────────────────────
   La carte de quête reste SOMBRE dans les deux thèmes ; le compteur prenait var(--text),
   noir en thème clair : invisible (défaut relevé le 29-10-2026, aussi sur le vrai site). */
.ob-quest-card .ob-pax-block { color: rgba(255, 255, 255, .62); }
.ob-quest-card .ob-pax-block strong { color: #FFFFFF; }
/* Carte de quête étroite (deux par rangée sur téléphone : 138 px à 320, 166 px à 375) : la
   rangée « Live » + « ✓ Terminée » n'a que 116 à 144 px — les deux pastilles s'y resserrent
   pour tenir sur UNE ligne (mesuré, tests/cartes/banc.mjs). Le bureau n'est pas touché. */
@media (max-width: 480px) {
  .ob-quest-card .qc-meta { gap: 4px; }
  .ob-quest-card .qc-meta .ob-status-pill { gap: 4px; padding: 3px 7px; font-size: 10px; }
  .ob-quest-card .qc-meta .ob-status-dot { width: 5px; height: 5px; }
  .ob-quest-card .qc-meta .ob-done-pill { gap: 3px; padding: 4px 7px; font-size: 10px; }
}
@media (max-width: 340px) {
  .ob-quest-card .qc-meta { gap: 3px; }
  .ob-quest-card .qc-meta .ob-status-pill { padding: 3px 6px; }
  .ob-quest-card .qc-meta .ob-done-pill { padding: 4px 6px; }
}

/* Thème clair : le sous-titre de la section d'accueil (« Les quêtes que tu as terminées »…) emprunte
   .gptp-col-sub, pensée pour un panneau SOMBRE (blanc à 45 %) — sur le fond clair, il disparaissait
   (29-10-2026). Le thème sombre n'est pas touché. */
[data-theme="light"] #fqSubtitle { color: var(--text2); }

/* Portail : aide sous un champ (paquet cartes, « Image de la carte »). */
.md-aide { margin: 6px 0 0; font-size: 12px; line-height: 1.5; color: var(--text2); }
