/* ======================================================================================================
   VELVET VAULT — l'identite visuelle de Gemz.
   Conception : l'owner avec Claude Design, remise le 2026-08-23 dans
   gemz-velvet-vault-visual-identity/ (turns 4 et 6 ; le turn 5, mode clair, a ete rejete).

   L'IDEE QUI TIENT TOUT LE SYSTEME : l'ecran est un ecrin de joaillier. Fond prune profond, montures
   en or, et la gemme comme UNIQUE couleur saturee. Si un ecran parait terne, c'est qu'il manque de
   gemmes, pas de couleur d'interface.

   ------------------------------------------------------------------------------------------------
   POURQUOI UNE FEUILLE A PART, CHARGEE EN DERNIER

   Le depot porte 596 declarations `!important` sur trois couches : style.css (221), le bloc inline
   `<style id="redesign-css">` d'index.html (295) et theme-tint.css (80). Une declaration `!important`
   gagne sur toute declaration normale quel que soit l'ordre des feuilles - un skin ecrit « proprement »
   plus bas dans style.css serait donc annule en silence par des regles vieilles de plusieurs versions.
   On charge apres theme-tint.css, et on repond `!important` la ou il le faut, uniquement la.

   CE N'EST PAS UNE REECRITURE. Le CSS existant est massivement pilote par des jetons : 2124 usages de
   `var(--…)`, et toute la peau V2 derive de trois pilotes poses sur `body.ui-v2` - `--h` (teinte),
   `--satk` (saturation), `--lift` (clarte). Repeindre ces trois valeurs deplace des centaines de
   regles d'un coup, sans les toucher. On ne reecrit ensuite QUE ce que le nouveau vocabulaire change
   vraiment : la monture doree remplace le biseau cartoon. C'est un changement de primitive, pas de
   couleur, donc lui doit etre ecrit.

   LA STRUCTURE NE BOUGE PAS - ni l'architecture d'information, ni la navigation, ni l'economie. Le
   handoff l'exige (« implement this as CSS against the existing DOM ; do not port the prototype's
   markup ») et c'est ce qui rend le chantier faisable : chaque element des maquettes correspond a
   quelque chose qui existe deja dans index.html.
   ====================================================================================================== */

/* ======================================================================================================
   1 · LES JETONS
   ====================================================================================================== */
:root {
  /* --- le fond. Le systeme demande un RADIAL prune, sans motif ni texture ------------------------
     `--dp-bg` est consomme par `.ambient` dans le bloc inline, en `!important` : on remplace la
     VALEUR du jeton plutot que d'aller disputer cette regle. */
  --dp-bg-c1: #4B2A6B;
  --dp-bg-c2: #2A1440;
  --dp-bg-c3: #140823;
  --dp-bg: radial-gradient(120% 70% at 50% 0%, var(--dp-bg-c1) 0%, var(--dp-bg-c2) 45%, var(--dp-bg-c3) 100%);

  /* --- l'or. Une seule famille, six paliers : c'est du metal, clair en haut, sombre en bas -------- */
  --vv-gold-1: #FFF3CE;
  --vv-gold-2: #FFE9B0;
  --vv-gold-3: #D9A441;
  --vv-gold-4: #C08C2C;
  --vv-gold-5: #B07E23;
  --vv-gold-6: #7A5312;
  --vv-gold-shadow: #5B3C0C;
  --vv-gold-text: #F2C463;
  --vv-gold-text-hi: #FFE6AE;
  --vv-on-gold: #3A2405;

  /* Le remplissage d'un bouton d'action, distinct des paliers ci-dessus : un bouton est plus clair et
     plus chaud qu'une monture, sinon il se confond avec le cadre qui l'entoure. */
  --vv-btn-1: #FFD983;
  --vv-btn-2: #E4A93A;
  --vv-btn-3: #C98D22;

  /* --- surfaces --------------------------------------------------------------------------------- */
  --vv-panel-hi: #33204C;
  --vv-panel-lo: #1D1032;
  --vv-panel-flat: #1E1031;
  --vv-well: #170C28;

  /* --- filets d'or : une seule teinte, quatre opacites ------------------------------------------- */
  --vv-line-strong: rgba(242, 196, 99, .45);   /* controle actif */
  --vv-line: rgba(242, 196, 99, .30);          /* conteneur */
  --vv-line-dash: rgba(242, 196, 99, .40);     /* emplacement vide */
  --vv-line-locked: rgba(255, 255, 255, .08);  /* verrouille */

  /* --- signal ----------------------------------------------------------------------------------- */
  --vv-gain: #8CE49A;
  --vv-alert: #C4283F;
  --vv-jewel: #E9D6FF;
  --vv-rank-1: #9FD8FF;
  --vv-rank-2: #E9F6FF;
  --vv-gemlvl-1: #C47DFF;
  --vv-gemlvl-2: #EFD6FF;
  --vv-cell: #4D3A86;

  /* --- l'or de marque et le corail du jeu suivent le nouveau vocabulaire -------------------------
     `--gold` est lu par 67 regles, `--coral` porte l'ancien CTA. Les repeindre ici suffit a les
     rallier, sans toucher une seule de ces regles. */
  --gold: var(--vv-gold-text);
  --coral: #C4283F;
  --coral-deep: #8E1B2C;
}

/* LES TROIS PILOTES, repeints en prune. Le bleu de marque etait a 230°, le prune de l'ecrin est a
   275° - et NETTEMENT plus sombre : `--lift` passe de 27 % a 4 %. Ce seul reglage fait basculer tout
   le chassis - cartes, barre du bas, puits, contours - de l'ancien bleu clair au velours profond.

   POURQUOI `!important` SUR DES VARIABLES, ET POURQUOI `:not(.wp-image)`.
   `src/game/19-wallpapers.js` ECRIT ces trois variables EN LIGNE sur `<body>` : il harmonise
   l'interface avec le fond que le joueur a equipe (le catalogue de wallpapers va de 21 % a 80 % de
   luminance, une interface figee y devient illisible). Une declaration en ligne bat toute feuille de
   style - sauf une declaration `!important`, qui est justement la seule facon de reprendre la main.

   Mais on ne la reprend que la ou l'harmonisation n'a plus d'objet : SANS wallpaper image, le fond
   est desormais le velours de l'ecrin, et harmoniser avec l'ancien bleu de marque n'a plus de sens.
   Avec une image derriere, l'harmonisation reste indispensable et garde la main - c'est pour ca que
   la condition porte sur `.wp-image` et pas sur autre chose. */
body.ui-v2:not(.wp-image) {
  --h: 275 !important;
  --satk: .92 !important;
  --lift: 4% !important;
}
body.ui-v2 {
  --board-alpha: .55;
  /* Le contour du chassis n'est plus une encre noire cartoon : il devient de l'or a faible opacite,
     ce qui suffit a rallier au nouveau vocabulaire les dizaines de regles qui le consomment. */
  --card-edge: rgba(242, 196, 99, .30);
  --card-deep: #100620;
}

/* ======================================================================================================
   2 · LE FOND
   Trois couches sur la racine, rien d'autre : degrade, vignette, et c'est tout. Ni motif, ni texture,
   ni trame - le fond est un decor, pas une surface active. La trame de losanges de la V2 est
   deliberement abandonnee ; elle appartenait au fond bleu qu'on remplace.
   ====================================================================================================== */
body:not(.wp-image) .ambient::before { display: none !important; }   /* la trame de losanges */
body:not(.wp-image) .ambient::after {
  background: transparent !important;
  box-shadow: inset 0 0 120px 20px rgba(10, 4, 20, .65) !important;
}
body.ui-v2:not(.wp-image) .blob { display: none !important; }

/* ======================================================================================================
   3 · TYPOGRAPHIE
   Lilita One pour ce que le joueur lit comme une VALEUR, Nunito 900 petites capitales pour ce qui NOMME
   cette valeur. Deux fontes, deux roles, jamais l'inverse.

   Le contour noir empile sur les titres flottants (style.css:79) appartenait a une interface posee sur
   un wallpaper clair ; sur le velours il ne cerne plus rien et alourdit. Il ne tombe QUE lorsque le
   joueur n'a pas de wallpaper image - avec une image derriere, il reste indispensable.
   ====================================================================================================== */
body.ui-v2:not(.wp-image) .view-title,
body.ui-v2:not(.wp-image) .hud-value,
body.ui-v2:not(.wp-image) .hud-label,
body.ui-v2:not(.wp-image) .mode-caption,
body.ui-v2:not(.wp-image) .shop-section,
body.ui-v2:not(.wp-image) .shop-sec-title,
body.ui-v2:not(.wp-image) .sec-title,
body.ui-v2:not(.wp-image) .view-sub,
body.ui-v2:not(.wp-image) .coll-sec-head,
body.ui-v2:not(.wp-image) .rotation-note,
body.ui-v2:not(.wp-image) .dl-note {
  text-shadow: none !important;
  -webkit-text-stroke: 0 !important;
}
body.ui-v2 .view-title, body.ui-v2 .panel-title {
  letter-spacing: .14em !important;
  color: #fff !important;
}
body.ui-v2 .coll-sec-head, body.ui-v2 .sec-title {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif !important;
  letter-spacing: .09em !important;
  color: #fff !important;
  opacity: 1 !important;
}

/* ======================================================================================================
   4 · LA MONTURE — la primitive centrale
   Un enrobage de 1,5px en degrade d'or autour d'un panneau sombre. C'est un SERTISSAGE, pas une
   bordure : clair en haut, sombre en bas, comme du metal.

   Le systeme V2 posait un biseau « cartoon » - contour d'encre de 3px, levre pleine de 5px, relief
   interne. On le remplace entierement : ce sont deux vocabulaires differents, et les melanger donnerait
   un objet qui ne ressemble ni a l'un ni a l'autre.

   Le handoff signale un piege sur cette primitive : realisee avec deux elements imbriques, elle laisse
   une bande d'or nue des que le panneau interieur se dimensionne a son contenu dans une grille etiree.
   On l'evite en la realisant avec `background-clip`, qui ne demande aucun second element : le degrade
   d'or est peint sur la BORDURE, la surface sombre sur la boite de padding.
   ====================================================================================================== */
body.ui-v2 .card,
body.ui-v2 .pod,
body.ui-v2 .badge-box,
body.ui-v2 .raised,
body.ui-v2 .panel {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 .card, body.ui-v2 .pod, body.ui-v2 .badge-box, body.ui-v2 .raised { border-radius: 18px !important; }
body.ui-v2 .panel { border-radius: 22px !important; box-shadow: 0 30px 60px rgba(0, 0, 0, .6) !important; }

/* Un conteneur secondaire ne porte pas de monture : un simple filet d'or sur un creux. L'or est un
   signal de valeur, en distribuer partout le vide de son sens. */
body.ui-v2 .seg-row,
body.ui-v2 .coll-cat {
  background: rgba(0, 0, 0, .28) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
}
/* UNE LIGNE DE REGLAGE N'EST PAS UN CONTENANT. Je lui avais donne le filet d'or des conteneurs :
   chaque ligne se retrouvait dans son propre cadre, alors qu'elle contient deja un controle qui
   porte le sien (le curseur, le bouton OUI, la liste de langues). Deux cadres emboites pour une
   seule information. La maquette ne met de filet que sur les CONTROLES ; les lignes, elles, se
   separent par un simple trait. */
body.ui-v2 .setting-row {
  background: transparent !important;
  /* AUCUN TRAIT NON PLUS (owner, 2026-08-25). J'avais remplace le cadre de chaque ligne par un filet
     de separation ; il reste un trait de trop. Les lignes se separent deja par leur espacement, et
     chaque controle porte son propre contour - ajouter une regle horizontale, c'est redire une
     troisieme fois ce que la mise en page dit deja. */
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}

/* Un CREUX : la main, les emplacements d'ecrin, le loadout. Ils s'enfoncent, ils ne se soulevent pas. */
body.ui-v2 .recessed,
body.ui-v2 .tray-slot,
body.ui-v2 .hold-slot,
body.ui-v2 .loadout-slot,
body.ui-v2 .gem-slot {
  background: rgba(0, 0, 0, .30) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .55) !important;
}
/* `.slot-empty` a ete RETIRE de cette liste. Il ne portait aucune bordure ; poser `border-style`
   seul en fait apparaitre une, parce que la largeur par defaut d'une bordure est `medium` (~3px) et
   non zero. Resultat : un second rectangle pointille A L'INTERIEUR de l'emplacement d'ecrin, autour
   de « Gagne des niveaux pour obtenir des ecrins ! », alors que l'emplacement lui-meme est deja en
   pointilles. Deux cadres pour un seul etat vide.
   Les trois autres selecteurs recoivent bien une bordure de la regle CREUX juste au-dessus : sur eux
   `border-style` change une bordure existante au lieu d'en creer une. */
body.ui-v2 .hold-slot:not(.filled),
body.ui-v2 .loadout-slot.empty,
body.ui-v2 .gem-slot.empty {
  border-style: dashed !important;
  border-color: var(--vv-line-dash) !important;
}
/* Verrouille : le filet devient blanc et sourd. La rupture de couleur dit « pas encore a toi » sans
   qu'on ait besoin de masquer la condition d'ouverture, qui doit rester lisible.
   `.slot-lock` a ete RETIRE : c'est une IMAGE de cadenas, sans bordure. Comme pour `.slot-empty`,
   poser `border-style` seul en fabriquait une (largeur par defaut `medium`, ~3px) - un petit carre
   pointille apparaissait donc autour de chaque cadenas, dans un emplacement deja en pointilles. */
body.ui-v2 .gem-slot.locked {
  border-color: var(--vv-line-locked) !important;
  border-style: dashed !important;
}

/* ======================================================================================================
   5 · BOUTONS
   L'or dit « ceci a de la valeur » ou « ceci est l'action principale ». UN SEUL or par ecran.
   L'ombre est DURE et proportionnelle a la hauteur du bouton ; au press, le bouton descend exactement
   de cette valeur et l'ombre passe a zero, en 90 ms : le doigt enfonce un objet reel.
   ====================================================================================================== */
body.ui-v2 .btn {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  border-radius: 16px !important;
  color: var(--vv-gold-text) !important;
  text-shadow: none !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .45), 0 9px 16px rgba(0, 0, 0, .38) !important;
  transition: transform 90ms ease-out, box-shadow 90ms ease-out !important;
}
body.ui-v2 .btn:active {
  transform: translateY(4px) !important;
  box-shadow: 0 0 0 rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .38) !important;
  animation: none !important;   /* la V2 animait le press (`newPress`) ; la nouvelle course est plus courte */
}

/* L'ACTION PRINCIPALE : or plein, ombre de 8px. Une par ecran.
   `.btn-primary` etait corail - c'etait LE bouton d'appel du jeu, il devient donc le bouton d'or. */
body.ui-v2 .btn-primary,
body.ui-v2 .btn-gold {
  background:
    linear-gradient(var(--vv-btn-1), var(--vv-btn-2) 60%, var(--vv-btn-3)) padding-box,
    linear-gradient(var(--vv-gold-1), var(--vv-gold-3) 50%, var(--vv-gold-5)) border-box !important;
  border: 2px solid transparent !important;
  color: var(--vv-on-gold) !important;
  text-shadow: 0 1px 0 rgba(255, 255, 255, .45) !important;
  box-shadow: 0 8px 0 var(--vv-gold-shadow), 0 16px 28px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 .btn-primary:active,
body.ui-v2 .btn-gold:active {
  transform: translateY(8px) !important;
  box-shadow: 0 0 0 var(--vv-gold-shadow), 0 4px 10px rgba(0, 0, 0, .45) !important;
}

/* SECONDAIRE : ni or plein, ni monture pleine - un filet, et le texte en or clair. */
body.ui-v2 .btn-ghost,
body.ui-v2 .btn-ghost2,
body.ui-v2 .btn-mini {
  background: rgba(0, 0, 0, .26) !important;
  border: 1.5px solid var(--vv-line) !important;
  color: var(--vv-gold-text) !important;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .40) !important;
  border-radius: 14px !important;
}
body.ui-v2 .btn-ghost:active,
body.ui-v2 .btn-ghost2:active,
body.ui-v2 .btn-mini:active { transform: translateY(3px) !important; box-shadow: 0 0 0 rgba(0, 0, 0, .40) !important; }

/* DESTRUCTIF / REFUS : la seule autre couleur pleine du systeme, et elle ne sert qu'a ca. */
body.ui-v2 .btn-coral {
  background: linear-gradient(var(--vv-alert), #8E1B2C) padding-box,
              linear-gradient(#E45268, #7A1524) border-box !important;
  border: 1.5px solid transparent !important;
  color: #fff !important;
  box-shadow: 0 5px 0 #5E101E, 0 10px 18px rgba(0, 0, 0, .4) !important;
}
body.ui-v2 .btn-coral:active { transform: translateY(5px) !important; box-shadow: 0 0 0 #5E101E !important; }

/* Indisponible : on retire le reflet et on desature. On ne retire JAMAIS le texte de la condition. */
body.ui-v2 .btn[disabled], body.ui-v2 .btn:disabled {
  filter: grayscale(.65) brightness(.75) !important;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .35) !important;
}

/* ======================================================================================================
   6 · EN-TETE ET PORTEFEUILLE
   Navigation a gauche, monnaies au centre en pastilles egales, avatar a droite dans son cerceau d'or.
   ====================================================================================================== */
body.ui-v2 .chip {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  border-radius: 999px !important;
  box-shadow: none !important;
  color: var(--vv-gold-text-hi) !important;
}
/* Deux monnaies, deux couleurs : la piece se lit en or, le joyau en lilas. C'est le meme creneau
   visuel que les pierres de la collection, donc il doit s'en distinguer d'un coup d'oeil. */
body.ui-v2 #jewelChip, body.ui-v2 .jewel-chip { color: var(--vv-jewel) !important; }

body.ui-v2 .topbar-btn, body.ui-v2 button#btnBack {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  border-radius: 13px !important;
  box-shadow: none !important;
}
/* L'AVATAR : un cerceau d'or conique - le seul degrade conique du systeme. Il tourne comme du metal
   poli, et signale que ce disque est une personne, pas un bouton. */
body.ui-v2 .topbar-profile .avatar-pic {
  box-shadow: 0 0 0 2.5px var(--vv-gold-4), 0 0 0 3.5px rgba(0, 0, 0, .35) !important;
}

/* ======================================================================================================
   7 · BARRE DE NAVIGATION
   L'onglet actif se souleve dans un disque serti : le SEUL or de la barre.
   ====================================================================================================== */
body.ui-v2 #navBar::before {
  background: linear-gradient(#241539, var(--vv-well)) !important;
  border-top: 1.5px solid var(--vv-line-strong) !important;
  box-shadow: 0 -10px 26px rgba(0, 0, 0, .5) !important;
}
/* PAS DE CASSURE NETTE ENTRE LA PAGE ET LE MENU (owner 2026-09-06 : « ca a l'air de fonctionner sur
   desktop mais ca ne fonctionne pas sur iPhone »).

   Le fondu existait deja, mais comme un MASQUE pose sur quatre vues nommees une par une
   (#view-collection / #view-board / #view-goals / #view-shop, style.css). Il ne couvre donc ni le
   Profil ni ses panneaux - et c'est justement le classement du Profil que la capture montre, coupe
   net au milieu d'une ligne. Sur bureau la liste tient sans deborder : rien a couper, rien a voir.
   D'ou « ca marche sur desktop » : ce n'etait pas une difference de plateforme, mais de longueur de
   liste. Une enumeration de vues ne peut pas etre exhaustive, et l'oubli y est silencieux.

   On renverse la charge : le fondu appartient a la BARRE, qui est unique. Un voile degrade aux
   couleurs de la barre monte au-dessus d'elle et eteint ce qui passe dessous - quelle que soit la
   vue, sans dependre de `mask-image`, et sans rien a reposer sur le prochain ecran ajoute.
   Deux details qui ne se devinent pas en relisant :
     · `z-index: -1` : `::after` est le DERNIER enfant de la barre, il peindrait donc PAR-DESSUS
       l'onglet actif, qui se souleve de 10 px hors du bandeau. En negatif il repasse dessous, tout en
       restant dans la pile de #navBar (z-index 5) - donc toujours au-dessus du contenu de la page.
     · `bottom: calc(100% - 9px)` : la surface opaque (`::before`) commence 9 px SOUS le haut de
       l'element. Le voile doit finir la, pas 9 px plus haut, sinon il reste une marche.
   L'accueil est exclu : il ne defile pas, rien n'y est coupe, et son bouton JOUER tombe pile dans la
   bande - le voile l'assombrirait pour rien. */
body.ui-v2:not([data-view="home"]) #navBar::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: calc(100% - 9px); height: 34px;
  z-index: -1; pointer-events: none;
  /* owner 2026-09-06 : « il est trop large sur les autres ecrans, rends-le plus discret, plus fin en
     hauteur ». 58 -> 34 px. Le voile n'a plus a faire disparaitre une liste a lui seul - c'est le
     masque du conteneur qui defile qui s'en charge, la ou il y en a un (ci-dessous, et style.css) -
     il ne fait plus qu'adoucir le BORD de la barre. Un role, une hauteur. */
  background: linear-gradient(180deg, rgba(23, 12, 40, 0) 0%, rgba(23, 12, 40, .42) 46%, #241539 100%);
}
/* L'ONGLET INACTIF RESTE EN COULEUR (owner 2026-08-27, revenant sur sa demande precedente).

   Deux tentatives ont echoue, et pour la meme raison. `opacity: .58` posait un voile ; `grayscale(1)`
   a supprime le voile mais a introduit pire : le gris est le signe universel du DESACTIVE. Les quatre
   onglets non selectionnes avaient l'air de pages fermees alors qu'ils sont tous accessibles a tout
   instant - un signal FAUX vaut moins que pas de signal du tout.

   On ne marque donc plus l'inactif : c'est l'ACTIF qui se distingue, par trois marqueurs cumules - il
   se souleve de 12 px, il prend un disque serti d'or, son libelle passe a l'or. Trois signaux
   positifs, sur un fond de pairs identiques, se lisent mieux que quatre signaux negatifs.

   Consequence voulue, et demandee : les pastilles de notification gardent leur rouge. Elles vivent
   DANS le bouton, donc tout filtre pose sur `.nav-item` les emportait - c'est ce qui les grisait, et
   une notification grise n'appelle plus personne. */
body.ui-v2 .nav-label { letter-spacing: .10em !important; color: #fff !important; }
body.ui-v2 #navBar .nav-item.active .nav-label { color: var(--vv-gold-text) !important; }
body.ui-v2 #navBar .nav-item.active { transform: translateY(-10px) scale(1.06) !important; }
body.ui-v2 #navBar .nav-item.active .nav-ic {
  padding: 3px !important;
  background:
    linear-gradient(#3D2758, #221238) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 50%, var(--vv-gold-6)) border-box !important;
  border: 2px solid transparent !important;
  border-radius: 50% !important;
  box-shadow: 0 6px 14px rgba(0, 0, 0, .55) !important;
  box-sizing: content-box !important;
}

/* ======================================================================================================
   8 · ONGLETS
   La pastille active glisse en 180 ms.
   ====================================================================================================== */
body.ui-v2 .seg {
  color: rgba(255, 255, 255, .55) !important;
  background: transparent !important;
  box-shadow: none !important;
  text-shadow: none !important;
  transition: color 180ms cubic-bezier(.4, 0, .2, 1), background 180ms cubic-bezier(.4, 0, .2, 1) !important;
}
body.ui-v2 .seg.on {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  color: var(--vv-on-gold) !important;
  box-shadow: 0 2px 0 var(--vv-gold-shadow) !important;
}

/* ======================================================================================================
   9 · PROGRESSION
   Le puits est toujours le meme creux sombre ; seul le remplissage change, et sa couleur dit ce qu'il
   mesure : bleu pour le rang, violet pour le niveau d'une gemme, or pour l'objectif d'une partie.
   ====================================================================================================== */
body.ui-v2 .bar, body.ui-v2 .rush-bar, body.ui-v2 .gem-bar, body.ui-v2 .ach-prog {
  background: var(--vv-well) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7) !important;
}
body.ui-v2 .bar > i, body.ui-v2 .rush-bar > i {
  background: linear-gradient(var(--vv-rank-2), var(--vv-rank-1)) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55) !important;
}
body.ui-v2 .gem-bar > i {
  background: linear-gradient(90deg, var(--vv-gemlvl-1), var(--vv-gemlvl-2)) !important;
}

/* ======================================================================================================
   10 · CARTE D'OBJET
   Trois couches, toujours dans cet ordre : halo de rarete, objet, deux lignes de texte. JAMAIS de motif
   derriere un objet - l'objet est le sujet.

   La rarete se dit sur TROIS marches, jamais par la seule teinte : le halo la colore, l'etiquette la
   nomme, et a partir de legendaire la monture passe a l'or plein. `--rc` porte deja la teinte par
   rarete dans le CSS existant ; on la reprend telle quelle au lieu d'ouvrir une seconde table.
   ====================================================================================================== */
body.ui-v2 .gemcard, body.ui-v2 .pucard {
  background:
    radial-gradient(120% 95% at 50% 12%, color-mix(in srgb, var(--rc) 22%, transparent), rgba(30, 16, 49, .94) 72%),
    var(--vv-panel-flat) padding-box,
    linear-gradient(rgba(242, 196, 99, .45), rgba(176, 126, 35, .35)) border-box !important;
  border: 1.5px solid transparent !important;
  border-radius: 16px !important;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .4) !important;
}
/* Legendaire et au-dessus : monture d'or PLEINE. C'est la troisieme marche du signal de rarete. */
body.ui-v2 .gemcard.rar-legendaire,
body.ui-v2 .gemcard.rar-exotique,
body.ui-v2 .gemcard.rar-mythique,
body.ui-v2 .gemcard.equipped {
  background:
    radial-gradient(120% 95% at 50% 12%, color-mix(in srgb, var(--rc) 26%, transparent), rgba(30, 16, 49, .94) 72%),
    var(--vv-panel-flat) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 52%, var(--vv-gold-4)) border-box !important;
  box-shadow: 0 0 18px color-mix(in srgb, var(--rc) 30%, transparent), 0 6px 18px rgba(0, 0, 0, .4) !important;
}
/* Non decouverte : des tirets, jamais de monture - on ne sertit pas ce qu'on ne possede pas. */
body.ui-v2 .gemcard.is-locked, body.ui-v2 .pucard.is-locked {
  background: rgba(0, 0, 0, .24) !important;
  border: 1.5px dashed var(--vv-line-locked) !important;
  box-shadow: none !important;
  opacity: 1 !important;
}
body.ui-v2 .prev-name { color: #fff !important; }
body.ui-v2 .gemcard.is-locked .prev-name,
body.ui-v2 .pucard.is-locked .prev-name { color: rgba(255, 255, 255, .45) !important; }
body.ui-v2 .gem-lvl { color: rgba(255, 255, 255, .55) !important; }
body.ui-v2 .gemcard.equipped .gem-lvl { color: var(--vv-gold-text) !important; }

/* ======================================================================================================
   11 · LE PLATEAU
   L'interieur de l'ecrin. Les gemmes y sont la seule source de lumiere : la case vide est un creux
   quasi noir, et rien autour d'elle ne doit rivaliser avec la pierre qu'on y pose.
   ====================================================================================================== */
body.ui-v2 #boardWrap {
  border: 1.5px solid transparent !important;
  border-radius: 22px !important;
  background:
    linear-gradient(#1E1032, #180C29) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .5) !important;
}
/* `:not(.filled)` N'EST PAS UN DETAIL, c'est la correction d'une regression que j'ai introduite et
   que le joueur a vue avant moi : `background` est un RACCOURCI, il remet donc `background-color` a
   sa valeur initiale. Pose en `!important` sur `.cell`, il battait le `background-color: var(--c)`
   de `.cell.filled` (sans `!important`) et TOUS les blocs poses devenaient invisibles - il ne
   restait que leur contour, qui vient d'une autre propriete.
   Aucune de mes verifications ne pouvait le montrer : le harnais de captures photographie le plateau
   juste apres `PP.newGame()`, donc toujours VIDE. C'est reparable une fois, mais l'instrument l'etait
   aussi : la scene « plateau rempli » existe desormais dans `scripts/captures-scenes.mjs`. */
body.ui-v2 .cell:not(.filled) {
  background: rgba(255, 255, 255, .045) !important;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5) !important;
}
body.ui-v2 .cell { border-radius: 6px !important; }
/* Une case pleine porte une facette eclairee en haut et une arete portee en bas : c'est ce qui la fait
   lire comme de la pierre taillee plutot que comme un carre de couleur. Le contour reste SA propre
   teinte assombrie, jamais du noir - la regle V2 avait raison sur ce point, on la garde. */
body.ui-v2 .cell.filled {
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .30), inset 0 -4px 0 rgba(0, 0, 0, .30) !important;
}
/* L'apercu de depose : de l'or transparent, cerne de tirets. */
body.ui-v2 .cell.ghost, body.ui-v2 .cell.preview {
  background: rgba(242, 196, 99, .22) !important;
  outline: 1px dashed var(--vv-line-dash);
  outline-offset: -1px;
}

/* ======================================================================================================
   12 · MOUVEMENT
   Deux animations decoratives au plus, visibles en meme temps. L'ornement ne porte JAMAIS l'etat.
   ====================================================================================================== */
@keyframes vv-sheen {
  0%, 62% { transform: translateX(-140%) skewX(-18deg); }
  100%    { transform: translateX(420%) skewX(-18deg); }
}
/* Le reflet ne se pose que sur l'action principale de l'accueil : un seul par region. */
body.ui-v2 #btnPlay { position: relative; overflow: hidden; }
body.ui-v2 #btnPlay::after {
  content: '';
  position: absolute; top: 0; bottom: 0; left: 0; width: 55px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .5), transparent);
  animation: vv-sheen 4.5s ease-in-out infinite;
  pointer-events: none;
}

/* L'ornement disparait, l'etat reste. L'enfoncement, le glissement d'onglet et le remplissage des
   barres portent un etat : ils survivent. Les reflets, eux, sont decoratifs. */
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 #btnPlay::after { display: none !important; }
}

/* ======================================================================================================
   13 · LES SURFACES CLAIRES DU SKIN V1
   Le jeu porte, par-dessus la peau V2, un skin « clair » ecrit dans le bloc inline d'index.html : des
   cartes blanches a texte encre. Ces regles sont marquees `!important` et gagnent donc sur tout le
   reste ; les repeindre demandait de les identifier une par une.

   Elles ont ete RELEVEES, pas devinees : `scripts/audit-skin-surfaces.mjs` ouvre chaque scene dans un
   telephone virtuel, lit la couleur de fond CALCULEE de chaque element visible et remonte ceux qui
   restent clairs. Dix-sept selecteurs, sur dix-sept ecrans. Rejouer ce script apres coup doit rendre
   une liste vide - c'est le test de cette section.

   LE TEXTE SUIT LA SURFACE, ET IL LE FAIT TOUT SEUL : `--dp-ink` est l'encre sombre de ce skin, lue
   par 142 regles. Plutot que de repeindre 142 regles, on redefinit le JETON sur le conteneur qu'on
   assombrit - toute sa descendance herite de la nouvelle valeur, sans qu'aucune de ces regles soit
   touchee. Meme geste pour `--dp-depth`, le biseau clair du meme skin.
   ====================================================================================================== */

/* --- LE PORTEFEUILLE ---------------------------------------------------------------------------
   Deux pastilles blanches en haut de CHAQUE ecran : c'etait le reste de skin clair le plus visible
   du jeu. Le selecteur d'origine est `#topbar .coin-chip` en `!important` ; on le bat en ajoutant
   `body.ui-v2` devant, ce qui suffit et n'elargit pas la portee. */
body.ui-v2 #topbar .coin-chip,
body.ui-v2 #topbar .jewel-chip {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .40), 0 8px 14px rgba(0, 0, 0, .35) !important;
}
body.ui-v2 #topbar .coin-chip > span { color: var(--vv-gold-text-hi) !important; }
body.ui-v2 #topbar .jewel-chip > span { color: var(--vv-jewel) !important; }

/* --- LE BANDEAU DE RANG DE L'ACCUEIL ------------------------------------------------------------
   La plus grande surface claire du jeu, et celle qui porte l'ecusson : elle doit devenir l'ecrin de
   cet ecusson, pas un rectangle blanc pose dessus. */
body.ui-v2 .home-lv-xp {
  --dp-ink: #fff;
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45) !important;
  color: #fff;
}
body.ui-v2 .home-lv-xp .home-lv-cap { color: var(--vv-gold-text) !important; }
body.ui-v2 .home-lv-xp .hlv-xpbar { background: var(--vv-well) !important; }
body.ui-v2 .home-lv-xp .hlv-xpbar i { background: linear-gradient(var(--vv-rank-2), var(--vv-rank-1)) !important; }
body.ui-v2 .home-lv-xp .hlv-xptxt { color: rgba(255, 255, 255, .6) !important; }
body.ui-v2 .home-lv-xp::after { color: var(--vv-gold-text) !important; }

/* --- L'EMPLACEMENT D'ECRIN (accueil) -----------------------------------------------------------
   Il ne suit pas la gamme des cartes : c'est un COFFRE, il porte donc la monture pleine. Vide ou
   verrouille, il redevient un creux en tirets : on ne sertit pas ce qu'on ne possede pas. */
body.ui-v2[data-view="home"] .box-slot {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45) !important;
}
body.ui-v2[data-view="home"] .box-slot.is-empty,
body.ui-v2[data-view="home"] .box-slot.is-locked {
  background: rgba(0, 0, 0, .26) !important;
  border: 1.5px dashed var(--vv-line-dash) !important;
  box-shadow: none !important;
}
/* Pret a ouvrir : l'or RAYONNE au lieu de simplement cerner. C'est le seul halo anime du jeu, et il
   ne signale qu'une chose - une recompense qui attend. */
body.ui-v2[data-view="home"] .box-slot.is-ready {
  box-shadow: 0 0 26px 4px rgba(242, 196, 99, .55), 0 6px 18px rgba(0, 0, 0, .45) !important;
}

/* --- LA BOUTIQUE -------------------------------------------------------------------------------- */
body.ui-v2 .shop-box-tile,
body.ui-v2 .shop-deal-tile {
  --dp-ink: #fff;
  --dp-depth: rgba(0, 0, 0, .5);
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45) !important;
  color: #fff !important;
}
/* Les deux tuiles sans prix gardent leur exception : elles doivent se voir a travers la grille.
   owner 2026-08-30 : elles ont ECHANGE leur contrepartie - la case 0 ne demande plus de pub, la case
   CADEAU en demande une. Les couleurs suivent, sinon elles diraient le contraire du bouton : le VERT
   (la couleur du bouton « gratuit » partout dans le jeu) va desormais a ce qui ne coute rien, et l'OR,
   qui a toujours signale la pub dans cette grille, part avec elle sur la case cadeau. */
body.ui-v2 .shop-deal-tile.deal-free-tile {
  background:
    linear-gradient(#16321A, #0B1E0D) padding-box,
    linear-gradient(#9BE07A, #4FA83C 50%, #2E7226) border-box !important;
  border: 2px solid transparent !important;
}
body.ui-v2 .shop-name { color: #fff !important; }

/* --- OBJECTIFS ET DEFIS -------------------------------------------------------------------------- */
body.ui-v2 .ach-row,
body.ui-v2 .daily-card,
body.ui-v2 #view-goals .daily-featured .daily-card,
body.ui-v2 .daily-featured,
body.ui-v2 .streak-widget {
  --dp-ink: #fff;
  --dp-depth: rgba(0, 0, 0, .5);
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: 0 5px 14px rgba(0, 0, 0, .4) !important;
  color: #fff !important;
}
/* A RECLAMER : la monture d'or pleine. C'est le seul etat de cet ecran qui appelle un geste. */
body.ui-v2 .ach-row.ready,
body.ui-v2 .daily-card.ready {
  border-color: transparent !important;
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  box-shadow: 0 0 18px rgba(242, 196, 99, .35), 0 5px 14px rgba(0, 0, 0, .4) !important;
}
/* La serie de jours : un chapelet de creux, dont le prochain seul s'allume. */
body.ui-v2 .streak-pip {
  --dp-ink: #fff;
  background: rgba(0, 0, 0, .32) !important;
  border: 1.5px solid var(--vv-line-locked) !important;
  color: #fff !important;
}
body.ui-v2 .streak-pip.done { border-color: var(--vv-line) !important; }
body.ui-v2 .streak-pip.next {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  border-color: var(--vv-gold-2) !important;
  --dp-ink: var(--vv-on-gold);
  color: var(--vv-on-gold) !important;
}

/* --- PROFIL -------------------------------------------------------------------------------------- */
body.ui-v2 #view-profile .pstat {
  --dp-ink: #fff;
  background: rgba(0, 0, 0, .30) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
  color: #fff !important;
}
/* Connexion sociale : la marque du fournisseur vit dans son logo, pas dans le fond du bouton. Un
   bouton blanc par service ferait trois taches claires sur l'ecran le plus sombre du jeu. */
body.ui-v2 #view-profile .btn-sso {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line) !important;
  color: #fff !important;
  -webkit-text-stroke: 0 !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .45), 0 8px 14px rgba(0, 0, 0, .35) !important;
}
/* Le crayon de l'avatar etait corail - il devient or : c'est une invitation a agir, pas une alerte. */
body.ui-v2 .avatar-edit {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  border-color: var(--vv-gold-2) !important;
}
body.ui-v2 .avatar-edit svg { fill: var(--vv-on-gold) !important; }

/* --- L'ECHELLE DES NIVEAUX ----------------------------------------------------------------------
   Le seul ecran ou la couleur code une INFORMATION (fait / en cours / a venir). On garde ce codage
   et on le retranspose : le noeud franchi passe a l'or, celui du moment reste bleu, la suite est
   sourde. La carte, elle, rejoint la gamme des autres cartes du jeu. */
body.ui-v2 .rt-card {
  --dp-ink: #fff;
  --dp-depth: rgba(0, 0, 0, .5);
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: 0 5px 14px rgba(0, 0, 0, .42) !important;
  color: #fff !important;
}
body.ui-v2 .rt-row.next .rt-card {
  border-color: transparent !important;
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
}
body.ui-v2 .rt-node {
  background: var(--vv-panel-hi) !important;
  border: 2px solid var(--vv-line) !important;
  color: rgba(255, 255, 255, .8) !important;
  -webkit-text-stroke: 0 !important;
  box-shadow: 0 3px 7px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 .rt-row.done .rt-node {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  border-color: var(--vv-gold-2) !important;
  color: var(--vv-on-gold) !important;
}
body.ui-v2 .rt-row.next .rt-node {
  background: linear-gradient(var(--vv-rank-2), var(--vv-rank-1)) !important;
  border-color: #fff !important;
  color: #123049 !important;
}
body.ui-v2 .route-rankpill {
  --dp-ink: #fff;
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  box-shadow: 0 6px 16px rgba(0, 0, 0, .5) !important;
  color: #fff !important;
}
body.ui-v2 .route-rankpill .rp-txt span { color: rgba(255, 255, 255, .6) !important; }

/* --- DIVERS -------------------------------------------------------------------------------------- */
body.ui-v2 .mode-menu {
  --dp-ink: #fff;
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  box-shadow: 0 14px 30px rgba(0, 0, 0, .55) !important;
  color: #fff !important;
}
/* MA ligne dans le classement : elle se distingue par l'or, comme tout ce qui me concerne. */
body.ui-v2 #profilePaneBoard #boardList > li.me {
  --dp-ink: #fff;
  background: linear-gradient(#3A2A12, #241804) !important;
  border: 2px solid var(--vv-gold-3) !important;
  box-shadow: 0 5px 16px rgba(242, 196, 99, .30) !important;
  color: #fff !important;
}
/* La barre d'emplacements de gemmes : un CONTENANT, donc un creux, pas une surface vive. */
body.ui-v2 .gem-slots-bar {
  background: rgba(0, 0, 0, .28) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
}

/* ======================================================================================================
   14 · LA FAMILLE « SURFACE CLAIRE »
   Le skin V1 ne peint pas ses cartes blanches une par une : il les fait toutes descendre de trois
   jetons - `--surface-lite` (le fond), `--lite-edge` (le contour) et `--ink-on-light` (l'encre) - lus
   par 37 regles reparties sur les trois feuilles.

   Les repeindre ici retourne ces 37 regles d'un coup, et surtout retourne celles qu'aucun audit ne
   verra jamais : les etats qui ne s'affichent qu'a un moment precis d'une partie. Chasser les cartes
   une a une aurait laisse ces cas-la en blanc.

   L'ENCRE SUIT LA SURFACE. « Sur fond clair » devient « sur fond serti » : l'encre passe donc au
   blanc et son contour a l'encre sombre - c'est-a-dire exactement le couple deja utilise sur fond
   sombre. On echange les deux valeurs plutot que d'en inventer de nouvelles.

   `--dp-ink` NE BOUGE PAS, et c'est deliberé : ce jeton sert aussi de couleur de CONTOUR au texte
   blanc pose sur une image (regle du kit de design, en tete d'index.html). Le passer au blanc
   effacerait ces contours en meme temps qu'il reparerait les cartes. Il est donc redefini au cas par
   cas, sur le conteneur qu'on assombrit - c'est ce que fait la section 13.
   ====================================================================================================== */
body.ui-v2 {
  --surface-lite: #241436;
  --lite-edge: rgba(242, 196, 99, .32);
  --ink-on-light: #fff;
  --stroke-on-light: rgba(4, 2, 16, .9);
  --dp-surface: #241436;
  --dp-depth: rgba(0, 0, 0, .45);
  --dp-depth-strong: rgba(0, 0, 0, .65);
  --dp-ambient: rgba(0, 0, 0, .45);
}
/* Le bouton « remonter » de l'echelle code son fond en blanc pur, hors de la famille ci-dessus. */
body.ui-v2 .route-up, body.ui-v2 #ovRoute .route-up {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  -webkit-text-stroke: 0 !important;
  box-shadow: 0 5px 12px rgba(0, 0, 0, .5) !important;
}

/* ======================================================================================================
   15 · DERNIERS RESTES DU SKIN CLAIR
   Trois surfaces que l'audit ne voit pas - il ne lit que la couleur de FOND, or celles-ci se peignent
   par une bordure, une image ou un degrade.
   ====================================================================================================== */

/* Le bouton RETOUR etait un disque blanc sur tous les ecrans sauf l'accueil : la tache la plus claire
   de l'interface, pour l'action la moins importante de l'ecran. */
body.ui-v2:not([data-view="home"]) #btnBack {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .45), 0 8px 14px rgba(0, 0, 0, .35) !important;
}
/* L'AVATAR n'a PLUS de bordure doree (owner 2026-08-30, cf. la note detaillee plus bas) : elle rendait
   le cadre Or invisible en tant que recompense. Le disque se distingue par sa forme et son portrait. */
body.ui-v2 #topbar #btnProfile .avatar-pic {
  border: 0 !important;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5), 0 2px 7px rgba(0, 0, 0, .45) !important;
}
/* Le ruban de section etait corail. Or : c'est une mise en avant, pas une alerte - et le corail est
   desormais reserve au refus et a la perte. */
body.ui-v2 .featured-ribbon, body.ui-v2 .featured-ribbon.ribbon-coral {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  color: var(--vv-on-gold) !important;
  text-shadow: none !important;
  box-shadow: 0 3px 0 var(--vv-gold-shadow) !important;
}
body.ui-v2 .daily-featured .featured-ribbon { color: var(--vv-on-gold) !important; }

/* UN SEUL OR PAR ECRAN. Sur l'accueil, c'est JOUER. Le prix d'ouverture d'un emplacement d'ecrin
   portait le meme or plein juste au-dessus : deux appels de meme force, donc aucun des deux ne
   ressortait. Il redescend d'un cran - filet et texte dores, fond sombre : il reste lisible comme une
   depense, il ne dispute plus l'action principale. */
body.ui-v2[data-view="home"] .box-slot .btn-gold {
  background: rgba(0, 0, 0, .30) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  -webkit-text-stroke: 0 !important;
  text-shadow: none !important;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .45) !important;
}

/* ======================================================================================================
   16 · L'ECHELLE
   Les bandes de rang sont redescendues dans le velours (RT_ANCHOR, src/game/18-pass-and-route.js).
   Restent les textes des cartes, ecrits pour un fond clair : encre sombre sur carte sombre, donc
   illisibles. Ils suivent la surface, comme partout ailleurs.
   ====================================================================================================== */
body.ui-v2 .rt-obj-val { color: #fff !important; }
body.ui-v2 .rt-obj-desc { color: rgba(255, 255, 255, .6) !important; }
body.ui-v2 .rt-obj-lbl { color: var(--vv-gold-text) !important; }
body.ui-v2 .rt-obj-card.tone-obj-B .rt-obj-lbl { color: var(--vv-rank-1) !important; }
body.ui-v2 .rt-obj-card.tone-obj-C .rt-obj-lbl { color: #FF8FA3 !important; }
body.ui-v2 .rt-gem-lbl, body.ui-v2 .rt-info-i { color: rgba(255, 255, 255, .72) !important; }
/* Le rail vertical : un filon d'or qui traverse l'ecran, eteint au-dessus du niveau atteint. */
body.ui-v2 .rt-rail, body.ui-v2 .rt-spine { background: rgba(255, 255, 255, .10) !important; }
body.ui-v2 .rt-rail.done, body.ui-v2 .rt-spine.done {
  background: linear-gradient(var(--vv-gold-3), var(--vv-gold-5)) !important;
}

/* ======================================================================================================
   17 · L'ENCRE DES PANNEAUX
   Meme geste qu'a la section 14, applique au dernier endroit ou il manquait. Les fenetres du jeu
   (`.panel`, dans un `.overlay`) etaient des cartes CLAIRES a encre sombre : reglages, fin de partie,
   joyaux, avatars, ligue, passe. Le panneau est passe au velours a la section 4 ; son encre doit
   suivre, sinon le titre d'une fenetre se lit a 1,27:1 - c'est-a-dire pas du tout.

   Ces textes ont ete RELEVES par `scripts/audit-skin-contraste.mjs`, qui mesure le contraste RENDU de
   chaque texte visible sur les dix-sept scenes, en recomposant le fond effectif couche par couche.
   Une inversion de skin ne se verifie pas a l'oeil : une encre a 1,3:1 sur une carte sombre reste
   VISIBLE sur un ecran de bureau lumineux, et disparait sur un telephone au soleil.
   ====================================================================================================== */
body.ui-v2 .overlay .panel {
  --dp-ink: #fff;
  color: #fff;
}
body.ui-v2 .overlay:not(.reward-sheet) .panel .btn-ghost2 {
  color: var(--vv-gold-text) !important;
  background: rgba(0, 0, 0, .26) !important;
  border-color: var(--vv-line) !important;
}
/* Les textes de second rang gardent leur hierarchie : ils s'attenuent, ils ne disparaissent pas. */
body.ui-v2 .card-foot,
body.ui-v2 .go-best,
body.ui-v2 .hud-label,
body.ui-v2 .setting-val,
body.ui-v2 .rt-obj-rw-cap { color: rgba(255, 255, 255, .62) !important; }
body.ui-v2 .setting-name,
body.ui-v2 .profile-name-big,
body.ui-v2 .pass-lane-lbl,
body.ui-v2 .gd-lvl,
body.ui-v2 .gd-stat-v { color: #fff !important; }
body.ui-v2 #view-profile .pstat span,
body.ui-v2 #view-profile .pstat b { color: inherit !important; }
/* Le bloc inline peint separement les textes secondaires DANS une fenetre (`.overlay .panel p`,
   `.card-foot`, `.hud-label`…) en #4a3a70 - un mauve sombre, choisi pour un panneau clair. */
body.ui-v2 .overlay:not(.reward-sheet) .panel p,
body.ui-v2 .overlay:not(.reward-sheet) .panel .card-foot,
body.ui-v2 .overlay:not(.reward-sheet) .panel .buy-sep,
body.ui-v2 .overlay:not(.reward-sheet) .panel label,
body.ui-v2 .overlay:not(.reward-sheet) .panel .hud-label,
body.ui-v2 .overlay:not(.reward-sheet) .panel .shop-name { color: rgba(255, 255, 255, .68) !important; }

/* ======================================================================================================
   18 · LES FENETRES DU JEU
   Reglages, guide, chances, avatars, ligue, passe, fin de partie : sept fenetres encore entierement
   BLANCHES apres la section 4, parce que le bloc inline les peint avec `linear-gradient(#fff, #f3ecff)`
   en `!important`, et non avec une couleur de fond.

   C'est ce detail qui les a fait passer sous les deux audits : `backgroundColor` vaut « transparent »
   quand le fond est un degrade, et une fenetre entierement blanche etait donc declaree conforme.
   L'instrument lit desormais aussi les arrets de couleur des degrades - c'est ce qui a rendu ces sept
   ecrans visibles.
   ====================================================================================================== */
body.ui-v2 .overlay:not(.reward-sheet) .panel {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  box-shadow: 0 24px 50px rgba(0, 0, 0, .6) !important;
  color: #fff !important;
}
body.ui-v2 .overlay:not(.reward-sheet) .panel .panel-title { color: #fff !important; }
/* Une carte DANS une fenetre : un cran plus clair que la fenetre, sans monture - sinon deux montures
   d'or s'emboitent et l'oeil ne sait plus laquelle porte la valeur. */
body.ui-v2 .overlay:not(.reward-sheet) .panel .card,
body.ui-v2 .overlay:not(.reward-sheet) .panel .shop-card,
body.ui-v2 .overlay:not(.reward-sheet) .panel .pack {
  background: rgba(255, 255, 255, .06) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
  color: #fff !important;
}
/* Le menu de mode suit la meme regle que les fenetres. */
body.ui-v2 .mode-item { color: #fff !important; background: rgba(255, 255, 255, .06) !important; }
body.ui-v2 .mode-item.on {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  color: var(--vv-on-gold) !important;
}

/* FERMER n'est pas DETRUIRE. Le rouge est reserve a ce qu'on perd - une partie ratee, un achat
   refuse. Une croix de fermeture est une sortie neutre : elle prend le velours et le filet d'or,
   comme le bouton retour, avec lequel elle fait la meme chose. */
body.ui-v2 .ov-x {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .5), 0 8px 14px rgba(0, 0, 0, .4) !important;
}
body.ui-v2 .ov-x:active { box-shadow: 0 0 0 rgba(0, 0, 0, .5) !important; }
/* Meme raison pour le retour de l'echelle, qui etait un bouton corail plein. */
body.ui-v2 .route-back {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  -webkit-text-stroke: 0 !important;
  box-shadow: 0 5px 12px rgba(0, 0, 0, .5) !important;
}
/* Le retour de la barre du haut : un disque blanc a contour d'encre, peint par `body.ui-v2 #topbar
   button#btnBack` - DEUX identifiants dans le selecteur. Aucune regle de classe, si specifique
   soit-elle, ne peut y repondre : il faut reprendre les deux identifiants. C'est le seul endroit du
   skin ou la specificite a demande de monter aussi haut. */
body.ui-v2 #topbar button#btnBack {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: var(--vv-gold-text) !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .5), 0 8px 14px rgba(0, 0, 0, .4) !important;
}
body.ui-v2 #topbar button#btnBack:active { box-shadow: 0 0 0 rgba(0, 0, 0, .5) !important; }

/* ======================================================================================================
   19 · CONTROLES DE FORMULAIRE
   Curseurs et listes deroulantes : les seuls elements du jeu dessines par le navigateur. Ils
   n'heritent d'aucun des jetons ci-dessus et doivent etre repeints explicitement.
   ====================================================================================================== */
body.ui-v2 input[type="range"],
body.ui-v2 #ovSettings input[type="range"] {
  background: var(--vv-well) !important;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .7) !important;
}
/* La poignee etait corail : c'est une valeur qu'on regle, pas une alerte. Elle passe a l'or, comme
   tout ce qui se manipule. */
body.ui-v2 input[type="range"]::-webkit-slider-thumb {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  box-shadow: 0 2px 0 var(--vv-gold-shadow), 0 0 8px rgba(242, 196, 99, .45) !important;
}
body.ui-v2 input[type="range"]::-moz-range-thumb {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
}
body.ui-v2 select, body.ui-v2 .lang-select {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  color: #fff !important;
}
body.ui-v2 select option { background: var(--vv-panel-lo); color: #fff; }
body.ui-v2 input[type="text"], body.ui-v2 input[type="tel"], body.ui-v2 textarea {
  background: rgba(0, 0, 0, .32) !important;
  border: 1.5px solid var(--vv-line) !important;
  color: #fff !important;
}

/* ======================================================================================================
   20 · LE TEXTE DES BOUTONS
   La V2 cerne le texte de CHAQUE bouton d'un contour d'encre epais (`--btn-stroke-w`) : c'etait la
   solution retenue quand un bouton pouvait se retrouver sur un wallpaper de n'importe quelle
   luminance, et elle etait juste dans ce contexte.

   Sur un bouton d'or, elle ne l'est plus. L'encre du contour et l'encre du texte sont la meme, et le
   contour empate donc les lettres au lieu de les detacher - « REJOUER » lit comme un tampon, pas
   comme un mot. Le fond dore est une surface CONNUE : le contraste y est acquis par construction, le
   contour ne repare rien et coute la nettete.

   Il ne tombe que sur les boutons dont on maitrise le fond. Partout ailleurs - un bouton pose sur une
   image, sur le plateau - il reste.
   ====================================================================================================== */
body.ui-v2 .btn-primary,
body.ui-v2 .btn-gold,
body.ui-v2 .btn-ghost,
body.ui-v2 .btn-ghost2,
body.ui-v2 .btn-mini,
body.ui-v2 .btn-coral,
body.ui-v2 .ov-x,
body.ui-v2 .seg,
body.ui-v2 #ovGemDetail .gd-btn,
body.ui-v2 .syn-cta,
body.ui-v2[data-view="home"] .box-slot .btn-mini {
  -webkit-text-stroke: 0 !important;
  paint-order: normal;
}
body.ui-v2 .btn-primary, body.ui-v2 .btn-gold { text-shadow: 0 1px 0 rgba(255, 255, 255, .40) !important; }

/* ======================================================================================================
   21 · LA FICHE D'UNE GEMME
   L'ecran ou la pierre est le sujet, et le seul qui montre le rendu 3D. Tout le reste doit s'effacer :
   pas de blanc, pas de vert plein, une seule action.

   Le cadre etait dessine pour un panneau CLAIR - contours en `--dp-ink`, qui vaut desormais blanc dans
   une fenetre (section 17). Resultat : la barre de copies et le bouton de fermeture portaient un
   anneau blanc epais, l'element le plus voyant d'un ecran cense montrer une gemme.
   ====================================================================================================== */
body.ui-v2 #ovGemDetail .gd-bar {
  background: var(--vv-well) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7) !important;
}
body.ui-v2 #ovGemDetail .gd-sep { background: rgba(242, 196, 99, .16) !important; }
body.ui-v2 #ovGemDetail .gd-close {
  border: 1.5px solid var(--vv-line-strong) !important;
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
}
body.ui-v2 #ovGemDetail .gd-close::before,
body.ui-v2 #ovGemDetail .gd-close::after { background: var(--vv-gold-text) !important; }

/* AMELIORER est LA seule action de cette fiche : elle prend l'or, comme toute action principale.
   Elle etait verte - or le vert du systeme ne dit qu'une chose, « ce que tu gagnes », et il continue
   de le dire juste a cote, sur les deltas (+1, +1.00) et sur la portion gagnee de chaque jauge. Deux
   sens pour une meme couleur sur le meme ecran, c'etait un de trop. */
body.ui-v2 #ovGemDetail .gd-btn-up {
  background:
    linear-gradient(var(--vv-btn-1), var(--vv-btn-2) 60%, var(--vv-btn-3)) padding-box,
    linear-gradient(var(--vv-gold-1), var(--vv-gold-3) 50%, var(--vv-gold-5)) border-box !important;
  border: 2px solid transparent !important;
  color: var(--vv-on-gold) !important;
  box-shadow: 0 6px 0 var(--vv-gold-shadow), 0 12px 22px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 #ovGemDetail .gd-btn-up:active { transform: translateY(6px) !important; box-shadow: 0 0 0 var(--vv-gold-shadow) !important; }
body.ui-v2 #ovGemDetail .gd-btn-up[disabled] {
  background: rgba(0, 0, 0, .30) !important;
  border-color: var(--vv-line-locked) !important;
  color: rgba(255, 255, 255, .45) !important;
  box-shadow: none !important;
}
/* Le gain reste vert - c'est sa seule fonction, et elle est maintenant sans concurrent. */
body.ui-v2 #ovGemDetail .gd-up { color: var(--vv-gain) !important; }
body.ui-v2 #ovGemDetail .gd-g { background: var(--vv-well) !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, .65) !important; }
body.ui-v2 #ovGemDetail .gd-g-next { background: linear-gradient(#A9F5BF, var(--vv-gain)) !important; }
body.ui-v2 #ovGemDetail .gd-g-now { background: linear-gradient(var(--vv-gemlvl-2), var(--vv-gemlvl-1)) !important; }

/* ======================================================================================================
   22 · COLLECTION · STYLES, ET LES DEUX VUES PLEIN ECRAN
   Ces ecrans-la n'apparaissaient dans aucun audit tant que celui-ci ne cliquait pas les SOUS-ONGLETS :
   un audit qui n'ouvre que l'onglet par defaut d'une vue declare propre tout ce qu'il n'a pas vu. Les
   scenes `collection-bonus`, `collection-styles`, `styles-fonds`, `classement` et `loadout` ont ete
   ajoutees aux deux instruments pour cette raison.
   ====================================================================================================== */
body.ui-v2 .mix-bar {
  background: rgba(0, 0, 0, .28) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
}
body.ui-v2 .mix-head { color: #fff !important; }
body.ui-v2 .mix-head b { color: var(--vv-gold-text) !important; }
body.ui-v2 .mix-slot.empty {
  background: rgba(0, 0, 0, .30) !important;
  border-color: var(--vv-line-dash) !important;
}
/* Une vignette de style est un CADRE autour d'une image : elle porte donc la monture, pas un fond. */
body.ui-v2 #view-collection .style-prev {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line) !important;
}
body.ui-v2 #view-collection .style-prev.equipped {
  border-color: transparent !important;
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
}
body.ui-v2 #view-collection .style-prev.is-locked {
  background: rgba(0, 0, 0, .24) !important;
  border: 1.5px dashed var(--vv-line-locked) !important;
}
body.ui-v2 #view-collection .prev-name { color: #fff !important; }

/* LOADOUT et CRAFT sont des vues PLEIN ECRAN : elles peignent leur propre fond, et etaient donc les
   deux seuls ecrans du jeu ou le velours disparaissait entierement. */
body.ui-v2 #view-loadout, body.ui-v2 #view-craft {
  background: radial-gradient(120% 70% at 50% 0%, var(--vv-bg-top) 0%, var(--vv-bg-mid) 45%, var(--vv-bg-bot) 100%) !important;
}
body.ui-v2 #ovProfile .profile-nav,
body.ui-v2 #view-loadout .profile-nav,
body.ui-v2 #view-craft .profile-nav {
  background: rgba(10, 4, 20, .88) !important;
  border-top: 1.5px solid var(--vv-line) !important;
  box-shadow: 0 -6px 20px rgba(0, 0, 0, .5) !important;
}
body.ui-v2 #view-loadout .btn-ghost2, body.ui-v2 #view-craft .btn-ghost2 { color: var(--vv-gold-text) !important; }
body.ui-v2 #view-loadout .loadout-sub,
body.ui-v2 #view-craft .loadout-sub,
body.ui-v2 #view-craft .craft-pool-cap { color: rgba(255, 255, 255, .62) !important; }
/* Le CLASSEMENT et le LOADOUT ecrivaient leurs libelles en encre sombre : ils vivaient sur des vues
   claires que les deux sections precedentes viennent de passer au velours. */
body.ui-v2 #profilePaneBoard .profile-name,
body.ui-v2 #profilePaneBoard .card-foot,
body.ui-v2 #profilePaneBoard .hud-label,
body.ui-v2 #profilePaneBoard .lb-name,
body.ui-v2 #profilePaneBoard .lb-score { color: #fff !important; }
body.ui-v2 #profilePaneBoard .card-foot, body.ui-v2 #profilePaneBoard .hud-label { color: rgba(255, 255, 255, .66) !important; }
body.ui-v2 #view-loadout .loadout-hint { color: rgba(255, 255, 255, .6) !important; }

/* ======================================================================================================
   23 · DEUX JETONS D'ACCENT, ET UN LOGO NOIR
   ====================================================================================================== */
body.ui-v2 {
  /* `--dp-coral-lip` ne peint AUCUNE surface : ses quinze usages sont tous des petits libelles - le
     titre « Statistiques », le rang sur l'accueil, la categorie d'un bonus, l'en-tete des cadres. Un
     rose framboise y jouait le role d'accent, et c'est ce role que l'or tient maintenant. Repeindre
     le jeton rallie les quinze d'un coup ; les peindre un par un aurait manque ceux qui ne
     s'affichent qu'en cours de partie. */
  --dp-coral-lip: var(--vv-gold-text);
  /* `--dp-gem` reste violet : c'est l'accent des ecrans de PIERRES, et le violet y designe la gemme
     elle-meme, pas une action. Il ne concurrence donc pas l'or. */
}
/* Le logo Apple porte `fill="#000"` DANS le SVG : sur un bouton clair il se detachait, sur le velours
   il disparait. Les logos Google et Facebook, eux, sont colores et n'ont pas ce probleme - c'est le
   seul des trois a corriger. */
body.ui-v2 .btn-sso[data-provider="apple"] .sso-ic svg,
body.ui-v2 .btn-sso[data-provider="apple"] .sso-ic path { fill: #fff !important; }

/* ======================================================================================================
   24 · L'ECRAN SYNERGIE
   Le seul ecran plein format du jeu, et le seul qui vivait sur deux images cuites en violet vif : il
   restait litteralement hors de l'ecrin. Son bandeau portait en plus le mot « SYNERGY » PEINT dans
   l'image - le dernier titre du jeu qui ne se traduisait pas.

   L'en-tete est desormais rendu en DOM (`renderSynergyIntro`, src/game/25-wiring-and-settings.js) a
   partir des chaines `synIntroHead` / `synIntroSub`, qui existaient deja dans les quatre langues. Les
   deux images tombent, et le probleme d'i18n avec elles.
   ====================================================================================================== */
/* Le velours est porte par l'OVERLAY, pas par `.syn-full` : ce dernier se dimensionne a son contenu
   et laissait donc voir l'accueil floute dans les coins hauts. */
body.ui-v2 .syn-overlay {
  background: radial-gradient(120% 55% at 50% 0%, var(--vv-bg-top) 0%, var(--vv-bg-mid) 42%, var(--vv-bg-bot) 100%) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
body.ui-v2 .syn-full { background: none !important; }
body.ui-v2 .syn-heroband {
  aspect-ratio: auto !important;
  background: radial-gradient(70% 100% at 50% 24%, rgba(242, 196, 99, .22), transparent 70%) !important;
  padding: 26px 20px 18px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
body.ui-v2 .syn-hero-gems { display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 4px; }
body.ui-v2 .syn-hero-gems .rt-gem {
  width: 54px; height: 54px;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, .55)) drop-shadow(0 0 18px rgba(242, 196, 99, .35));
}
/* Le diamant du milieu est plus haut et plus grand : trois pierres alignees se lisent comme un motif,
   trois pierres en triangle se lisent comme une parure. */
body.ui-v2 .syn-hero-gems .rt-gem:nth-child(2) { width: 68px; height: 68px; margin: 0 -2px 12px; }
body.ui-v2 .syn-hero-t {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-weight: 400;
  font-size: 40px; letter-spacing: .14em; margin: 0;
  color: #fff; text-shadow: 0 0 26px rgba(242, 196, 99, .45);
}
/* Le sous-titre entre deux losanges d'or : c'est le seul ornement de l'ecran, et il encadre la
   promesse plutot que de decorer le vide. */
body.ui-v2 .syn-hero-s {
  margin: 2px 0 0; font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: 12px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--vv-gold-text) !important; text-align: center;
}
body.ui-v2 .syn-hero-s::before { content: '\25C6\00a0\00a0'; }
body.ui-v2 .syn-hero-s::after  { content: '\00a0\00a0\25C6'; }

body.ui-v2 .syn-card {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 .syn-card-gem {
  background: radial-gradient(circle at 50% 42%, rgba(242, 196, 99, .18), transparent 72%) !important;
  box-shadow: inset 0 0 0 1.5px var(--vv-line) !important;
}
body.ui-v2 .syn-card-title { text-shadow: none !important; }
body.ui-v2 .syn-hl, body.ui-v2 .syn-card-desc .syn-hl, body.ui-v2 .syn-x { color: var(--vv-gold-text) !important; }
body.ui-v2 .syn-card-title .syn-num {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  color: var(--vv-on-gold) !important;
}
/* C'EST PARTI ! est l'unique action de l'ecran : elle prend l'or plein. */
body.ui-v2 .syn-cta {
  background:
    linear-gradient(var(--vv-btn-1), var(--vv-btn-2) 60%, var(--vv-btn-3)) padding-box,
    linear-gradient(var(--vv-gold-1), var(--vv-gold-3) 50%, var(--vv-gold-5)) border-box !important;
  border: 2px solid transparent !important;
  color: var(--vv-on-gold) !important;
  box-shadow: 0 8px 0 var(--vv-gold-shadow), 0 16px 28px rgba(0, 0, 0, .5) !important;
}
body.ui-v2 .syn-cta:active { transform: translateY(8px) !important; box-shadow: 0 0 0 var(--vv-gold-shadow) !important; }

/* ======================================================================================================
   25 · CONFORMITE AU HANDOFF
   Les sections precedentes ont amene le jeu DANS le systeme. Celle-ci le cale sur les valeurs exactes
   du document de remise (`gemz-velvet-vault-visual-identity/project/design_handoff_velvet_vault/`),
   qui les donne au pixel : « Colours, type, spacing, radii and shadows are final and exact. Recreate
   pixel-perfectly at a 390 x 844 reference viewport ».

   On ne recopie pas tout : la plupart des valeurs etaient deja atteintes en repeignant les jetons. Ne
   figurent ici que les ecarts mesures sur les captures.
   ====================================================================================================== */

/* --- L'EN-TETE ----------------------------------------------------------------------------------
   « Two flexible wallet chips - full-round, panel gradient, gold hairline, padding 0 12px 0 5px,
   29px icon, Lilita 17px value. » */
body.ui-v2 #topbar .coin-chip,
body.ui-v2 #topbar .jewel-chip { padding: 0 12px 0 5px !important; }
body.ui-v2 #topbar .coin-chip > span,
body.ui-v2 #topbar .jewel-chip > span {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif !important;
  font-size: 17px !important; letter-spacing: .02em !important;
}
body.ui-v2 #topbar .coin-chip .coin-ic,
body.ui-v2 #topbar .jewel-chip .jewel-mini { width: 29px !important; height: 29px !important; }

/* LA BARRE DU HAUT EN JEU NE SE CALE PLUS SUR LE PLATEAU. Elle etait bridee a `--board-w` pour aligner
   ses bords sur ceux du plateau - une intention juste, mais qui coutait la lisibilite de la SEULE
   information qu'on y lit vraiment : sur un iPhone, le plateau fait 273px, et les cinq elements de la
   barre n'y tenaient pas. Les pastilles se comprimaient donc jusqu'a tronquer leur valeur, et le
   joueur voyait « 4… » a la place de « 48k ». Mesure faite avec ET sans cette feuille : le defaut
   preexiste, il ne vient pas du skin.
   Le handoff tranche dans l'autre sens - « Header: back · settings · two wallet chips (36px variant)
   · 38px avatar », en pleine largeur d'ecran comme sur tous les autres ecrans. L'alignement sur le
   plateau disparait donc, et la valeur redevient lisible. */
body.ui-v2[data-view="game"] #topbar { max-width: none !important; }
body.ui-v2[data-view="game"] #topbar .coin-chip > span,
body.ui-v2[data-view="game"] #topbar .jewel-chip > span {
  font-size: 15px !important; overflow: visible; text-overflow: clip;
}
body.ui-v2[data-view="game"] #topbar .coin-chip .coin-ic,
body.ui-v2[data-view="game"] #topbar .jewel-chip .jewel-mini { width: 25px !important; height: 25px !important; }

/* --- LA BARRE DU BAS ----------------------------------------------------------------------------
   « The active item lifts: translateY(-12px), a 56px gold-bezelled disc (padding 2px). » */
body.ui-v2 #navBar .nav-item.active { transform: translateY(-12px) scale(1.06) !important; }
body.ui-v2 #navBar .nav-item.active .nav-ic { padding: 2px !important; }
body.ui-v2 .nav-label { font-size: 8.5px !important; }

/* --- L'OBJECTIF DE PARTIE -----------------------------------------------------------------------
   « 22px, radius 11, #170C28, 1.5px solid rgba(242,196,99,.3), fill linear-gradient(90deg,#F2C463,
   #FFE9B0), centred label Nunito 900 10px .14em. » */
body.ui-v2 #objBar, body.ui-v2 .obj-bar, body.ui-v2 #rushBar {
  height: 22px !important; border-radius: 11px !important;
  background: var(--vv-well) !important;
  border: 1.5px solid rgba(242, 196, 99, .3) !important;
  box-shadow: none !important;
}
body.ui-v2 #objBar > i, body.ui-v2 .obj-bar > i, body.ui-v2 #rushBar > i {
  background: linear-gradient(90deg, var(--vv-gold-text), var(--vv-gold-2)) !important;
}
body.ui-v2 #objBar > b, body.ui-v2 .obj-bar > b, body.ui-v2 #rushBar > b {
  font-family: 'Nunito', sans-serif !important; font-weight: 900 !important;
  font-size: 10px !important; letter-spacing: .14em !important; color: #fff !important;
}

/* --- LES ONGLETS --------------------------------------------------------------------------------
   « Tab bar: 42px, radius 21, rgba(0,0,0,.3), 1.5px solid rgba(242,196,99,.28), padding 3px. Active
   tab is a radius-18 fill. » */
body.ui-v2 .seg-row {
  border-radius: 21px !important; padding: 3px !important;
  border-color: rgba(242, 196, 99, .28) !important;
  background: rgba(0, 0, 0, .30) !important;
}
body.ui-v2 .seg.on { border-radius: 18px !important; }

/* --- LA GRILLE DE COLLECTION --------------------------------------------------------------------
   « Undiscovered slots: dashed, 118px min height, 28px padlock at opacity .4. » */
body.ui-v2 .gemcard.is-locked { min-height: 118px; }
body.ui-v2 .gemcard.is-locked .prev-lock, body.ui-v2 .gemcard.is-locked img[src*="padlock"] { opacity: .4; }

/* --- LA BOUTIQUE --------------------------------------------------------------------------------
   « a full-width 26px gold price pill (linear-gradient(#FFD983,#D9A441), 0 3px 0 #6D4A10, jewel icon
   + Lilita 13px) ». Le prix etait une pastille grise : c'est pourtant la seule chose qu'on compare
   d'une carte a l'autre, et le handoff insiste - « Price always sits in the same place on every
   card ». Il devient donc l'element dore de la carte. */
body.ui-v2 .shop-box-tile .sb-price, body.ui-v2 .sd-buy {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  border: 0 !important;
  color: var(--vv-on-gold) !important;
  -webkit-text-stroke: 0 !important; text-shadow: none !important;
  box-shadow: 0 3px 0 #6D4A10 !important;
  border-radius: 13px !important;
}
/* « Today's offers ... a 24px outlined price chip (rgba(0,0,0,.35), gold hairline, Lilita 12px
   #F2C463) - real-money prices are deliberately quieter than soft-currency prices. » */
body.ui-v2 .shop-deal-tile .sd-buy.pay-cash {
  background: rgba(0, 0, 0, .35) !important;
  border: 1.5px solid var(--vv-line) !important;
  color: var(--vv-gold-text) !important;
  box-shadow: none !important;
}
/* Le filet d'or qui comble la fin d'un titre de section, comme sur `JEWEL CASES`. */
body.ui-v2 .shop-section, body.ui-v2 .sec-title, body.ui-v2 .coll-sec-head {
  display: flex !important; align-items: center; gap: 10px;
}
body.ui-v2 .shop-section::after, body.ui-v2 .sec-title::after, body.ui-v2 .coll-sec-head::after {
  content: ''; flex: 1 1 0; min-width: 12px; height: 1px;
  background: linear-gradient(90deg, rgba(242, 196, 99, .5), transparent);
}

/* --- LA FICHE D'UNE GEMME -----------------------------------------------------------------------
   « The Gem Detail screen uses a tighter base plus one purple bloom behind the gem. » C'est le seul
   ecran du jeu qui s'ecarte du fond commun, et pour une raison : la pierre doit sembler eclairee par
   quelque chose. */
body.ui-v2 #ovGemDetail.gd-ov {
  background: radial-gradient(90% 45% at 50% 22%, #5C3480 0%, #2A1440 52%, #120720 100%) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
body.ui-v2 #ovGemDetail .gd-glow {
  background: radial-gradient(circle, rgba(196, 120, 255, .35), transparent 62%) !important;
  filter: blur(14px) !important; opacity: 1 !important;
}
/* « SPAWN RATE 28 % (gold fill - a rate, not an upgrade) » : le taux d'apparition ne se GAGNE pas,
   il se constate. Il ne porte donc pas le violet des statistiques ameliorables. */
body.ui-v2 #ovGemDetail .gd-stat:last-child .gd-g-now,
body.ui-v2 #ovGemDetail .gd-g-rate {
  background: linear-gradient(var(--vv-gold-2), var(--vv-gold-3)) !important;
}

/* --- MOUVEMENT ----------------------------------------------------------------------------------
   « Case / gem float: translateY(0 -> -9px -> 0), 4.5-5.5s ease-in-out, infinite. Stagger the
   durations so cards don't bob in unison. » Le decalage compte autant que le mouvement : quatre
   cartes qui montent ensemble lisent comme un bug d'affichage, quatre cartes decalees lisent comme
   des objets poses. */
@keyframes vv-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
body.ui-v2[data-view="home"] .box-slot:not(.is-empty):not(.is-locked) .slot-art { animation: vv-float 4.5s ease-in-out infinite; }
body.ui-v2[data-view="home"] .box-slot:nth-child(2) .slot-art { animation-duration: 5.1s; }
body.ui-v2[data-view="home"] .box-slot:nth-child(3) .slot-art { animation-duration: 4.8s; }
body.ui-v2[data-view="home"] .box-slot:nth-child(4) .slot-art { animation-duration: 5.4s; }
@media (prefers-reduced-motion: reduce) {
  body.ui-v2[data-view="home"] .box-slot .slot-art { animation: none !important; }
}
/* Les lignes du CLASSEMENT, hors la mienne : `hsl(var(--h) 22% 91%)` - une surface claire calculee
   sur la teinte, donc devenue prune PALE au lieu de bleu pale. Elle suit maintenant la meme regle que
   toutes les autres lignes de liste du jeu. */
body.ui-v2 #profilePaneBoard #boardList > li:not(.me) {
  --dp-ink: #fff;
  background: rgba(255, 255, 255, .05) !important;
  border: 1.5px solid var(--vv-line) !important;
  box-shadow: none !important;
  opacity: 1 !important;
  color: #fff !important;
}

/* ======================================================================================================
   26 · LES DEUX BOUTONS DE L'EN-TETE
   ====================================================================================================== */
/* L'ENGRENAGE. Le skin V1 lui retirait tout cadre et le cernait d'un contour BLANC epais, dessine par
   quatre `drop-shadow` empiles - une solution faite pour poser une icone sombre sur un wallpaper
   quelconque. Sur le velours, ce halo blanc devient l'element le plus clair de la barre, pour l'action
   la moins importante de l'ecran. Le handoff demande l'inverse : un carre de 36-40px, rayon 12-13,
   degrade de panneau, filet d'or - c'est-a-dire exactement le bouton retour, dont il est le voisin. */
/* SPECIFICITE : `body.ui-v2 button#btnSettings` et non `body.ui-v2 #btnSettings`. Le selecteur d'en
   face porte le type `button`, ce qui lui donne un type de plus - a `!important` egal, il gagnait.
   Le `button` ici ne selectionne rien de plus, il remet juste les deux regles a egalite, et l'ordre
   des feuilles tranche alors en faveur de celle-ci. */
/* owner 2026-08-25 : l'engrenage n'etait pas centre, et le coupable est MESURE - le bouton est une
   boite flex avec `align-items: center` mais SANS `justify-content`. L'axe transversal (vertical)
   etait donc centre, l'axe principal (horizontal) non : l'icone de 24 px collait au bord gauche de
   sa boite de contenu de 31 px, laissant 7 px de jeu a droite. Marges relevees : 6,5 a gauche contre
   13,5 a droite, 10 en haut comme en bas. L'asset n'y est pour rien : le centroide alpha de
   gear.webp tombe a 0,7 px du centre une fois ramene a 24 px.
   Et le CERCLE, comme l'owner le propose : #btnBack, son voisin immediat, est DEJA un disque de
   44 px parfaitement centre (marges 11/11/11/11, mesurees). Le bouton des reglages etait le seul
   carre arrondi de la barre ; en disque, les deux extremites de la barre se repondent - l'engrenage
   a gauche, l'avatar a droite, meme diametre. */
body.ui-v2 button#btnSettings {
  background: linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) !important;
  border: 1.5px solid var(--vv-line-strong) !important;
  border-radius: 50% !important;
  width: 44px !important; height: 44px !important;
  padding: 0 !important;
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  box-shadow: 0 4px 0 rgba(0, 0, 0, .45), 0 8px 14px rgba(0, 0, 0, .35) !important;
}
body.ui-v2 button#btnSettings .topbtn-ic {
  width: 24px !important; height: 24px !important;
  filter: brightness(1.15) drop-shadow(0 1px 2px rgba(0, 0, 0, .6)) !important;
}

/* L'AVATAR DE LA BARRE DU HAUT : PLUS DE CERCEAU D'OR PAR DEFAUT (owner 2026-08-30).
   Il portait un degrade conique dore EN PERMANENCE, quel que soit le cadre equipe. Consequence, dite
   par l'owner : « le contour par defaut est deja dore, ce qui annule totalement la valeur des cadres
   d'avatar, et en particulier le dore ». Un joueur qui debloquait enfin le cadre Or ne voyait aucune
   difference - sa recompense etait deja peinte sur son avatar depuis le premier lancement.
   Le disque reste donc NU tant qu'aucun cadre n'est porte. Le cerceau conique n'est pas supprime : il
   descend sur `.avatar-frame`, ou il devient ce qu'il aurait toujours du etre - la marque d'un cadre
   gagne. Le portrait, lui, cesse de se retracter (le padding qui revelait le cerceau n'existe plus). */
body.ui-v2 #topbar #btnProfile .avatar-pic {
  padding: 0 !important;
  border: 0 !important;
  box-sizing: border-box !important;
  background: linear-gradient(135deg, var(--a1, #6c7bdc), var(--a2, #a6b0f5)) !important;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .5) !important;
}
/* Un cadre EQUIPE reprend le cerceau : c'est lui, desormais, qui vaut quelque chose. */
body.ui-v2 #topbar #btnProfile .avatar-frame .avatar-pic { padding: 0 !important; }
/* Le portrait remplit tout le disque. Il se retractait de 2,5 px pour laisser paraitre le cerceau
   conique ; sans cerceau par defaut, ce retrait ne laissait plus voir qu'un anneau de degrade nu. */
body.ui-v2 #topbar #btnProfile .avatar-pic .avatar-img {
  inset: 0 !important; width: auto !important; height: auto !important;
  border-radius: 50%;
  background-size: cover; background-position: center;
}

/* ======================================================================================================
   27 · DEUX SIGNAUX DE LA COLLECTION
   ====================================================================================================== */
/* « EQUIPE » etait vert. Le vert du systeme ne dit qu'une chose - ce que tu GAGNES - et il le dit
   deja, au meme moment, sur les deltas de la fiche voisine. « Equipe » n'est pas un gain, c'est un
   etat : il prend l'or, comme le reste de ce qui a de la valeur pour le joueur. Le handoff le pose
   noir sur blanc - `#F2C463` quand la gemme est equipee, `rgba(255,255,255,.5)` sinon. */
body.ui-v2 .prev-eq { color: var(--vv-gold-text) !important; }

/* LE FILET DE TITRE DE SECTION passe ENTRE le libelle et le compte, pas apres les deux : c'est ce qui
   fait lire « POSSEDES ——— 10/34 » comme une ligne unique plutot que comme deux mots colles suivis
   d'un trait. `order` sur un pseudo-element fonctionne dans une boite flex, et evite d'ajouter un
   element au DOM pour un trait. */
body.ui-v2 .coll-sec-head { justify-content: flex-start !important; }
body.ui-v2 .coll-sec-head::after { order: 0; }
body.ui-v2 .coll-sec-head > * { order: 1; }

/* ======================================================================================================
   28 · LA FICHE, SUITE — TROIS ECARTS AU HANDOFF
   ====================================================================================================== */
/* 1. LA BARRE DE COPIES. Son remplissage etait `color-mix(--rc 62%, #fff)` : sur une gemme commune,
   dont la teinte de rarete est un gris bleute, le melange avec du blanc donnait une grosse pastille
   BLANCHE au milieu de l'ecran le plus sombre du jeu. Le handoff donne la valeur - « 11px progress
   bar (linear-gradient(90deg,#C47DFF,#EFD6FF)) » : le violet du NIVEAU, le meme que sur les cartes de
   la collection. Cette barre mesure une montee de niveau, pas une rarete. */
body.ui-v2 #ovGemDetail .gd-bar > i {
  background: linear-gradient(90deg, var(--vv-gemlvl-1), var(--vv-gemlvl-2)) !important;
}
body.ui-v2 #ovGemDetail .gd-bar.gd-bar-full > i {
  background: linear-gradient(90deg, #6FD88A, var(--vv-gain)) !important;
}

/* 2. AMELIORER, INDISPONIBLE. Le handoff ne l'eteint pas : « Disabled when coins are short: drop the
   sheen, filter: grayscale(.6) brightness(.8) ». Le bouton garde donc sa FORME de bouton d'or - le
   joueur voit ce qu'il pourra faire - et perd seulement son eclat. Le remplacer par un panneau sombre
   effacait l'action au lieu de la suspendre. */
body.ui-v2 #ovGemDetail .gd-btn-up[disabled] {
  background:
    linear-gradient(var(--vv-btn-1), var(--vv-btn-2) 60%, var(--vv-btn-3)) padding-box,
    linear-gradient(var(--vv-gold-1), var(--vv-gold-3) 50%, var(--vv-gold-5)) border-box !important;
  border: 2px solid transparent !important;
  color: var(--vv-on-gold) !important;
  filter: grayscale(.6) brightness(.8) !important;
  box-shadow: 0 6px 0 var(--vv-gold-shadow) !important;
}

/* 3. LE CADRE. Il portait la teinte de rarete en 3px pleins : sur une commune, un anneau GRIS de la
   hauteur de l'ecran. Le handoff en fait un panneau serti comme les autres - « Stats panel: gold-set,
   radius 22/21 » - et laisse la rarete se dire ou elle se lit vraiment : la pastille au-dessus de la
   pierre, et le halo derriere elle. Une seule information, un seul endroit. */
body.ui-v2 #ovGemDetail .gd-panel {
  border: 1.5px solid transparent !important;
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .5) !important;
}
/* La pastille de rarete porte l'or plein, et sa teinte reste dans le libelle. */
body.ui-v2 #ovGemDetail .gd-rar {
  background: linear-gradient(var(--vv-gold-2), var(--vv-gold-3)) !important;
  color: var(--vv-on-gold) !important;
  border: 0 !important;
  font-family: 'Nunito', sans-serif !important; font-weight: 900 !important;
  font-size: 10px !important; letter-spacing: .20em !important;
}

/* ======================================================================================================
   29 · LE RAIL DE L'ECHELLE — « le filon »
   Le trajet deja parcouru etait peint en VERT vif sur toute la hauteur de l'ecran : la plus longue
   ligne du jeu, dans la seule couleur que le systeme reserve au gain. Le handoff nomme cet ecran « the
   seam » - le filon - et c'est exactement ce que le rail doit etre : une veine d'or qui monte jusqu'ou
   le joueur est arrive, et de la pierre nue au-dessus.
   ====================================================================================================== */
body.ui-v2 .rt-row::before {
  background: rgba(255, 255, 255, .10) !important;
  border-left-color: rgba(0, 0, 0, .35) !important;
  border-right-color: rgba(0, 0, 0, .35) !important;
}
body.ui-v2 .rt-row.done::before {
  background: linear-gradient(90deg, var(--vv-gold-5), var(--vv-gold-3) 45%, var(--vv-gold-5)) !important;
  border-left-color: rgba(90, 60, 10, .55) !important;
  border-right-color: rgba(90, 60, 10, .55) !important;
}
body.ui-v2 .rt-row.next::before { background: rgba(255, 255, 255, .14) !important; }
/* Le rail se dessine en TROIS morceaux, pas un : la rangee (`.rt-row::before`), le separateur entre
   deux rangees (`.rt-sep::before`), et le demi-segment du dernier niveau franchi
   (`.rt-lastdone::before`, vert en bas / gris en haut). N'en repeindre qu'un laisse une portion verte
   au milieu du filon - c'est ce qui restait sous le noeud 36. */
body.ui-v2 .rt-sep::before { background: rgba(255, 255, 255, .10) !important; }
body.ui-v2 .rt-sep.done::before {
  background: linear-gradient(90deg, var(--vv-gold-5), var(--vv-gold-3) 45%, var(--vv-gold-5)) !important;
}
body.ui-v2 .rt-row.done.rt-lastdone::before {
  background: linear-gradient(0deg, var(--vv-gold-4) 50%, rgba(255, 255, 255, .10) 50%) !important;
}

/* LE RAIL DOIT S'ALLUMER AU PASSAGE DU CURSEUR, PAS AVANT (owner 2026-08-29 : « le rail n'affiche
   pas correctement la progression d'un niveau a un autre »).

   `renderRoute` sort les niveaux gagnes en `done` - donc rail DEJA dore - et la ceremonie les eteint
   en leur posant `rt-pending`, qu'elle retire un par un au passage du curseur. La regle d'extinction
   vit dans le <style> d'index.html : `.rt-row.rt-pending::before { ... !important }`, specificite
   (0,2,1). Les regles dorees, elles, vivent ICI : `body.ui-v2 .rt-row.done::before`, specificite
   (0,3,1) + `!important`, et velvet.css est charge EN DERNIER. L'extinction ne pouvait donc jamais
   gagner : entre deux `!important`, c'est la specificite qui tranche, et a specificite egale l'ordre.

   Mesure faite dans le jeu : une ligne `done rt-pending` rendait exactement le meme degrade dore
   qu'une ligne `done` seule. Tout le filon etait donc dore AVANT que le curseur y passe - il ne
   restait plus aucune progression a voir, ce qui est precisement le symptome.

   On remonte donc l'extinction ici, avec un ID (`#ovRoute`) : un ID bat n'importe quel nombre de
   classes, la regle ne peut plus etre debordee par une regle doree ajoutee plus tard. */
body.ui-v2 #ovRoute .rt-row.rt-pending::before,
body.ui-v2 #ovRoute .rt-sep.rt-pending::before { background: rgba(255, 255, 255, .09) !important; }

/* ET LE CERCLE DU NIVEAU AVEC LUI (owner 2026-08-30 : « le cercle du niveau qui vient d'etre complete
   s'affiche en jaune / dore AVANT que le rail de progression ne l'atteigne »).

   MEME PIEGE, UNE STRATE PLUS LOIN. La correction ci-dessus n'avait remonte que le RAIL (`::before`) ;
   la pastille ronde du numero, elle, est restee sous la regle d'index.html
   `.rt-row.rt-pending .rt-node` (0,2,1), que `body.ui-v2 .rt-row.done .rt-node` (0,3,1) bat toujours.
   Resultat : le filon s'allumait bien au passage du curseur, mais le cercle du niveau gagne etait
   dore depuis la premiere image - il annoncait la recompense avant qu'on l'ait vue arriver.

   Trois proprietes a reprendre, pas seulement le fond : la regle doree pose AUSSI `border-color` et
   `color`, et n'ecraser que le fond laisserait un cercle gris a bord dore avec un chiffre en brun. */
body.ui-v2 #ovRoute .rt-row.rt-pending .rt-node {
  background: #8e87ad !important;
  border-color: rgba(255, 255, 255, .22) !important;
  color: rgba(255, 255, 255, .78) !important;
  animation: none !important;
  box-shadow: none !important;
}

/* PENDANT LA CEREMONIE, LA LISTE NE SE TOUCHE PAS (owner : « il faut cliquer plusieurs fois sur
   CONTINUER avant qu'il ferme »).
   `#routeScroll` occupe TOUT l'ecran, y compris SOUS le bouton, et la montee lui ecrit son
   `scrollTop` a chaque image pendant plusieurs secondes. Sur iOS, un conteneur defilant qui vient
   d'etre defile ABSORBE le premier contact pour arreter son inertie - meme quand ce contact atterrit
   sur un enfant pose par-dessus. Le premier tap est mange, le deuxieme passe : exactement ce que
   l'owner decrit, et exactement ce qui ne se reproduit pas a la souris.
   Pendant la ceremonie le joueur n'a rien a faire defiler - la montee conduit la vue et CONTINUER est
   la seule action. On sort donc la liste du chemin tactile ; elle le retrouve des que la ceremonie
   se termine. */
body.ui-v2 #ovRoute.route-celeb #routeScroll { pointer-events: none; touch-action: none; }
/* Et le bouton refuse toute ambiguite de geste (double-tap, zoom) : sa zone est a lui seul. */
body.ui-v2 #ovRoute .route-celeb-btn { touch-action: manipulation; }

/* ======================================================================================================
   30 · LES ETIQUETTES DE RARETE
   Elles portaient chacune leur teinte en TEXTE sur un fond de la MEME teinte a 13 % : « EPIC » violet
   sur violet, « COMMON » vert sur vert. Sur le fond clair d'origine ca tenait ; sur le velours, le
   contraste tombe entre 1,6 et 2,3 pour 1 - c'est-a-dire sous le seuil de lisibilite d'un petit texte
   sur telephone (mesure : `scripts/audit-skin-contraste.mjs`).

   Le handoff donne une seconde gamme, plus claire, faite pour un fond sombre : rare `#8ECBFF`, epique
   `#C9A0FF`, legendaire `#F2C463`, mythique `#FF9FB4`, cosmetique `#FFD0EA`, bonus `#9FE6C0`. Le fond
   de la pastille devient neutre : c'est le TEXTE qui porte la rarete, pas la pastille - « Text only,
   never the only signal », et le nom ecrit reste de toute facon le signal principal.
   ====================================================================================================== */
body.ui-v2 .rar {
  background: rgba(0, 0, 0, .32) !important;
  border: 1px solid rgba(255, 255, 255, .10) !important;
}
body.ui-v2 .rar.common   { color: var(--vv-lbl-bonus, #9FE6C0) !important; }
body.ui-v2 .rar.rare     { color: #8ECBFF !important; }
body.ui-v2 .rar.epic     { color: #C9A0FF !important; }
body.ui-v2 .rar.legendary{ color: var(--vv-gold-text) !important; }
body.ui-v2 .rar.mythic   { color: #FF9FB4 !important; }
body.ui-v2 .rar.cosmetic { color: #FFD0EA !important; }
/* Les emplacements d'ecrin de l'accueil gardent leur pastille PLEINE : la, elle est posee a cheval sur
   la bordure d'une carte et sert de titre, pas d'etiquette dans une liste. */
body.ui-v2[data-view="home"] .box-slot .rar {
  background: linear-gradient(var(--rar-lo, #fff), var(--rar-c, #ccc)) !important;
  border: 2.5px solid rgba(6, 3, 20, .6) !important;
  color: var(--rar-ink, #2b1c00) !important;
}

/* Deux libelles secondaires ecrits dans un mauve sombre, hors de l'echelle de gris du systeme. */
body.ui-v2 .cl-stat-lbl { color: rgba(255, 255, 255, .55) !important; }
body.ui-v2 .odds-item-head { color: rgba(255, 255, 255, .62) !important; }
/* Les derniers micro-libelles sous le seuil. Le systeme place ses libelles a `rgba(255,255,255,.5)`,
   ce qui suffit sur `#1D1032` mais frole le seuil sur `#33204C`, le haut du degrade de panneau : on
   monte donc d'un cran ceux qui vivent sur la partie claire du panneau, et on rend en BLANC ce qui
   est une valeur plutot qu'un libelle - un pourcentage se lit, il ne s'annote pas. */
body.ui-v2 .odds-item-head b, body.ui-v2 .odds-sub b { color: #fff !important; }
body.ui-v2 .cl-stat-lbl, body.ui-v2 .odds-item-head { color: rgba(255, 255, 255, .70) !important; }
body.ui-v2 .overlay .panel a, body.ui-v2 .legal-links a { color: rgba(255, 255, 255, .78) !important; }
body.ui-v2 .gg-mult-lbl, body.ui-v2 #ovGemGuide em { color: rgba(255, 255, 255, .70) !important; }

/* ======================================================================================================
   31 · L'ACCUEIL DU PREMIER LANCEMENT
   Le tout premier ecran qu'un nouveau joueur voit - et le seul qu'aucun audit n'avait ouvert : la
   sauvegarde de demonstration porte `onboarded: true`, donc les instruments passaient directement au
   menu. Sur le deploiement, la trame de losanges y etait toujours la, le bandeau de consentement
   repondait en vert, et le plateau de demonstration gardait les couleurs de la V1.
   ====================================================================================================== */
/* La trame de losanges vit sur DEUX selecteurs, groupes dans la meme regle : `.ambient::before` et
   `.welcome-ov::before`. N'en eteindre qu'un laisse le motif exactement la ou il compte le plus. */
body.ui-v2 .welcome-ov::before, .welcome-ov::before { display: none !important; }
.welcome-ov {
  background: radial-gradient(120% 70% at 50% 0%, #4B2A6B 0%, #2A1440 45%, #140823 100%) !important;
  box-shadow: inset 0 0 120px 20px rgba(10, 4, 20, .65) !important;
}
/* « Welcome! » etait ambre a contour brun, un lettrage de l'ancienne identite. Dans l'ecrin, un titre
   se lit en blanc et c'est l'or qui eclaire derriere lui. */
.welcome-ov .welcome-hello {
  color: #fff !important;
  -webkit-text-stroke: 0 !important;
  text-shadow: 0 0 30px rgba(242, 196, 99, .5) !important;
  letter-spacing: .06em !important;
}
.welcome-ov .welcome-board {
  background:
    linear-gradient(#1E1032, #180C29) padding-box,
    linear-gradient(#FFE9B0, #C08C2C 45%, #7A5312) border-box !important;
  border: 1.5px solid transparent !important;
  border-radius: 22px !important;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .5) !important;
}
/* `:not(.wc-f)` - MEME PIEGE QUE `.cell`, une troisieme fois. `background` est un raccourci : pose en
   `!important` sur la classe de base, il remet `background-color` a zero et efface les six blocs de
   couleur de la demonstration (`.wc-pk`, `.wc-bl`, `.wc-gr`, `.wc-yl`), qui n'ont pas d'`!important`.
   La demonstration d'onboarding montrait donc une grille entierement vide, ou l'on ne comprenait plus
   ni la ligne a completer ni ce que le bloc violet vient y faire - c'est-a-dire le seul geste que cet
   ecran doit enseigner. `.wc-f` marque une case PLEINE : elle garde sa couleur. */
.welcome-ov .wc-c:not(.wc-f) { background: rgba(255, 255, 255, .045) !important; }
.welcome-ov .wc-c { border-radius: 6px !important; }
/* `:not(.wc-clr)` - LE RELIEF APPARTIENT AU BLOC, PAS A LA CASE, et c'est ce que disait deja la regle
   d'origine (`box-shadow: none` sur `.wc-clr`). Les trois cases de la ligne du bas sont neutres : leur
   couleur ET leur relief vivent dans un `::after` qui s'efface a chaque tour d'animation. En posant le
   relief sur la CASE, je le laissais derriere quand le bloc disparaissait : trois fantomes restaient
   sur la ligne effacee, et le quatrieme emplacement - celui du bloc qu'on vient de poser, qui n'est
   pas une `.wc-c` mais l'element `.wc-drop` - restait propre. D'ou une ligne qui s'efface a moitie. */
.welcome-ov .wc-c.wc-f:not(.wc-clr) { box-shadow: inset 0 2px 0 rgba(255, 255, 255, .18), 0 2px 0 rgba(0, 0, 0, .35) !important; }

/* LE BANDEAU DE CONSENTEMENT. Il repondait « OK » en vert plein - la couleur que le systeme reserve a
   ce qu'on GAGNE. Accepter une mesure d'audience n'est pas un gain ; c'est un choix neutre, et les
   deux reponses doivent avoir le meme poids visuel pour que le choix reste libre. Les deux boutons
   partagent donc la meme forme, et seul le contour distingue celui qui valide. */
#pp-consent {
  background: linear-gradient(#33204C, #1D1032) !important;
  border: 1.5px solid rgba(242, 196, 99, .30) !important;
  color: #fff !important;
}
.pp-consent-b {
  background: rgba(0, 0, 0, .30) !important;
  border: 1.5px solid rgba(242, 196, 99, .45) !important;
  color: #F2C463 !important;
  box-shadow: 0 3px 0 rgba(0, 0, 0, .45) !important;
}
.pp-consent-b:active { transform: translateY(3px); box-shadow: 0 0 0 rgba(0, 0, 0, .45) !important; }
.pp-consent-b.ghost {
  background: transparent !important;
  border-color: rgba(255, 255, 255, .22) !important;
  color: rgba(255, 255, 255, .72) !important;
  box-shadow: none !important;
}

/* ======================================================================================================
   32 · LA BOUTIQUE : LES PACKS DE PIECES ET DE JOYAUX
   « 150 pièces », « 50 joyaux », leurs prix : tous écrits en `--dp-ink`, l'encre sombre du skin clair,
   par une règle `#view-shop …` en `!important`. Sur les cartes passées au velours, ça donnait du
   violet sombre sur violet sombre - illisible, et signalé par l'owner.

   POURQUOI LES DEUX AUDITS SONT PASSES A COTE : ils ignoraient tout élément hors du champ visible
   (`r.bottom < 0 || r.top > innerHeight`). Les packs vivent sous la ligne de flottaison de la
   boutique. Ce filtre est retiré : un élément qui n'est pas à l'écran a quand même un style calculé,
   et les vues masquées sont déjà exclues par leur taille nulle.
   ====================================================================================================== */
body.ui-v2 #view-shop .shop-name,
body.ui-v2 #view-shop .card-title,
body.ui-v2 #view-shop .featured-body { color: #fff !important; }
body.ui-v2 #view-shop .card-foot:not(.rotation-note),
body.ui-v2 #view-shop .shop-sub { color: rgba(255, 255, 255, .66) !important; }
/* Le prix est une VALEUR : il se lit en or, comme partout ailleurs dans le jeu. */
body.ui-v2 #view-shop .pack-price { color: var(--vv-gold-text) !important; }

/* ======================================================================================================
   33 · QUATRE DEFAUTS REVELES PAR L'AUDIT ELARGI
   En cessant d'ignorer ce qui est hors du champ visible, l'audit a montre quatre elements qui
   n'avaient jamais ete regardes.
   ====================================================================================================== */

/* 1. LA ROUE DE LA FORTUNE. Un degrade orange -> rose -> violet vif, contour blanc de 2,5px : le seul
   element du jeu entierement hors de la palette, et le plus voyant de la boutique. Ses deux textes
   tombaient a 2,8 et 2,0 pour 1. Elle garde son role - c'est une invitation - mais le prend dans le
   vocabulaire de l'ecrin : panneau serti, et une lueur d'or derriere quand un tour est disponible.
   L'or attire l'oeil aussi bien que le rose, et lui veut dire quelque chose. */
/* L'icone du bouton = une roue MINIATURE : la monture `wheel.webp` posee sur les quartiers du jour.
   Seule, cette monture est un anneau vide - on reconnaitrait un cerceau, pas une roue de la fortune. */
body.ui-v2 .shop-wheel-card .sw-wheel { position: relative; display: block; width: 44px; height: 44px; flex: 0 0 auto; }
body.ui-v2 .shop-wheel-card .sw-wheel > i {
  position: absolute; inset: 14%; border-radius: 50%; display: block;
  box-shadow: inset 0 0 6px rgba(0, 0, 0, .45);
}
body.ui-v2 .shop-wheel-card .sw-wheel > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

body.ui-v2 .shop-wheel-card {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
  color: #fff !important;
  box-shadow: 0 0 22px rgba(242, 196, 99, .22), 0 6px 18px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 .shop-wheel-card .sw-txt b { color: #fff !important; }
body.ui-v2 .shop-wheel-card .sw-txt span { color: var(--vv-gold-text) !important; }

/* 2. LES CARTES DE PACK gardaient le relief du skin clair : un liseré interne blanc a 85 % et une
   lèvre claire en bas, dessinés pour une carte blanche. Sur le velours, ils cerclaient chaque pack
   d'argent. La profondeur du systeme est une ombre portee, pas un biseau. */
body.ui-v2 #view-shop .card {
  box-shadow: 0 6px 18px rgba(0, 0, 0, .40) !important;
}

/* 3. LE NOEUD DE NIVEAU BOSS restait dans les couleurs de la V2 - vert, bleu, gris selon l'etat -
   parce que ses trois regles d'etat passent par `#ovRoute` : un IDENTIFIANT, que mes regles de
   classe ne pouvaient pas battre, si specifiques soient-elles.

   J'ai d'abord voulu lui rendre le cramoisi de la decision D3. C'etait une erreur, et le code le
   disait : une decision owner PLUS RECENTE l'a explicitement retire - « le noeud du boss n'impose
   plus SA couleur : il reprend celle de son ETAT, comme tous les autres. Il reste identifiable par
   son anneau. Un jalon ne doit se distinguer ni par sa couleur ni par son contour, seulement par ce
   qu'il contient. » On ne restaure donc rien : on fait simplement entrer ce noeud dans les couleurs
   d'etat du nouveau systeme, comme les autres. */
body.ui-v2 #ovRoute .rt-node-boss {
  background: var(--vv-panel-hi) !important;
  border: 2px solid var(--vv-line) !important;
  color: rgba(255, 255, 255, .8) !important;
  box-shadow: 0 3px 7px rgba(0, 0, 0, .45) !important;
}
body.ui-v2 #ovRoute .rt-row.done .rt-node-boss {
  background: linear-gradient(var(--vv-btn-1), var(--vv-gold-3)) !important;
  border-color: var(--vv-gold-2) !important;
  color: var(--vv-on-gold) !important;
}
body.ui-v2 #ovRoute .rt-row.next .rt-node-boss {
  background: linear-gradient(var(--vv-rank-2), var(--vv-rank-1)) !important;
  border-color: #fff !important;
  color: #123049 !important;
}
body.ui-v2 #ovRoute .rt-row.locked .rt-node-boss,
body.ui-v2 #ovRoute .rt-row.rt-pending .rt-node-boss {
  background: var(--vv-panel-hi) !important;
}
/* Meme cause pour la carte d'objectif de type C et son libelle ambre : `#ovRoute` a nouveau. */
body.ui-v2 #ovRoute .rt-obj-card.tone-obj-C .rt-obj-lbl { color: #FF8FA3 !important; }
/* Le libelle « Desormais dans les ecrins », en violet sombre sur velours (1,99:1). */
body.ui-v2 .rt-gem-lbl, body.ui-v2 .rt-fam-txt span { color: rgba(255, 255, 255, .72) !important; }

/* 4. La note des chances, ecrite dans le mauve sourd du skin clair. */
body.ui-v2 .odds-note { color: rgba(255, 255, 255, .70) !important; }

/* ======================================================================================================
   34 · LA CASE SOUS UNE GEMME
   « Il y a toujours une sorte de case de fond derriere les gemmes. Les gemmes doivent sembler etre
   deposees sur une case vide. » (owner, 2026-08-25)

   Le logement sous une gemme est VOULU - c'est une decision owner du 2026-08-14, « conserver la case
   de fond, et poser les gemmes et les blocs DESSUS », sans quoi la pierre flotte dans un trou de la
   grille. Le defaut n'etait donc pas sa presence, mais sa VALEUR : la regle recopiait le fond de case
   vide d'alors, `hsl(var(--h) 62% 6% / .26)`, et je l'ai laissee derriere quand la case vide est
   passee a `rgba(255,255,255,.045)`. Les deux ont diverge, et la case sous une gemme est devenue plus
   sombre que ses voisines - donc visible comme un carre.

   Elle reprend ici, a l'identique, le fond ET le creux d'une case vide. Une seule source pour les
   deux : si l'une bouge, l'autre doit bouger avec elle. */
body.ui-v2 .cell.filled.gem.gemv2:not(.gemv2-flat) {
  background-color: rgba(255, 255, 255, .045) !important;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5) !important;
}

/* LES BLOCS A MOTIF REJOIGNENT LES GEMMES (owner 2026-09-07) : « retire les blocs de fond des
   thematiques animales ; comme pour les gemmes ils doivent donner l'impression d'etre poses SUR le
   plateau, pas d'etre poses sur un bloc sombre qui est pose sur le plateau ».

   C'est LE MEME DEFAUT que celui reparé juste au-dessus, une case plus loin. Le carre de COULEUR
   etait bien parti le 2026-08-14 ; dessous restait `hsl(--h 62% 6% / .26)` (style.css), qui n'etait
   pas une invention mais la valeur de la case VIDE de l'epoque, recopiee pour que le logement d'un
   motif ressemble a un logement. La case vide est passee depuis a `rgba(255,255,255,.045)`. Les deux
   ont diverge, et un chat se retrouvait dans un creux plus sombre que ses voisines : exactement le
   carre qu'on croyait avoir retire.

   `!important` sur les deux proprietes, pour la meme raison que la regle des gemmes : `.cell.filled`
   pose plus haut une facette en relief (`inset 0 2px 0 blanc, inset 0 -4px 0 noir`) en `!important`,
   et c'est elle qui fait lire une case comme de la PIERRE TAILLEE. Sur un aplat c'est juste ; sous un
   motif, elle redessine le bloc qu'on cherche a effacer.

   Les MATIERES portent aussi `.sprite` et ne bougent pas d'un pixel : leur tuile remplit la case a
   100 % (zoom 1.185, mesure), donc rien de ce qu'on change dessous ne se voit. Les COULEURS ne sont
   pas concernees, elles ne portent pas `.sprite` - leur aplat EST le bloc. */
body.ui-v2 .cell.filled.sprite {
  background-color: rgba(255, 255, 255, .045) !important;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5) !important;
}

/* ======================================================================================================
   35 · LES DERNIERS CADRES DU SKIN CLAIR (succes, classement)
   « Il y a encore des anciens cadres dans la page des succes et dans la page du leaderboard. »
   (owner, 2026-08-25)

   Trois regles du bloc inline peignent le meme relief « bold » : un lisere INTERNE presque blanc
   (`inset 0 2px 0 rgba(255,255,255,.85)`) et une levre claire dessous. C'etait la recette des cartes
   BLANCHES de la V1 ; sur le velours, elle cercle chaque carte d'argent.

   Elles sont ancrees sur un IDENTIFIANT (`#view-goals`, `#profilePaneBoard`), donc aucune regle de classe ne
   peut les battre - c'est pour ca que la section 13 les avait manquees alors qu'elle nommait bien
   `.ach-row`. Il faut reprendre l'identifiant. La boutique avait deja ete corrigee ainsi (section 33) ;
   ces deux vues sont les deux dernieres de la meme famille.

   Elles n'ont pas ete trouvees a l'oeil : `scripts/tmp-relief.mjs` a cherche le MOTIF - tout element
   portant une ombre interne dont un canal depasse 195 - et n'en a rendu que deux.
   ====================================================================================================== */
body.ui-v2 #view-goals .daily-card,
body.ui-v2 #view-goals .ach-row,
body.ui-v2 #profilePaneBoard .card,
body.ui-v2 #profilePaneBoard #boardList > li {
  box-shadow: 0 5px 14px rgba(0, 0, 0, .40) !important;
}
/* La carte d'identite du classement porte le nom du joueur : elle merite la monture, comme la carte
   de rang de l'accueil. C'est la seule de cet ecran qui parle de LUI. */
body.ui-v2 #profilePaneBoard .card.profile-card {
  background:
    linear-gradient(var(--vv-panel-hi), var(--vv-panel-lo)) padding-box,
    linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box !important;
  border: 1.5px solid transparent !important;
}

/* ======================================================================================================
   36 · L'ECRAN DE CHARGEMENT
   Il portait une illustration plein cadre. Il porte maintenant une pierre : un brillant dore qui
   flotte et tourne sur le velours de l'ecrin, un titre, une signature, une barre, et une phrase.

   LA PIERRE TOURNE VRAIMENT EN 3D, et pourtant aucun moteur 3D ne tourne ici. Elle est rendue par le
   MEME viseur que l'atelier, sur 48 prises de vue a 360 degres, cuites en une planche de sprites
   (`scripts/bake-splash-gem.mjs`). Monter three.js - 1,0 Mo vendorise - plus un contexte WebGL avant
   d'afficher quoi que ce soit rendrait plus lent exactement ce que cet ecran sert a couvrir. Ce qui
   part dans l'app est une image de 272 Ko ; elle remplace 464 Ko d'illustration.

   `steps(48)` et non une transition continue : chaque image de la planche doit s'afficher NETTE, une
   interpolation ferait un fondu entre deux poses, c'est-a-dire un flou.
   ====================================================================================================== */
#splash {
  background: radial-gradient(110% 62% at 50% 30%, #4B2A6B 0%, #2A1440 46%, #140823 100%) !important;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0;
}
#splash .sp-cover, #splash .sp-dots { display: none !important; }   /* l'illustration et ses trois points */

/* LA PIERRE EST UN VRAI RENDU 3D, monte par src/game/27-splash-3d.js, et non plus une planche de
   sprites. Le detail du choix - et pourquoi c'est l'ecran de STUDIO qui le rend possible - est dans
   ce fichier. Ici il ne reste que la boite et son flottement : le canevas se cale dedans tout seul.

   Elle est PLUS GRANDE et PLUS HAUTE qu'avant (owner). L'espace vient d'un `margin-top` negatif : la
   colonne du splash est centree, et pousser la pierre vers le haut sans deplacer le titre demande de
   lui reprendre de la place, pas d'en ajouter en bas. */
@keyframes sp-flotte { 0%, 100% { transform: translateY(-7px); } 50% { transform: translateY(7px); } }
#splash .sp-gem {
  position: relative;
  width: min(62vw, 250px); aspect-ratio: 1;
  margin-top: -8vh;
  filter: drop-shadow(0 18px 30px rgba(0, 0, 0, .55)) drop-shadow(0 0 30px rgba(242, 196, 99, .35));
  animation: sp-flotte 5s ease-in-out infinite;
  /* La pierre se FAIT TOURNER au doigt : gem3d ecoute le pointeur. `touch-action: none` empeche le
     navigateur de prendre le geste pour un defilement et de couper la rotation au premier pixel. */
  touch-action: none;
  cursor: grab;
}
#splash .sp-gem canvas { width: 100% !important; height: 100% !important; display: block; }
#splash .sp-gem:active { cursor: grabbing; }

#splash .sp-name {
  margin-top: 14px;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-weight: 400;
  font-size: 44px; letter-spacing: .12em;
  background: linear-gradient(#FFF3CE, #FFD983 42%, #C08C2C);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 2px 0 rgba(90, 60, 12, .55)) drop-shadow(0 0 22px rgba(242, 196, 99, .30));
}
#splash .sp-tag {
  margin: 6px 0 0;
  font-family: 'Nunito', system-ui, sans-serif; font-weight: 900;
  font-size: 10.5px; letter-spacing: .24em; text-transform: uppercase;
  color: rgba(255, 255, 255, .62);
}
/* La barre est INDETERMINEE, et c'est volontaire : le jeu ne sait pas dire ou il en est de son
   chargement, et une jauge qui avance a une vitesse inventee promet une progression qu'elle ne mesure
   pas. Un balayage dit « ca travaille » sans mentir sur le combien. */
@keyframes sp-balaye { 0% { left: -38%; } 100% { left: 100%; } }
#splash .sp-bar {
  position: relative; overflow: hidden;
  width: min(52vw, 210px); height: 6px; margin-top: 34px;
  background: rgba(0, 0, 0, .45);
  /* FUSELEE JUSQU'A LA POINTE (owner). L'ellipse fait exactement la largeur de la barre : elle la
     touche a x = 0 et x = 100 %, ou sa hauteur est nulle. Les deux bouts se terminent donc en pointe,
     pas en bout arrondi - a 60 % de demi-largeur ils gardaient encore 55 % de l'epaisseur du milieu,
     ce qui se lisait comme une barre a coins ronds.
     `clip-path` et non `-webkit-mask` : sur iOS, un masque pose sur un element dont un enfant s'anime
     fait disparaitre l'enfant (le meme piege a coute les gemmes du plateau en son temps). */
  clip-path: ellipse(50% 50% at 50% 50%);
}
#splash .sp-bar > i {
  position: absolute; top: 0; bottom: 0; width: 38%;
  background: linear-gradient(90deg, transparent, #F2C463, #FFE9B0, #F2C463, transparent);
  animation: sp-balaye 1.5s ease-in-out infinite;
}
#splash .sp-note {
  margin: 12px 0 0;
  font-family: 'Nunito', system-ui, sans-serif; font-weight: 900;
  font-size: 9.5px; letter-spacing: .18em; text-transform: uppercase;
  color: rgba(255, 255, 255, .42);
}
/* L'ornement tombe, l'ecran reste : sans mouvement, la pierre s'arrete sur une pose nette et la barre
   se fige remplie. Rien ne clignote, et l'ecran dit toujours « ca charge ». */
@media (prefers-reduced-motion: reduce) {
  #splash .sp-gem, #splash .sp-gem > i, #splash .sp-gem > i > b { animation: none; }
  #splash .sp-bar > i { animation: none; left: 0; width: 100%; }
}

/* ======================================================================================================
   37 · LA LIGNE QUI S'EFFACE REDEVIENT UNE LIGNE VIDE
   Apres l'effacement, les trois cases du bas restaient d'une teinte differente de leurs voisines :
   `hsl(var(--n-hi-surface) / .1)` - un blanc TEINTE a 10 % - la ou une case vide est a
   `rgba(255,255,255,.045)`. Deux valeurs pour une meme chose, ecrites a deux epoques.

   C'est la troisieme fois que ce meme ecart se produit (la case sous une gemme, section 34, puis le
   plateau, section 11) : une regle recopie le fond de case vide au lieu de le PARTAGER, puis la case
   vide change et la copie reste. Elles pointent maintenant toutes vers la meme valeur.
   ====================================================================================================== */
body.ui-v2 #ovWelcome .wc-c.wc-clr {
  background: rgba(255, 255, 255, .045) !important;
}
/* SAUF LA CASE DE LA GEMME (owner 2026-09-05 : « elle a un fond derriere elle qui rend sa case
   etrange »). Une case de bloc est une surface ; une pierre n'en est pas une - lui laisser le creux
   d'une case vide la fait lire comme un bloc mal peint.
   ET C'EST ICI QUE LA REGLE DOIT VIVRE, pas dans style.css : la regle juste au-dessus a exactement la
   meme specificite (un id, trois classes, un element), et velvet.css est charge APRES style.css. Une
   surcharge posee la-bas perdait donc la cascade en silence - le `box-shadow` passait, le `background`
   non, ce qui est le genre d'ecart qu'on ne voit qu'a l'ecran.
   `.wc-f` est ECRIT dans le selecteur bien qu'il soit redondant : le garde-fou « le fond des cases
   VIDES ne deborde jamais sur les cases PLEINES » (velvet-skin.test.js) exige qu'une regle de fond sur
   `.wc-c` nomme l'etat PLEIN qu'elle vise. Le dire ici coute un mot et evite d'elargir le garde-fou. */
/* RETOURNEMENT ASSUME (owner 2026-09-08 : « ajoute une case vide derriere elle, actuellement elle
   n'a pas de case vide derriere elle »). Le 5 septembre il demandait l'inverse - « elle a un fond
   derriere elle qui rend sa case etrange » - et les deux demandes sont justes, parce qu'elles ne
   parlent pas du meme fond. Ce qui genait alors, c'etait le fond d'un BLOC : une surface pleine avec
   son relief, sous une pierre qui n'en est pas une. Ce qu'il reclame aujourd'hui, c'est le LOGEMENT
   d'une case vide - exactement ce qu'une gemme a sur le vrai plateau.
   La valeur est donc la meme que partout ailleurs, et c'est tout l'interet : case vide, case-gemme et
   case-motif du plateau, case-gemme de l'onboarding, une seule apparence de logement. C'est la
   quatrieme fois que cette valeur devait etre partagee plutot que recopiee (sections 11, 34, et le
   plateau des blocs a motif) ; ici elle l'est. */
body.ui-v2 #ovWelcome .wc-c.wc-f.wc-gem-cell {
  background: rgba(255, 255, 255, .045) !important;
  /* LE FOND SEUL, PAS LE CREUX. Sur le vrai plateau une case vide porte aussi un `inset 0 1px 2px` ;
     ici, non - les cases vides de l'onboarding n'ont jamais eu que le fond (cf. `.wc-c:not(.wc-f)`
     plus haut). C'est SA voisine que cette case doit imiter, pas celle d'un autre ecran.
     Et le creux serait un piege : sur la ligne qui s'efface, le relief appartient au `::after` du
     BLOC, qui disparait avec l'animation. Une ombre posee sur la CASE lui survivrait et laisserait un
     fantome sur une ligne censee etre vide - c'est exactement ce que garde velvet-skin.test.js.
     `none !important` reste necessaire : `.wc-c.wc-f` (style.css) pose sinon le relief d'un bloc. */
  box-shadow: none !important;
}

/* ======================================================================================================
   38 · LES FRAGMENTS DE GEMMES VOISINES SUR LE PLATEAU
   En revenant a l'atlas, chaque case-gemme montrait des eclats des quatre cellules voisines colles a
   ses bords : un liseré vert a gauche, gris a droite, bleu en haut, un triangle rouge en bas.

   Ce n'est pas un debordement d'echantillonnage - j'ai commence par la, et doubler la marge
   transparente autour de chaque vignette n'y a rien change. C'est une regle de peinture :
     - `background-size: 1000% 900%` se resout sur la boite de PADDING ;
     - `background-clip` vaut `border-box` par defaut, donc le fond est PEINT jusqu'au bord exterieur.
   La case-gemme porte justement une bordure de 2 px rendue TRANSPARENTE (style.css, pour qu'elle garde
   exactement la meme boite que les autres cases). Le fond deborde donc de 2 px de tous les cotes, et
   dans cet anneau on voit ce qui, dans l'atlas, entoure la vignette : ses voisines.

   `background-clip: padding-box` arrete la peinture au bord interieur. La boite ne bouge pas d'un
   pixel, la grille non plus. */
body.ui-v2 .cell.filled.gem.gemv2,
body.ui-v2 .pcell.gem.gemv2 {
  background-clip: padding-box !important;
}

/* ======================================================================================================
   39 · LA ROUE DE LA FORTUNE, NOUVEAU CADRE
   `wheel.webp` (owner) remplace `fortune_wheel_frame.webp`. Les deux ne se calent pas pareil : l'ancien
   portait un SOCLE sous l'anneau, donc son cercle interieur n'etait pas au centre de l'image, et le
   CSS compensait par un decalage vertical (`top: 7.2%` contre `left: 10.1%`). Le nouveau est un anneau
   pur, centre et carre.

   Les valeurs viennent d'une mesure de l'image, pas d'un reglage a l'oeil : en balayant depuis le
   centre, le moyeu s'arrete a 17 % du rayon, le vide va jusqu'a 75,4 %, et l'anneau occupe 75,4 % a
   96,9 %. Le disque fait donc 78 % de large (un poil plus que les 75,4 %, pour que l'anneau recouvre
   son bord au lieu de laisser un liseré), centre a 11 %.
   ====================================================================================================== */
/* LE VOILE BLANC DU DISQUE, RAMENE DE 42 % A 18 % (owner 2026-08-27 : « moins pastel »).
   Il ne suffisait pas d'assombrir les quartiers : `.wheel-disc::before` posait un halo blanc a 42 %
   sur la moitie haute du disque. MESURE : il remontait des tons a 31-46 % de luminosite jusqu'a
   60-69 % - precisement la plage pastel dont on sortait, et seulement sur les quartiers du haut, ce
   qui donnait une roue a deux regimes. A 18 % ils retombent a 43-56 % : le disque garde son relief
   (il doit rester un objet eclaire, pas un aplat) sans redelaver la couleur.
   L'ombre du bas est renforcee en echange : c'est elle, et non le halo, qui donne le volume. */
body.ui-v2 .wheel-disc::before {
  background:
    radial-gradient(circle at 50% 30%, rgba(255, 255, 255, .18), rgba(255, 255, 255, 0) 54%),
    radial-gradient(circle at 50% 60%, rgba(0, 0, 0, 0) 48%, rgba(0, 0, 0, .38) 100%) !important;
}
body.ui-v2 .wheel-disc {
  width: 78% !important; height: 78% !important;
  left: 11% !important; top: 11% !important;
}
/* Le nouveau cadre porte SON moyeu, dessine dans l'image. Celui du CSS - un disque blanc cercle d'or -
   se posait par-dessus et faisait double. */
body.ui-v2 .wheel-hub { display: none !important; }

/* ======================================================================================================
   40 · L'ECLAT DES GEMMES
   « Elles ont perdu de leur couleur, elles tendent toutes vers les niveaux de gris. » (owner)

   La mesure dit qu'elles ne sont PAS desaturees a l'affichage : la case rendue sort a 0,604 de
   saturation moyenne quand la cellule d'atlas est a 0,567. Ce que l'oeil compare, c'est l'etape
   d'avant - le moteur 2D posait des aplats pleins cernes d'un contour, et un aplat chante toujours
   plus fort qu'un rendu photographique de 30 px, ou une bonne part de la surface part en speculaires
   blancs et en ombres.

   La reponse n'est donc pas de « reparer » une desaturation qui n'existe pas, mais de donner a la
   pierre l'eclat qu'on attend d'un bijou. Trois dosages mesures sur le plateau :
       aucun                                    0,631
       saturate(1.25)                           0,738
       saturate(1.45) contrast(1.05)            0,810   <- retenu
       saturate(1.7) contrast(1.08) brightness  0,845   trop : le rubis vire a l'aplat cramoisi
                                                        et perd le dessin de ses facettes
   Au-dela de 1,45 le gain de chiffre se paie en matiere perdue.

   LA LUEUR DE RARETE PASSE PAR UNE VARIABLE, et ce n'est pas de la coquetterie : `filter` est UNE
   propriete, et poser une saturation ici ecraserait les huit `drop-shadow` de rarete de style.css -
   les gemmes perdraient leur halo, c'est-a-dire le seul signal de rarete qu'elles portent en jeu. La
   variable les compose au lieu de les remplacer ; le repli vide (`var(--gem-lueur,)`) laisse la liste
   valide pour une commune, qui n'a pas de halo.
   ====================================================================================================== */
body.ui-v2 .cell.filled.gem.gemv2,
body.ui-v2 .pcell.gem.gemv2 {
  filter: saturate(1.45) contrast(1.05) var(--gem-lueur,) !important;
}
body.ui-v2 .cell.filled.gem.gemv2.rar-atypique,   body.ui-v2 .pcell.gem.gemv2.rar-atypique   { --gem-lueur: drop-shadow(0 0 1.5px color-mix(in srgb, var(--c) 40%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-rare,       body.ui-v2 .pcell.gem.gemv2.rar-rare       { --gem-lueur: drop-shadow(0 0 2.5px color-mix(in srgb, var(--c) 48%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-epique,     body.ui-v2 .pcell.gem.gemv2.rar-epique     { --gem-lueur: drop-shadow(0 0 3.5px color-mix(in srgb, var(--c) 55%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-legendaire, body.ui-v2 .pcell.gem.gemv2.rar-legendaire { --gem-lueur: drop-shadow(0 0 5px color-mix(in srgb, var(--c) 62%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-exotique,   body.ui-v2 .pcell.gem.gemv2.rar-exotique   { --gem-lueur: drop-shadow(0 0 6.5px color-mix(in srgb, var(--c) 68%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-mythique,   body.ui-v2 .pcell.gem.gemv2.rar-mythique   { --gem-lueur: drop-shadow(0 0 8px color-mix(in srgb, var(--c) 74%, transparent)); }
body.ui-v2 .cell.filled.gem.gemv2.rar-meteorite,  body.ui-v2 .pcell.gem.gemv2.rar-meteorite  { --gem-lueur: drop-shadow(0 0 6px color-mix(in srgb, var(--c) 80%, transparent)) drop-shadow(0 0 13px color-mix(in srgb, var(--c) 42%, transparent)); }

/* ======================================================================================================
   41 · L'ECRAN DE STUDIO
   Il precede le chargement et tient 2,2 s. Volontairement pauvre : une marque sur du blanc, rien a
   charger d'autre. C'est ce qui lui permet de s'afficher au premier rendu - et pendant qu'on le lit,
   WebGL se met en route derriere pour la pierre de l'ecran suivant. Un ecran deja PEINT reste correct
   meme quand le fil principal ne repond plus, ce qui est precisement le cas pendant cette chauffe.

   NOIR SUR BLANC (owner), a rebours du reste du jeu : une signature de studio n'appartient pas a
   l'univers du jeu, elle le precede. La rupture est le message.

   LE FONDU EST PORTE PAR UN SEUL ELEMENT, et c'est ce qui rend le nom et le logo rigoureusement
   synchrones : deux animations jumelles derivent toujours un peu (retard de compositing, arrondi de
   frame). Une seule opacite sur leur parent commun ne peut pas desynchroniser.
   ====================================================================================================== */
#studio {
  position: fixed; inset: 0; z-index: 10000;
  display: grid; place-items: center;
  background: #fff;
  transition: opacity .4s ease;
}
#studio.hide { opacity: 0; pointer-events: none; }
@keyframes st-souffle {
  0%   { opacity: 0; transform: scale(.985); }
  20%  { opacity: 1; transform: scale(1); }
  76%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.012); }
}
#studio .st-in {
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  /* La duree suit STUDIO_MS (src/game/27-splash-3d.js). Si l'une bouge, l'autre doit suivre : sans
     ca le fondu de sortie se ferait couper net par la disparition de l'ecran, ou le laisserait blanc
     un instant apres s'etre eteint. */
  animation: st-souffle 2.2s cubic-bezier(.4, 0, .2, 1) both;
}
/* `brightness(0)` rend chaque pixel opaque NOIR en gardant l'alpha : le logo est fourni en trait gris,
   et c'est la facon la plus sure de le passer au noir sans retoucher le fichier ni perdre son detourage. */
#studio .st-logo { width: min(30vw, 116px); height: auto; display: block; filter: brightness(0); }
#studio .st-mark {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-weight: 400;
  font-size: 22px; letter-spacing: .26em; line-height: 1.2; text-align: center;
  color: #000;
}
#studio .st-mark span { display: block; font-size: 15px; letter-spacing: .34em; }

/* L'ecran de chargement n'apparait qu'une fois le studio passe (classe posee par 27-splash-3d). Sans
   ca les deux se superposeraient une fraction de seconde, et l'on verrait la pierre a travers. */
#splash { opacity: 0; transition: opacity .45s ease; }
#splash.show { opacity: 1; }
#splash.hide { opacity: 0 !important; }

/* ======================================================================================================
   42 · LA CARTE DE COLLECTION : MONTURE, ETIQUETTE, JAUGE (owner 2026-08-25)
   Trois demandes qui ne font qu'une : la carte doit se lire comme une piece SERTIE, et toutes les
   cartes d'une rangee doivent avoir exactement la meme hauteur de texte.
   ====================================================================================================== */

/* 1 · LA MONTURE D'OR POUR TOUTES. La section 10 ne la donnait qu'a partir de legendaire (et aux
   equipees) ; les autres portaient un or a 45 % d'opacite, qui lit comme du brun. L'owner veut la
   monture partout : c'est un ecrin, pas un catalogue. Le signal de rarete ne disparait pas pour
   autant - il reste porte par le HALO (`--rc` au sommet de la carte) et, a partir de legendaire, par
   la LUEUR exterieure que la regle de la section 10 continue d'ajouter. Ce qui change, c'est que la
   monture cesse d'etre une marche de rarete pour devenir le chassis commun. */
/* LA MONTURE PORTE LA RARETE (owner 2026-09-05 : « mets un vrai contour colore correspondant a la
   rarete de chaque gemme ; les gemmes possedees sont difficilement differenciables »).
   La monture etait DOREE pour toutes - un choix assume a l'epoque (« c'est un ecrin, pas un
   catalogue »), la rarete n'etant plus portee que par un halo a 24 % et une lueur a 26 %. A l'usage
   ces deux signaux sont trop faibles : sur une grille de trente-quatre pierres, on ne distingue pas
   une commune d'une legendaire, et c'est justement l'information qu'on vient chercher depuis que des
   niveaux exigent une rarete precise.
   Le contour prend donc la couleur de rarete (`--rc`, posee sur la carte par gemCardHTML depuis
   PrismGemRarityV2.hexOf), en degrade clair->fonc pour garder le relief. L'ECRIN NE DISPARAIT PAS
   POUR AUTANT : un filet d'or reste, en INSET, juste sous le contour - la carte est toujours montee,
   mais sa monture dit maintenant quelque chose. Et la lueur exterieure passe de 26 % a 45 % : c'est
   elle qui fait lire la rarete d'un coup d'oeil sur toute une grille, pas le trait de 2 px. */
/* DEUX DEFAUTS QUI SE CACHAIENT L'UN L'AUTRE, et qui expliquent le constat de l'owner (2026-09-05 :
   « les gemmes possedees sont toujours difficilement differenciables au niveau de leurs raretes »)
   alors qu'une regle de bordure coloree existe pourtant plus bas depuis le 2026-09-04.

   1. LE FOND ETAIT INVALIDE, donc IGNORE EN ENTIER. La monture etait peinte par la ruse classique du
      degrade de bordure : `radial-gradient(...), var(--vv-panel-flat) padding-box, linear-gradient(...)
      border-box`. Or dans le raccourci `background`, une COULEUR n'est admise que sur la DERNIERE
      couche - `--vv-panel-flat` est une couleur (#1E1031) posee au milieu, donc toute la declaration
      tombe. Mesure : `getComputedStyle(carte).backgroundImage` valait `none`. Ni la monture doree ni
      la monture de rarete n'ont jamais ete peintes ; ce qu'on voyait etait le cadre V1 de style.css.
      La couche du milieu devient donc un degrade constant, qui est une <image> et non une couleur.

   2. `border: ... solid transparent` TUAIT LA COULEUR DE RARETE. Cette regle-ci porte
      `:not(.is-locked)`, donc une classe de plus que `body.ui-v2 .gemcard` qui pose la bordure de
      rarete plus bas : elle gagnait la cascade malgre sa position anterieure, et imposait
      `transparent`. On ne fixe plus ici QUE l'epaisseur et le style ; la couleur appartient a la
      regle de rarete, qui est la seule a savoir quoi y mettre. */
body.ui-v2 .gemcard:not(.is-locked) {
  background:
    radial-gradient(120% 95% at 50% 12%, color-mix(in srgb, var(--rc) 28%, transparent), rgba(30, 16, 49, .94) 72%),
    linear-gradient(var(--vv-panel-flat), var(--vv-panel-flat)) !important;
  border-width: 2.5px !important;
  border-style: solid !important;
  box-shadow:
    inset 0 0 0 1px rgba(242, 196, 99, .45),
    inset 0 1.5px 0 rgba(255, 255, 255, .14),
    0 0 16px color-mix(in srgb, var(--rc) 45%, transparent),
    0 4px 0 rgba(0, 0, 0, .30),
    0 7px 16px rgba(0, 0, 0, .42) !important;
}
/* Et elle s'enfonce au tap, comme tout ce qui est cliquable dans ce jeu. */
body.ui-v2 .gemcard:not(.is-locked):active {
  transform: translateY(2px);
  box-shadow:
    inset 0 0 0 1px rgba(242, 196, 99, .40),
    inset 0 1.5px 0 rgba(255, 255, 255, .10),
    0 0 12px color-mix(in srgb, var(--rc) 34%, transparent),
    0 1px 0 rgba(0, 0, 0, .30),
    0 2px 7px rgba(0, 0, 0, .42) !important;
}

/* 2 · « EQUIPE » DEVIENT UNE ETIQUETTE POSEE SUR LA MONTURE. En flux, ce mot ajoutait une ligne a la
   carte : dans une rangee de trois, la carte equipee poussait son nom, son niveau et sa jauge un cran
   plus bas que ses voisines, et la rangee perdait son alignement (visible sur toute la grille). Sorti
   du flux, il ne compte plus dans la hauteur - les quatre lignes de texte sont les memes pour toutes.
   Il chevauche le bord haut de la monture, la ou il ne recouvre rien : la pierre commence plus bas.
   Fond sombre et non l'or, sinon l'etiquette se fondrait dans la monture qu'elle est censee marquer. */
body.ui-v2 .gemcard .prev-eq {
  position: absolute; top: 0; left: 50%; transform: translate(-50%, -52%);
  z-index: 4; pointer-events: none; white-space: nowrap;
  padding: 2px 9px 2.5px; border-radius: 99px;
  font-size: 8.5px; letter-spacing: .09em; line-height: 1;
  background: linear-gradient(180deg, #2a1740, #1b0f2c);
  border: 1px solid var(--vv-gold-4);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .55);
}
/* La carte equipee gardait sa monture d'or par la section 10 ; maintenant que toutes l'ont, son signal
   propre est l'etiquette PLUS une lueur doree franche - c'est le seul endroit ou l'or rayonne. */
body.ui-v2 .gemcard.equipped {
  box-shadow:
    inset 0 0 0 1px rgba(242, 196, 99, .75),
    inset 0 1.5px 0 rgba(255, 255, 255, .14),
    0 0 16px color-mix(in srgb, var(--rc) 45%, transparent),
    0 0 22px rgba(242, 196, 99, .30),
    0 4px 0 rgba(0, 0, 0, .30),
    0 7px 16px rgba(0, 0, 0, .42) !important;
}
body.ui-v2 .gemcard.equipped:active {
  box-shadow:
    inset 0 1.5px 0 rgba(255, 255, 255, .10),
    0 0 12px rgba(242, 196, 99, .28),
    0 1px 0 rgba(0, 0, 0, .30),
    0 2px 7px rgba(0, 0, 0, .42) !important;
}

/* 3 · LA JAUGE DE COPIES. 5 px moins 2 x 1,5 px de liseré = 2 px de remplissage : la barre lisait comme
   un cheveu. 9 px laissent 6 px de remplissage, soit trois fois plus, sans que la carte grandisse
   (la ligne de texte au-dessus est plus haute que la jauge). */
/* ======================================================================================================
   LA JAUGE DE COPIES (owner 2026-08-27)
   Quatre demandes, une par couche : plus large, le compte DEDANS, vert + monture doree quand la gemme
   est prete, et une fleche qui appelle l'oeil. La raison est economique, pas decorative : la montee de
   niveau est le principal puits de pieces du jeu, et une gemme prete qui ne se signale pas est un puits
   qui ne coule pas. Une grille de 34 cartes doit dire d'un coup d'oeil OU depenser.
   ====================================================================================================== */
body.ui-v2 .gem-bar {
  /* 18 px moins 2 x 1.5 px de lisere = 15 px de hauteur utile : de quoi loger un chiffre lisible.
     A 9 px la barre ne pouvait rien contenir, et le compte vivait au-dessus, en gris, a 11 px. */
  height: 18px !important;
  position: relative; overflow: hidden; border-radius: 999px !important;
  display: flex; align-items: center;
}
body.ui-v2 .gem-bar > i { border-radius: 999px; }
/* LE COMPTE, DANS LA BARRE. Centre, et lisible AUSSI BIEN sur le remplissage que sur le creux vide :
   c'est pourquoi il porte un contour sombre plutot qu'une couleur choisie pour l'un des deux fonds. */
body.ui-v2 .gem-bar-txt {
  position: absolute; left: 0; right: 0; text-align: center;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-weight: 400;
  font-size: 11px; line-height: 1; letter-spacing: .02em; color: #fff;
  -webkit-text-stroke: 2.4px rgba(12, 6, 22, .92); paint-order: stroke fill;
  text-shadow: 0 1px 0 rgba(12, 6, 22, .8);
}
body.ui-v2 .gem-bar-txt span { opacity: .72; font-size: 10px; }

/* PRETE A MONTER : la barre passe au VERT et prend une monture d'or. Le vert ne sert nulle part
   ailleurs sur ces cartes - il ne peut donc etre confondu avec une rarete. */
body.ui-v2 .gem-bar.pret {
  border-color: var(--vv-gold-3) !important;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .55), 0 0 10px rgba(111, 224, 160, .55) !important;
}
body.ui-v2 .gem-bar.pret > i {
  background: linear-gradient(90deg, #3fbf78, #6fe0a0 55%, #9ff2c4) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5) !important;
}
/* LA BRILLANCE qui balaie la barre pleine. Lente et large : c'est un objet precieux qui accroche la
   lumiere, pas un bouton qui clignote. */
body.ui-v2 .gem-bar.pret::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: -45%; width: 38%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .72), transparent);
  transform: skewX(-18deg);
  animation: gemPretLustre 2.4s cubic-bezier(.42, 0, .3, 1) infinite;
}
@keyframes gemPretLustre { 0% { left: -45%; } 55%, 100% { left: 115%; } }

/* LA FLECHE. Elle pointe vers le haut - vers le niveau suivant - et rebondit. Posee en HAUT A DROITE
   de la carte, hors du flux : ajoutee dans le flux, elle decalerait le texte des cartes pretes et la
   rangee perdrait son alignement (meme lecon que l'etiquette « EQUIPE »). */
body.ui-v2 .gemcard { position: relative; }
body.ui-v2 .gem-fleche {
  position: absolute; top: 6px; right: 6px; width: 20px; height: 20px; z-index: 5;
  pointer-events: none;
  background: linear-gradient(180deg, var(--vv-gold-2), var(--vv-gold-4));
  clip-path: polygon(50% 0%, 100% 52%, 72% 52%, 72% 100%, 28% 100%, 28% 52%, 0% 52%);
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .6)) drop-shadow(0 0 6px rgba(242, 196, 99, .8));
  animation: gemFlecheBond 1.15s cubic-bezier(.34, 1.5, .5, 1) infinite;
}
@keyframes gemFlecheBond {
  0%, 62%, 100% { transform: translateY(0); }
  22% { transform: translateY(-6px); }
  40% { transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 .gem-bar.pret::after, body.ui-v2 .gem-fleche { animation: none; }
}

/* 4 · LA GRILLE D'ECRINS DE LA BOUTIQUE, DE NOUVEAU A TROIS COLONNES (owner 2026-09-05 : « mets les
   ecrins comme les power-ups en 3 colonnes, comme pour les autres sections, ce sera plus harmonieux »).
   Elle etait passee a deux le jour ou l'on croyait n'avoir plus que quatre ecrins - trois sur une
   rangee et un orphelin sur la seconde se lisent comme une grille inachevee. La premisse a vieilli :
   SHOP_BOX_RARITIES en compte SIX (commun, atypique, rare, epique, legendaire, exotique), donc trois
   colonnes donnent deux rangees pleines, et le rayon s'aligne enfin sur ses voisins.
   `minmax(0, 1fr)` et non `1fr` : une piste de grille a `min-width: auto`, donc une tuile plus large
   que sa part imposerait sa taille et ferait deborder le rayon - c'est exactement ce qui arrivait aux
   tuiles de power-up, dont l'icone porte un cadre qui deborde volontairement de l'image. */
body.ui-v2 .shop-box-grid { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }

/* ======================================================================================================
   43 · TOURNOIS ET DEFIS (Gemz V2)
   Deux ecrans neufs et une rangee d'acces sur l'accueil. Tout y reprend les primitives deja posees -
   monture d'or, panneau de velours, creux sombre - parce qu'un ecran qui invente sa propre matiere se
   lit comme une piece rapportee, et ces deux-la sont censes faire partie du jeu depuis toujours.

   Les deux vues doivent aussi etre DECLAREES ici : `.view { display: none }` vit dans style.css, dont
   la region d'en-tete est figee par empreinte (test/game-build.test.js). Une regle de meme specificite
   posee plus tard l'emporte par l'ordre du document - c'est exactement le mecanisme sur lequel repose
   tout ce fichier, et il evite de rouvrir une region protegee pour deux lignes de structure.
   ====================================================================================================== */
body[data-view="tournoi"] #view-tournoi,
body[data-view="defis"] #view-defis { display: flex; overflow-x: hidden; overflow-y: auto; }
body[data-view="tournoi"] #btnBack,
body[data-view="defis"] #btnBack { display: inline-flex; }

/* --- L'ACCUEIL : la rangee d'acces rapide -------------------------------------------------------
   Deux cartes de meme poids, dans l'espace qui restait vide entre la barre de rang et les ecrins.
   Elles ne rivalisent pas avec JOUER : pas d'or plein, une monture fine et un fond de panneau. */
/* L'ORDRE. L'accueil range ses enfants par `order` explicite (bloc inline d'index.html : identite 1,
   barre de rang 2, ecrins 3, JOUER 4). Sans valeur, cette rangee heritait de 0 et passait AVANT tout
   le reste - au-dessus de la barre de rang, pas en dessous. `order: 2`, la meme valeur que la barre
   de rang : a egalite, c'est l'ordre du DOM qui tranche, et la rangee y vient apres elle. Une seule
   ligne, et rien a rouvrir dans le bloc fige d'index.html.
   LA HAUTEUR. L'accueil ne defile pas (overflow hidden) : ce qui deborde est perdu. Les cartes sont
   donc COUCHEES - icone et libelle cote a cote - et tiennent en 54 px au lieu de 94. C'est aussi ce
   que l'owner decrivait : « l'espace vide » entre la barre de rang et les ecrins, pas une rangee de
   tuiles qui rivaliserait avec eux. */
body.ui-v2 .home-quick { order: 2; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin: 8px auto 2px; width: 100%; max-width: 330px; }
body.ui-v2 .hq-card {
  position: relative; display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8px;
  padding: 9px 10px; min-height: 54px; border: 1.5px solid transparent; border-radius: 15px; cursor: pointer;
  background: linear-gradient(180deg, #33204d, #24143a) padding-box,
              linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 48%, var(--vv-gold-6)) border-box;
  border-width: 2px;
  /* Le LISERE CLAIR en haut et l'ombre PORTEE en bas : c'est ce couple qui fait qu'une surface se lit
     comme un bouton en relief plutot que comme une etiquette posee a plat. */
  box-shadow: inset 0 1.5px 0 rgba(255, 255, 255, .16), 0 4px 0 rgba(0, 0, 0, .34), 0 6px 14px rgba(0, 0, 0, .4);
  -webkit-tap-highlight-color: transparent; transition: transform .12s ease, box-shadow .12s ease;
}
/* Et il s'ENFONCE : sans ce deplacement, le relief au repos ne prouve rien. */
body.ui-v2 .hq-card:active {
  transform: translateY(3px) scale(.985);
  box-shadow: inset 0 1.5px 0 rgba(255, 255, 255, .12), 0 1px 0 rgba(0, 0, 0, .34), 0 2px 6px rgba(0, 0, 0, .4);
}
body.ui-v2 .hq-ic { width: 28px; height: 28px; object-fit: contain; flex: 0 0 auto; }
body.ui-v2 .hq-lb { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 11px; letter-spacing: .05em; color: #fff; line-height: 1.1; }
body.ui-v2 .hq-stars { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 15px; color: var(--vv-gold-text); line-height: 1; }
body.ui-v2 .hq-badge { top: 5px; right: 7px; }
/* LA CARTE DE TOURNOI PORTE SON HORLOGE ET SA RECOMPENSE (owner 2026-08-30). La carte est une rangee
   centree ; ces deux elements s'y inserent a la suite du compte d'etoiles, l'ecrin en DERNIER - « a
   droite du texte de l'encadre », comme demande. L'horloge est volontairement plus discrete que le
   nombre d'etoiles : elle informe, elle ne doit pas concurrencer le chiffre qui, lui, recompense. */
/* L'HORLOGE EN PASTILLE SUR LE BORD DE LA CARTE (owner 2026-08-30 : « peux-tu plutot le mettre dans
   une pastille sur le bord superieur ou inferieur de la carte ? »). Dans la rangee, elle disputait la
   largeur au nom de division et a l'ecrin sur une carte de 193 px - trois informations en file, aucune
   qui respire. Sortie du flux, elle ne coute plus un pixel a l'interieur.
   BORD INFERIEUR, pas superieur : le coin haut-droite porte deja la pastille de recompenses en attente
   (`.hq-badge`), et deux pastilles sur la meme arete se disputeraient l'attention.
   Meme grammaire que le tag des defis - prune bordee d'or, a cheval sur l'arete - pour que le jeu
   n'invente pas une forme par ecran. */
body.ui-v2 .hq-tour-clock {
  position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 50%);
  padding: 2px 8px 2.5px; border-radius: 99px;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 9px; letter-spacing: .04em;
  line-height: 1; white-space: nowrap; color: var(--vv-gold-text);
  background: linear-gradient(180deg, #2a1740, #1b0f2c);
  border: 1.5px solid var(--vv-gold-4);
  box-shadow: 0 2px 5px rgba(0, 0, 0, .45);
  z-index: 2;
}
body.ui-v2 .hq-tour-rw { display: inline-flex; align-items: center; flex: 0 0 auto; }
body.ui-v2 .hq-tour-rw img { width: 26px; height: 26px; object-fit: contain; display: block; }
/* La carte tient sur une ligne etroite : on autorise le libelle a se comprimer AVANT que l'horloge ou
   l'ecrin ne debordent - c'est le seul element dont la troncature ne coute aucune information. */
body.ui-v2 #hqTour .hq-lb { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* La rangee des cartes rognait la pastille : elle depasse de la carte par le bas. */
body.ui-v2 .home-quick { overflow: visible; }

/* L'ACCUEIL DEFILE, ET JOUER RESTE COLLE EN BAS.
   Mesure, sur un iPhone 14 avec la sauvegarde de demonstration : la vue d'accueil fait 540 px, et son
   contenu en demande 603. Les ecrins y pesent 332 px a eux seuls - une hauteur FIXE et voulue
   (`--slot-h: 160px`, style.css), reglee deux fois par l'owner : une fois pour qu'un ecrin ait la
   meme taille sur telephone et sur iPad, une fois parce que l'illustration et le pied de carte
   debordaient d'une carte plus courte. Ces 160 px ne se rognent donc pas.
   Le debordement EXISTAIT DEJA avant la rangee d'acces - 9 px, assez pour que les bordures en tirets
   sortent de leur case (premier ecrin mesure a -32 px de sa grille) sans que ca saute aux yeux. La
   rangee l'a porte a 63 px, donc rendu evident.
   Faire defiler est la seule reponse qui ne defait aucun reglage : sur un ecran haut rien ne bouge
   (il n'y a rien a faire defiler), sur un ecran court le joueur atteint le bas. Et `sticky` garde
   JOUER a portee de pouce en permanence - un accueil ou le bouton principal passe sous la ligne de
   flottaison serait pire que le debordement qu'on corrige. */
body.ui-v2[data-view="home"] #view-home { overflow-y: auto; overflow-x: hidden; }
/* Et la grille cesse de se laisser COMPRIMER. C'est elle, la cause visible du chevauchement : ses
   rangees valent 160 px chacune (hauteur fixe), donc 332 px au total, mais `flex: 1 1 auto` reduisait
   sa BOITE a 269 - les rangees, elles, gardaient leurs 160. Avec `align-content: center` les 63 px en
   trop debordaient par les deux bouts, et les bordures en tirets passaient sous la rangee d'acces en
   haut et sous JOUER en bas. `flex-shrink: 0` lui rend sa taille reelle ; c'est la vue qui defile. */
body.ui-v2[data-view="home"] .home-cards { flex: 0 0 auto; }
body.ui-v2[data-view="home"] .play-wrap {
  position: sticky; bottom: 20px; z-index: 5;
  margin-bottom: 22px;
  padding: 10px 0 2px;
  /* AUCUN fond, et surtout pas un degrade (owner 2026-08-26). Il y en avait un ici, pose en meme
     temps que le defilement de l'accueil pour adoucir la coupure du contenu qui passe dessous - mais
     sa prune etait ECRITE EN DUR. Des que le joueur equipe un fond d'ecran de sa collection, cette
     bande d'une autre couleur se decoupe en plein milieu de son accueil. Un fond decoratif qui doit
     s'accorder a un fond CHOISI par le joueur n'a pas de valeur juste : la seule bonne valeur est
     aucune. */
  background: none;
}
/* Le rebond d'absorption des etoiles (design tournois SS8). Court et discret : il doit se remarquer
   sans jamais donner l'impression que la carte est un bouton qui vient d'etre presse. */
@keyframes hq-rebond { 0% { transform: scale(1); } 32% { transform: scale(1.09); } 62% { transform: scale(.98); } 100% { transform: scale(1); } }
body.ui-v2 .hq-rebond { animation: hq-rebond .62s cubic-bezier(.34, 1.56, .64, 1); }

/* --- LE BOUTON DORE DU MENU ---------------------------------------------------------------------
   SS8 : « quand un nouveau challenge est disponible, le bouton passe en DORE ». Une pastille aurait
   ete plus simple, et c'est precisement ce que le design refuse : on veut une affordance qui donne
   envie de cliquer, pas une notification a faire disparaitre. */
body.ui-v2 #navBar .nav-item.nav-dore .nav-ic { filter: drop-shadow(0 0 7px rgba(242, 196, 99, .95)) drop-shadow(0 0 14px rgba(242, 196, 99, .5)); }
body.ui-v2 #navBar .nav-item.nav-dore .nav-label { color: var(--vv-gold-text); }

/* --- MONTEE DE NIVEAU D'UNE GEMME : les rayons qui remontent la pierre ---------------------------
   Demande owner : « des rayons de lumiere verticaux qui parcourent le rendu 3D de la gemme de bas en
   haut ». Repris le 2026-08-29 sur ses retours : « pas contenus dans un cercle », « plus fins, comme
   dans un anime », « qu'ils depassent le cadre de la gemme », et un FLASH pour finir, cale sur le son.

   CE QUI LES ENFERMAIT : `overflow: hidden` + `border-radius: 50%` sur le calque. C'etait la un
   reflexe - eviter que les traits debordent - et c'est exactement ce que l'owner ne veut pas : un
   rayon qui s'arrete net au bord d'un disque se lit comme un masque, pas comme une lumiere. Le calque
   deborde donc maintenant de son cadre, dans les deux sens.

   Tout se joue en calque AU-DESSUS du canvas, jamais dedans : toucher au rendu 3D pour une
   celebration voudrait dire reconstruire un materiau a chaque montee de niveau, et le viseur met plus
   de temps a se remonter que l'animation ne dure.
   `pointer-events: none` est vital : le viseur se fait tourner au doigt, et un calque qui intercepte
   le geste rendrait la pierre inerte pendant une seconde apres chaque amelioration. */
body.ui-v2 #ovGemDetail .gd-viewer { position: relative; }
body.ui-v2 .gd-lvlfx {
  /* Le calque deborde SOUS la pierre (les traits en partent) et a peine au-dessus : l'owner veut
     qu'ils aient l'air de la COUVRIR de bas en haut, pas de filer vers le ciel. Asymetrique, donc :
     -14 % en haut, -34 % en bas. */
  position: absolute; inset: -14% -14% -34%;
  z-index: 3; pointer-events: none; opacity: 0;
}
body.ui-v2 .gd-viewer.lvl-up .gd-lvlfx { opacity: 1; }
/* UNE LIGNE SIMPLE, ET SON AURA (owner : « plus fins, plus discrets, plus uniformes - une ligne
   simple avec eventuellement une aura lumineuse »). Tous les traits ont donc la MEME epaisseur et la
   MEME hauteur : c'est leur decalage dans le temps, et lui seul, qui fait la vague. Des epaisseurs
   variees donnaient un rideau bricole ; l'uniformite se lit comme un effet voulu.
   Le trait fait 2 px et ne porte plus de coeur blanc sature : un degrade doux vers la teinte de la
   gemme, plus une ombre portee large et faible qui EST l'aura. */
/* `left`, `bottom` et le retard sont poses EN LIGNE par le JS, tires au sort a chaque lecture : neuf
   colonnes regulieres se lisaient comme un peigne. Ici, seule l'apparence du trait est en CSS. */
body.ui-v2 .gd-lvlfx i {
  position: absolute; width: 2px; height: 44%;
  background: linear-gradient(to top, transparent, var(--gd-fx, #ffffff) 34%, #fff 74%, transparent);
  filter: blur(.6px); opacity: 0; border-radius: 999px;
  box-shadow: 0 0 12px 2px color-mix(in srgb, var(--gd-fx, #ffffff) 55%, transparent);
}
body.ui-v2 .gd-viewer.lvl-up .gd-lvlfx i { animation: gdRay var(--a2-lvl, 800ms) cubic-bezier(.25, .6, .25, 1) both; }
/* La course est calee pour que le HAUT du trait finisse a peu pres au sommet de la pierre, et il est
   deja eteint a ce moment-la. Reglee a -142 % d'abord : les traits filaient jusqu'au titre, ce qui
   donnait « ils montent vers le ciel » au lieu de « ils la parcourent ». */
@keyframes gdRay {
  0%   { transform: translateY(0)     scaleY(.75); opacity: 0; }
  18%  { opacity: .8; }
  55%  { opacity: .72; }
  88%  { opacity: .12; }
  100% { transform: translateY(-96%)  scaleY(1.05); opacity: 0; }
}
/* Le halo sous les traits : sans lui, ils ont l'air de passer DEVANT la pierre au lieu d'en sortir. */
body.ui-v2 .gd-lvlfx::after {
  content: ''; position: absolute; left: 22%; right: 22%; top: 42%; bottom: 42%; border-radius: 50%;
  background: radial-gradient(circle, var(--gd-fx, #ffffff) 0%, transparent 72%);
  opacity: 0; filter: blur(6px);
}
body.ui-v2 .gd-viewer.lvl-up .gd-lvlfx::after { animation: gdHalo var(--a2-lvl, 900ms) ease-out both; }
@keyframes gdHalo {
  0%   { opacity: 0;   transform: scale(.7); }
  35%  { opacity: .22; transform: scale(1.05); }
  100% { opacity: 0;   transform: scale(1.28); }
}
/* LE FLASH DE FIN (owner : « se termine par un flash lumineux derriere la gemme »). Il part a 82 % de
   la duree - donc il suit les rayons au lieu de les concurrencer - et il vit DERRIERE la pierre
   (z-index sous le canvas), sinon il la blanchit au lieu de l'eclairer. */
/* Plus PETIT, plus DIFFUS, moins prononce (owner). Il tenait tout l'ecran et frappait a 95 % : il
   volait la fin de l'animation au lieu de la clore. Il vit maintenant DANS le cadre de la pierre,
   fortement floute, et culmine a 45 %. */
body.ui-v2 .gd-lvlflash {
  position: absolute; inset: 12%; z-index: 1; pointer-events: none; opacity: 0; border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .8) 0%, var(--gd-fx, #ffffff) 34%, transparent 72%);
  filter: blur(14px);
}
body.ui-v2 .gd-viewer.lvl-up .gd-lvlflash {
  animation: gdFlash calc(var(--a2-lvl, 900ms) * .42) ease-out calc(var(--a2-lvl, 900ms) * .82) both;
}
@keyframes gdFlash {
  0%   { opacity: 0;   transform: scale(.62); }
  28%  { opacity: .45; transform: scale(1); }
  100% { opacity: 0;   transform: scale(1.32); }
}
/* Et la pierre respire une fois - c'est ce qui dit que le rayon l'a TRAVERSEE. */
body.ui-v2 .gd-viewer.lvl-up .gd-3d { animation: gdPulse var(--a2-lvl, 900ms) ease-out; }
@keyframes gdPulse {
  0%, 100% { transform: translateY(-11%) scale(1); }
  30%      { transform: translateY(-11%) scale(1.05); }
  86%      { transform: translateY(-11%) scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 .gd-viewer.lvl-up .gd-lvlfx i,
  body.ui-v2 .gd-viewer.lvl-up .gd-lvlfx::after,
  body.ui-v2 .gd-viewer.lvl-up .gd-lvlflash,
  body.ui-v2 .gd-viewer.lvl-up .gd-3d { animation: none; }
}

/* --- CHANGEMENT DE STADE : la ceremonie ----------------------------------------------------------
   Demande owner 2026-08-29, en cinq temps : la pierre seule sur le fond de sa rarete avec des rayons
   comme a l'ouverture d'un ecrin ; elle se fond dans un nuage de lumiere qui efface l'ancienne
   forme ; le nuage s'evanouit sur la SILHOUETTE de la nouvelle ; la nouvelle forme apparait avec un
   effet dynamique et un son ; retour a la fiche.
   Les phases sont portees par des classes (`gs-p1`...`gs-p5`) et non par des animations enchainees :
   une chaine d'`animationend` casse des qu'une seule animation est coupee - onglet en arriere-plan,
   mouvement reduit - et la ceremonie reste alors bloquee a mi-chemin. Des classes posees par une
   horloge se rattrapent toujours, et le bouton de sortie a son propre filet (meme lecon que
   l'echelle de niveaux, qui savait rester sans issue). */
body.ui-v2 .gs-ov {
  display: none; flex-direction: column; align-items: center; justify-content: center; gap: 0;
  padding: 20px 16px calc(20px + var(--safe-bottom, 0px));
  background: var(--gs-bg, #2a1440);
  /* CLIPPE. Le calque de rayons fait 210vmax de cote, bien plus que l'ecran : sans cette ligne, la
     zone devient pannable et un glissement fait GLISSER TOUT L'ECRAN - et le panoramique decouvre
     alors le BORD de ce calque, qu'on lit comme « une coupure dans les rayons, dans le coin
     superieur gauche ». Un seul defaut, deux symptomes (owner 2026-08-29). */
  overflow: hidden;
}
body.ui-v2 .gs-ov.show { display: flex; }
/* Le lavis de rarete par-dessus le fond : c'est lui qui fait que la scene APPARTIENT a la pierre. */
body.ui-v2 .gs-ov::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 68% at 50% 34%, var(--gs-wash, transparent), transparent 72%);
}
/* LES RAYONS. Un eventail de coins effiles qui tourne tres lentement : la meme grammaire que la
   ceremonie d'ecrin, pour que les deux moments se ressemblent sans se confondre. */
/* LA GRAMMAIRE VALIDEE, celle des ecrans de revelation (owner 2026-08-29 : « les rayons qui tournent
   derriere sont une ancienne version, reprends celle avec des rayons plus larges et plus discrets, et
   un centre plus large »). Deux couches : un conique a pointes LARGES (28° de rayon, 12° de vide) et,
   PAR-DESSUS, un disque noir. L'element est en `mix-blend-mode: screen`, ou le noir n'ajoute rien :
   c'est donc ce disque qui EFFACE, et le reculer AGRANDIT le centre. Ses arrets sont pousses plus
   loin qu'ailleurs (.86 a 78 %) - c'est exactement le « centre plus large » demande.
   Centre a 50 % et non 42 % : la pierre doit s'y poser pile, et elle est au centre de l'ecran. */
body.ui-v2 .gs-rays {
  position: absolute; left: 50%; top: 50%; width: 210vmax; height: 210vmax;
  transform: translate(-50%, -50%); pointer-events: none; opacity: 0;
  transition: opacity .5s ease; mix-blend-mode: screen;
  background:
    radial-gradient(circle, rgba(0, 0, 0, .70) 0%, rgba(0, 0, 0, .86) 44%, #000 78%, #000 100%),
    repeating-conic-gradient(from 0deg, var(--gs-ray, rgba(255,255,255,.6)) 0deg 28deg, transparent 28deg 40deg);
  animation: gsSpin 64s linear infinite;
}
body.ui-v2 .gs-ov.gs-p1 .gs-rays, body.ui-v2 .gs-ov.gs-p2 .gs-rays,
body.ui-v2 .gs-ov.gs-p3 .gs-rays, body.ui-v2 .gs-ov.gs-p4 .gs-rays { opacity: .13; }
body.ui-v2 .gs-ov.gs-p5 .gs-rays { opacity: .18; }
@keyframes gsSpin { to { transform: translate(-50%, -50%) rotate(360deg); } }

/* LA SCENE EST CENTREE SUR L'ECRAN, pas sur le flux (owner : « il faudrait qu'elle soit centree du
   debut a la fin au centre des rayons lumineux »). En colonne flex, le titre au-dessus et le nom du
   stade + le bouton en dessous poussaient la pierre vers le haut : elle n'etait jamais au centre, et
   elle bougeait quand le nom du stade apparaissait. Titre et pied passent donc en position absolue -
   ils ne pesent plus sur le centre, qui est celui de l'ecran, donc celui des rayons. */
body.ui-v2 .gs-titre {
  position: absolute; left: 0; right: 0; top: max(9vh, calc(18px + var(--safe-top, 0px)));
  z-index: 2; text-align: center;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 26px; color: #fff;
  text-shadow: 0 3px 10px rgba(0, 0, 0, .55);
  opacity: 0; transform: translateY(-8px); transition: opacity .45s ease, transform .45s ease;
}
body.ui-v2 .gs-ov.gs-p1 .gs-titre { opacity: 1; transform: none; }

body.ui-v2 .gs-scene { position: relative; z-index: 2; width: min(300px, 78vw); aspect-ratio: 1; margin: 0 auto; }
/* LA PIERRE SE FAIT TOURNER AU DOIGT, comme dans la fiche (owner 2026-08-29).
   Elle le pouvait DEJA : gem3d attache ses ecouteurs au CANVAS lui-meme, donc la rotation suit le
   viseur partout ou il est monte. Ce qui manquait, c'est `touch-action: none` - sans lui le
   navigateur reclame le geste pour son propre defilement et le glissement ne parvient jamais au
   canvas. C'est la meme ligne qui manquait, et la meme cause que l'ecran qui bougeait.
   La rotation n'est offerte QU'A LA FIN : pendant la dissolution, le nuage et la silhouette, le
   canvas est floute, transforme ou eteint - un glissement s'y battrait avec les transitions en
   cours. `pointer-events` ne s'ouvre donc qu'a la phase 5, quand la nouvelle forme est posee. */
body.ui-v2 .gs-3d {
  position: absolute; inset: 0; display: grid; place-items: center;
  touch-action: none; pointer-events: none;
}
body.ui-v2 .gs-ov.gs-p5 .gs-3d { pointer-events: auto; }
body.ui-v2 .gs-3d canvas { width: 100% !important; height: 100% !important; display: block; touch-action: none; }

/* PHASE 2 - la pierre se FOND : elle enfle, se floute et s'eteint. Trois proprietes ensemble, parce
   qu'un simple fondu se lit comme « l'image se coupe » et non comme « la matiere se dissout ».

   LES QUATRE ETATS PORTENT LA MEME LISTE DE FILTRES, dans le MEME ORDRE, et c'est ce qui corrige le
   « cadre blanc qui s'affiche une fraction de seconde » signale par l'owner. Deux listes de filtres
   de formes differentes - `blur() brightness()` d'un cote, `brightness() drop-shadow()` de l'autre -
   ne s'interpolent pas : le navigateur BASCULE de l'une a l'autre au milieu de la transition. On
   voyait alors, le temps d'une image, le canvas a `brightness(1.8)` et presque opaque, c'est-a-dire
   un rectangle blanc. Avec la meme liste partout, chaque fonction s'interpole avec sa jumelle et il
   n'y a plus de saut. (Ce n'etait pas le matcap : la meme bascule se produit quelle que soit la
   matiere - elle se voyait moins sur poli -> facette, ou la pierre de depart est deja tres claire.) */
body.ui-v2 .gs-3d { transition: opacity .55s ease, transform .55s ease, filter .55s ease; }
body.ui-v2 .gs-3d { filter: blur(0px) brightness(1) drop-shadow(0 0 0 transparent); }
body.ui-v2 .gs-ov.gs-p2 .gs-3d { opacity: 0; transform: scale(1.18); filter: blur(13px) brightness(1.8) drop-shadow(0 0 0 transparent); }
/* PHASE 3 - le nuage seul. */
body.ui-v2 .gs-ov.gs-p3 .gs-3d { opacity: 0; transform: scale(1.18); filter: blur(13px) brightness(1.8) drop-shadow(0 0 0 transparent); }
/* PHASE 4 - la SILHOUETTE de la nouvelle forme. Le canvas est deja celui de la nouvelle pierre : on
   l'eteint par un filtre plutot que de dessiner une seconde image. Une silhouette obtenue autrement
   ne coinciderait pas au pixel pres avec la forme qui va s'allumer juste apres, et le passage se
   verrait comme un saut. */
body.ui-v2 .gs-ov.gs-p4 .gs-3d {
  opacity: .92; transform: scale(.93);
  filter: blur(0px) brightness(0) drop-shadow(0 0 18px var(--gs-ray, #fff));
}
/* PHASE 5 - elle s'allume, avec un rebond. */
body.ui-v2 .gs-ov.gs-p5 .gs-3d {
  opacity: 1; filter: blur(0px) brightness(1) drop-shadow(0 0 0 transparent);
  animation: gsPop .72s cubic-bezier(.2, 1.5, .35, 1) both;
}
/* Un canvas WebGL frais peut se peindre opaque le temps de sa premiere image : on impose le fond
   transparent, sinon ce blanc-la reapparaitrait au remontage de la nouvelle forme. */
body.ui-v2 .gs-3d canvas { background: transparent; }
@keyframes gsPop {
  0%   { transform: scale(.93); }
  55%  { transform: scale(1.09); }
  100% { transform: scale(1); }
}

/* LE NUAGE DE LUMIERE (repris le 2026-08-29 : « la boule de lumiere est inelegante, une texture plus
   organique, moins geometrique, plus diffuse, moins degrade scolaire »).

   POURQUOI LA PREMIERE VERSION NE POUVAIT PAS ETRE ORGANIQUE. C'etaient deux `radial-gradient(circle)`
   concentriques : un cercle parfait, des anneaux de couleur reguliers, un centre exactement au milieu.
   Aucun reglage de couleur ou d'opacite ne rattrape ca - la GEOMETRIE elle-meme est scolaire, et
   l'oeil lit « un dégradé » avant de lire « de la lumiere ».

   CE QUI LE REND ORGANIQUE ICI, et il faut les quatre ensemble :
     1. SIX masses au lieu d'une, de tailles differentes, DECENTREES - aucune n'est au milieu ;
     2. chacune deforme son propre contour (`border-radius` a huit valeurs qui s'animent) : le bord
        n'est jamais un cercle, et il ne se repete pas ;
     3. chacune DERIVE lentement, sur des periodes premieres entre elles (7,3 s, 9,1 s, 11,7 s...) -
        la figure d'ensemble ne se reforme donc jamais a l'identique ;
     4. le tout est FLOUTE EN BLOC et compose en `screen` : les masses ne se juxtaposent pas, elles
        se fondent, et leurs recouvrements s'eclaircissent comme de la vraie lumiere.
   Le flou est sur le CONTENEUR, pas sur chaque masse : flouter six elements separement les laisse
   distincts, flouter leur somme en fait une seule matiere. */
body.ui-v2 .gs-nuage {
  position: absolute; inset: -18%; pointer-events: none; opacity: 0;
  filter: blur(34px); mix-blend-mode: screen;   /* assez fort pour que les six masses ne se lisent plus une par une */
  transition: opacity .5s ease, transform .5s ease;
  transform: scale(.6);
}
body.ui-v2 .gs-nuage i {
  position: absolute; display: block;
  /* Coeur ETALE et non ponctuel : un centre blanc serre ressort comme une bille meme sous le flou.
     Le blanc s'arrete a 22 %, la teinte tient jusqu'a 58 %, et la chute est longue. */
  background: radial-gradient(circle at 42% 38%,
              rgba(255, 255, 255, .82) 0%, rgba(255, 255, 255, .5) 22%,
              var(--gs-ray, #cfe6ff) 58%, transparent 86%);
  will-change: transform, border-radius;
}
body.ui-v2 .gs-nuage i:nth-child(1) { left: 16%; top: 20%; width: 56%; height: 54%; opacity: .95;
  border-radius: 62% 38% 46% 54% / 58% 44% 56% 42%; animation: gsBlobA 7.3s ease-in-out infinite; }
body.ui-v2 .gs-nuage i:nth-child(2) { left: 34%; top: 12%; width: 44%; height: 60%; opacity: .8;
  border-radius: 40% 60% 66% 34% / 46% 62% 38% 54%; animation: gsBlobB 9.1s ease-in-out infinite; }
body.ui-v2 .gs-nuage i:nth-child(3) { left: 10%; top: 40%; width: 48%; height: 44%; opacity: .72;
  border-radius: 54% 46% 34% 66% / 62% 36% 64% 38%; animation: gsBlobC 11.7s ease-in-out infinite; }
body.ui-v2 .gs-nuage i:nth-child(4) { left: 40%; top: 38%; width: 52%; height: 50%; opacity: .85;
  border-radius: 36% 64% 58% 42% / 40% 56% 44% 60%; animation: gsBlobB 8.3s ease-in-out infinite reverse; }
body.ui-v2 .gs-nuage i:nth-child(5) { left: 26%; top: 30%; width: 34%; height: 34%; opacity: 1;
  border-radius: 58% 42% 62% 38% / 52% 60% 40% 48%; animation: gsBlobA 6.1s ease-in-out infinite reverse; }
body.ui-v2 .gs-nuage i:nth-child(6) { left: 46%; top: 22%; width: 30%; height: 38%; opacity: .68;
  border-radius: 44% 56% 40% 60% / 66% 34% 62% 38%; animation: gsBlobC 13.3s ease-in-out infinite; }
/* Les contours se deforment ET les masses derivent. Sans la derive, six bords qui ondulent sur place
   se lisent comme six lampes ; avec elle, c'est une matiere qui bouge. */
@keyframes gsBlobA {
  0%, 100% { border-radius: 62% 38% 46% 54% / 58% 44% 56% 42%; transform: translate(0, 0) scale(1); }
  33%      { border-radius: 42% 58% 62% 38% / 44% 62% 38% 56%; transform: translate(6%, -5%) scale(1.1); }
  66%      { border-radius: 56% 44% 36% 64% / 64% 38% 62% 36%; transform: translate(-5%, 4%) scale(.93); }
}
@keyframes gsBlobB {
  0%, 100% { border-radius: 40% 60% 66% 34% / 46% 62% 38% 54%; transform: translate(0, 0) scale(1); }
  40%      { border-radius: 64% 36% 40% 60% / 62% 40% 60% 38%; transform: translate(-7%, -4%) scale(1.13); }
  72%      { border-radius: 48% 52% 58% 42% / 38% 58% 42% 62%; transform: translate(4%, 6%) scale(.9); }
}
@keyframes gsBlobC {
  0%, 100% { border-radius: 54% 46% 34% 66% / 62% 36% 64% 38%; transform: translate(0, 0) scale(1); }
  45%      { border-radius: 36% 64% 60% 40% / 42% 64% 36% 58%; transform: translate(5%, 5%) scale(1.08); }
  80%      { border-radius: 60% 40% 44% 56% / 56% 42% 58% 44%; transform: translate(-4%, -6%) scale(.95); }
}
/* LA RESPIRATION D'ENSEMBLE (owner : « qu'elle grossisse et rapetisse plusieurs fois lentement »).
   Elle porte sur le CONTENEUR : les masses gardent leur vie propre par-dessus, et les deux echelles
   de mouvement ne se synchronisent jamais. */
body.ui-v2 .gs-ov.gs-p2 .gs-nuage { opacity: .9; animation: gsRespire 2.6s ease-in-out infinite; }
body.ui-v2 .gs-ov.gs-p3 .gs-nuage { opacity: 1;  animation: gsRespire 2.6s ease-in-out infinite; }
@keyframes gsRespire { 0%, 100% { transform: scale(.9); } 50% { transform: scale(1.2); } }
body.ui-v2 .gs-ov.gs-p4 .gs-nuage { opacity: .2; transform: scale(.7); animation: none; }
body.ui-v2 .gs-ov.gs-p5 .gs-nuage { opacity: 0;  transform: scale(1.5); animation: none; }
/* L'eclair de l'allumage : un disque blanc qui explose une fois, a l'instant precis ou la forme
   apparait. C'est lui qui donne le « choc » demande, pas le rebond seul. */
body.ui-v2 .gs-flash {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none; opacity: 0;
  background: radial-gradient(circle, #fff 0%, transparent 62%);
}
body.ui-v2 .gs-ov.gs-p5 .gs-flash { animation: gsFlash .5s ease-out both; }
@keyframes gsFlash {
  0%   { opacity: .95; transform: scale(.5); }
  100% { opacity: 0;   transform: scale(1.7); }
}

body.ui-v2 .gs-stade {
  position: absolute; left: 0; right: 0; bottom: calc(96px + var(--safe-bottom, 0px));
  z-index: 2; text-align: center;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 21px; letter-spacing: .04em;
  color: var(--vv-gold-text); text-shadow: 0 2px 8px rgba(0, 0, 0, .5);
  opacity: 0; transform: translateY(10px); transition: opacity .45s ease, transform .45s ease;
}
body.ui-v2 .gs-ov.gs-p5 .gs-stade { opacity: 1; transform: none; }
/* Le bouton de sortie apparait avec la nouvelle forme, jamais avant : le taper plus tot sauterait la
   revelation qu'on vient de mettre en scene. Mais il a un FILET cote JS - une ceremonie sans sortie
   est le pire ecran possible, et celle-ci arrive juste apres une depense. */
body.ui-v2 .gs-btn {
  position: absolute; left: 0; right: 0; margin-inline: auto;
  bottom: calc(24px + var(--safe-bottom, 0px));
  z-index: 3; width: min(84%, 320px);
  opacity: 0; pointer-events: none; transform: translateY(16px);
  transition: opacity .4s ease, transform .4s ease;
}
body.ui-v2 .gs-ov.gs-sortie .gs-btn { opacity: 1; pointer-events: auto; transform: none; }
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 .gs-rays { animation: none; }
  body.ui-v2 .gs-ov.gs-p5 .gs-3d, body.ui-v2 .gs-ov.gs-p5 .gs-flash { animation: none; }
}

/* --- PLAYGROUND DES ANIMATIONS V2 (outil de reglage, jamais vu par un joueur) ---------------------
   Un panneau qui DEFILE et qui ne mange pas l'ecran : les animations qu'il declenche s'affichent
   derriere lui, et un panneau plein ecran les cacherait. D'ou la demi-hauteur et le fond opaque -
   sur un fond translucide, les rayons et les nuages qu'on regle passent au travers et faussent le
   jugement. */
#anim2Lab {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 100000;
  max-height: 56vh; overflow: auto; -webkit-overflow-scrolling: touch;
  font-family: 'Nunito', system-ui, sans-serif;
}
#anim2Lab .a2-card {
  background: #140c22; border-top: 2px solid var(--vv-gold-4, #e0b45a);
  padding: 10px 12px calc(14px + var(--safe-bottom, 0px));
  display: flex; flex-direction: column; gap: 7px;
}
#anim2Lab { transition: transform .3s ease; }
/* Replie : seule la barre de titre depasse, et elle sert de poignee. */
#anim2Lab.a2-min { transform: translateY(calc(100% - 46px)); }
#anim2Lab .a2-top { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
#anim2Lab.a2-min .a2-top b::after { content: ' ▲'; opacity: .6; }
#anim2Lab .a2-top b { color: #fff; font-size: 14px; letter-spacing: .04em; }
#anim2Lab .a2-x {
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid rgba(255,255,255,.25);
  background: rgba(255,255,255,.08); color: #fff; font-size: 15px; line-height: 1; cursor: pointer;
}
#anim2Lab .a2-sec {
  margin-top: 6px; color: var(--vv-gold-text, #f2c463); font-size: 11.5px; font-weight: 900;
  letter-spacing: .08em; border-top: 1px solid rgba(255,255,255,.10); padding-top: 7px;
}
#anim2Lab .a2-l { display: grid; grid-template-columns: 108px 1fr 46px; align-items: center; gap: 8px; }
#anim2Lab .a2-l > span { color: rgba(255,255,255,.72); font-size: 11.5px; }
#anim2Lab .a2-l b { color: #fff; font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; }
#anim2Lab select {
  grid-column: 2 / -1; width: 100%; padding: 5px 7px; border-radius: 8px;
  background: rgba(255,255,255,.10); color: #fff; border: 1px solid rgba(255,255,255,.18); font-size: 12px;
}
#anim2Lab input[type=range] { width: 100%; }
#anim2Lab .a2-go {
  margin-top: 3px; padding: 9px; border-radius: 10px; border: 0; cursor: pointer;
  background: linear-gradient(var(--vv-gold-2, #ffd98a), var(--vv-gold-6, #a9761f));
  color: #3a2504; font-weight: 900; font-size: 13px; letter-spacing: .04em;
}
#anim2Lab .a2-foot { display: flex; gap: 7px; margin-top: 8px; }
#anim2Lab .a2-copy {
  flex: 1; padding: 8px; border-radius: 9px; cursor: pointer; font-size: 11.5px; font-weight: 800;
  background: rgba(255,255,255,.10); color: #fff; border: 1px solid rgba(255,255,255,.20);
}
/* Le resultat du « copier » s'affiche AUSSI en clair : dans une WebView, l'acces au presse-papier
   echoue souvent en silence, et un bouton qui ne dit rien passe pour un bouton casse. */
#anim2Lab .a2-out {
  color: rgba(255,255,255,.62); font-size: 10.5px; word-break: break-all; line-height: 1.35;
  font-family: ui-monospace, monospace;
}

/* --- EMPLACEMENTS PLEINS : le bouton de deblocage tient dans sa case ------------------------------
   Signale par l'owner le 2026-08-29 : le bouton « Debloquer un emplacement · 20 » DEBORDAIT du cadre,
   des deux cotes, jusqu'a sortir de l'ecran.
   La cause : `.btn-mini` se dimensionne sur son texte (padding 18px, corps 15px) et RIEN ne le
   contraignait a sa case - la grille pose bien `min-width: 0` sur `.cf-slot`, mais un enfant en
   largeur automatique passe outre. Le libelle francais fait a lui seul plus du double de la case.
   On ecrit donc la contrainte : largeur de la case, texte qui va a la ligne, et le PRIX sur sa propre
   ligne (il ne doit jamais se couper entre le chiffre et le joyau).
   La regle generique sur `.cf-rooms .btn` ferme le cas pour de bon : tout bouton futur pose dans une
   case est borne par elle, quel que soit son libelle ou sa langue. */
body.ui-v2 .cf-rooms .btn { max-width: 100%; min-width: 0; }
body.ui-v2 .cf-unlock-slot {
  width: 100%; max-width: 100%; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  padding: 8px 6px; font-size: 12px; line-height: 1.15; text-align: center;
  white-space: normal; overflow-wrap: anywhere; hyphens: auto;
}
body.ui-v2 .cf-unlock-lbl { display: block; }
body.ui-v2 .cf-unlock-cost {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 13.5px; white-space: nowrap;   /* le prix ne se coupe jamais entre le chiffre et le joyau */
}
/* La case du prochain emplacement porte un cadenas AU-DESSUS du bouton : sans cette reserve, les deux
   se touchent des que le libelle passe sur deux lignes. */
body.ui-v2 .cf-slot-next { gap: 8px; padding: 10px 8px; }

/* --- LE TOURNOI --------------------------------------------------------------------------------
   Refonte owner 2026-08-29, sur ses deux captures de reference. De haut en bas : titre CENTRE, ou
   l'on en est dans la SEMAINE, la DIVISION dans un cadre cliquable, puis le classement a cinq
   colonnes coupe par la zone de promotion. */

/* Le titre est centre pour de bon : le bouton de regles est SORTI du flux (une pastille posee a
   droite) au lieu d'etre un second element de flex. Tant qu'il partageait la ligne, le titre etait
   centre sur l'espace RESTANT - donc decale vers la gauche de la moitie du bouton. */
body.ui-v2 .tour-head { position: relative; display: block; text-align: center; }
body.ui-v2 .tour-head .view-title { margin: 0; }
body.ui-v2 .tour-i {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 17px; line-height: 1;
  color: #fff; background: rgba(255, 255, 255, .12); border: 1.5px solid rgba(255, 255, 255, .22);
  cursor: pointer;
}
body.ui-v2 .tour-weeklbl {
  margin: 8px 0 5px; text-align: center; font-family: 'Nunito', sans-serif; font-weight: 900;
  font-size: 10.5px; letter-spacing: .12em; color: rgba(255, 255, 255, .55);
}
/* Le rail des 7 jours. Les pastilles sont reliees par un trait continu : c'est ce trait qui fait
   lire une DUREE plutot que sept marqueurs independants. */
body.ui-v2 .tour-week {
  position: relative; display: flex; align-items: center; justify-content: space-between;
  gap: 0; margin: 0 auto 12px; width: min(300px, 82%);
}
body.ui-v2 .tour-week::before {
  content: ''; position: absolute; left: 7px; right: 7px; top: 50%; height: 3px; margin-top: -1.5px;
  border-radius: 2px; background: rgba(255, 255, 255, .13);
}
body.ui-v2 .tour-jour {
  position: relative; z-index: 1; width: 15px; height: 15px; border-radius: 50%;
  background: rgba(255, 255, 255, .13); display: flex; align-items: center; justify-content: center;
}
body.ui-v2 .tour-jour.passe { background: linear-gradient(var(--vv-gold-2), var(--vv-gold-6)); }
body.ui-v2 .tour-jour.courant {
  width: 27px; height: 27px; background: linear-gradient(var(--vv-gold-2), var(--vv-gold-6));
  box-shadow: 0 0 0 3px rgba(242, 196, 99, .22);
}
body.ui-v2 .tour-jour.courant b {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 14px; color: #3a2504; line-height: 1;
}

/* LE CADRE DE DIVISION. C'est un BOUTON, et il doit se lire comme tel : bordure doree, chevron a
   droite. Il mene a l'ecran des divisions - sans cette affordance, l'ecran serait introuvable. */
body.ui-v2 .tour-divbtn {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
  width: 100%; padding: 11px 14px; margin: 0 0 12px; border-radius: 18px; cursor: pointer;
  border: 1.5px solid transparent; text-align: left;
  background: var(--vv-panel-flat) padding-box,
              linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box;
}
body.ui-v2 .tour-divic { display: flex; gap: 2px; align-items: center; }
body.ui-v2 .tour-divic .tour-star { width: 16px; height: 16px; }
/* Les insignes de division (images owner). Taille FIXE et `object-fit: contain` : les cinq fichiers
   sont carres aujourd'hui, mais un insigne redessine plus large ne doit pas pousser le nom de la
   division hors de son cadre. */
/* Taille remontee a la demande de l'owner (2026-08-29) : les insignes sont le seul repere visuel
   d'une division, ils doivent porter la ligne. `object-fit: contain` reste indispensable - les cinq
   fichiers sont carres aujourd'hui, un insigne redessine plus large ne doit pas pousser le nom de la
   division hors de son cadre. */
body.ui-v2 .tour-divimg { width: 54px; height: 54px; object-fit: contain; display: block; }
body.ui-v2 .tdv-ic .tour-divimg { width: 50px; height: 50px; }
/* Les etoiles restent dans le DOM mais MASQUEES : elles ne reparaissent que si l'image manque
   (`onerror` pose `sans-art`). Les garder en reserve plutot que de les injecter au moment de l'echec
   evite tout balisage construit dans un attribut - ce qui avait fait fuir du code en clair. */
body.ui-v2 .tour-divwrap { display: inline-flex; align-items: center; }
body.ui-v2 .tour-divstars { display: none; gap: 2px; align-items: center; }
body.ui-v2 .tour-divwrap.sans-art .tour-divimg { display: none; }
body.ui-v2 .tour-divwrap.sans-art .tour-divstars { display: inline-flex; }
body.ui-v2 .tour-divtxt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
body.ui-v2 .tour-divtxt b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 18px; color: #fff; }
body.ui-v2 .tour-divtime { font-size: 11.5px; font-weight: 800; color: rgba(255, 255, 255, .6); }
body.ui-v2 .tour-divchev { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 24px; color: var(--vv-gold-text); line-height: 1; }

/* L'etoile est dessinee en CSS et non chargee : c'est un glyphe de 12 px repete sur chaque ligne du
   classement, et une image de plus au chargement pour cela ne se justifie pas. */
body.ui-v2 .tour-star {
  width: 12px; height: 12px; display: inline-block; background: linear-gradient(var(--vv-gold-2), var(--vv-gold-6));
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

body.ui-v2 .tour-pool { list-style: none; margin: 0; padding: 0 0 14px; display: flex; flex-direction: column; gap: 5px; }
/* CINQ COLONNES, dans l'ordre demande : rang, avatar, nom, etoiles, recompense. Le nom est la seule
   colonne elastique - c'est lui qui doit ceder, jamais un chiffre ni une image. */
body.ui-v2 .tour-row {
  display: grid; grid-template-columns: 26px 34px 1fr auto auto; align-items: center; gap: 8px;
  padding: 7px 11px; border-radius: 13px; background: rgba(0, 0, 0, .24);
  border: 1.5px solid var(--vv-line);
}
body.ui-v2 .tour-row.qual { border-color: rgba(111, 224, 160, .38); }
body.ui-v2 .tour-row.me { border-color: var(--vv-gold-4); background: rgba(242, 196, 99, .12); }
body.ui-v2 .tour-pos { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; color: var(--vv-gold-text); text-align: center; font-size: 14px; }
body.ui-v2 .tour-av { display: flex; align-items: center; justify-content: center; }
body.ui-v2 .tour-av .avatar { width: 34px; height: 34px; }
body.ui-v2 .tour-av-vide { display: block; border-radius: 50%; background: rgba(255, 255, 255, .10); }
body.ui-v2 .tour-nom { color: #fff; font-weight: 800; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body.ui-v2 .tour-sc { display: flex; align-items: center; gap: 4px; color: #fff; font-weight: 900; font-size: 13px; }
body.ui-v2 .tour-rw-cell { display: flex; align-items: center; justify-content: flex-end; min-width: 42px; }
body.ui-v2 .tour-rw-box { display: flex; align-items: center; gap: 2px; }
body.ui-v2 .tour-rw-box b, body.ui-v2 .tour-rw-coins b {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 12px; color: var(--vv-gold-text);
}
body.ui-v2 .tour-rw-coins { display: flex; align-items: center; gap: 3px; }
body.ui-v2 .tour-rw-rien { display: block; width: 22px; height: 2px; border-radius: 2px; background: rgba(255, 255, 255, .12); }

/* LA ZONE DE PROMOTION, en toutes lettres. Elle remplace le liseré vert qui disait « ici » sans dire
   quoi : une barre nommee repond a la question que le joueur se pose en lisant un classement. */
body.ui-v2 .tour-promo {
  display: flex; align-items: center; justify-content: center; margin: 3px 0;
  padding: 5px 10px; border-radius: 10px;
  background: linear-gradient(90deg, rgba(111, 224, 160, .10), rgba(111, 224, 160, .30), rgba(111, 224, 160, .10));
  border: 1px solid rgba(111, 224, 160, .35);
}
body.ui-v2 .tour-promo span {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 11.5px; letter-spacing: .1em;
  color: #b6f2d1;
}

/* L'ECRAN DES DIVISIONS. Ordre INVERSE - la plus haute en haut : une echelle se lit en montant, et
   la division du joueur doit se voir comme une marche. */
body.ui-v2 .tdv-head {
  display: flex; justify-content: space-between; margin: 10px 2px 6px;
  font-family: 'Nunito', sans-serif; font-weight: 900; font-size: 10px; letter-spacing: .1em;
  color: rgba(255, 255, 255, .5);
}
body.ui-v2 .tdv-liste { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
body.ui-v2 .tdv {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 11px;
  padding: 10px 12px; border-radius: 15px; background: rgba(0, 0, 0, .26);
  border: 1.5px solid var(--vv-line);
}
body.ui-v2 .tdv.courante {
  border-color: var(--vv-gold-4); background: rgba(242, 196, 99, .13);
  box-shadow: 0 0 0 2px rgba(242, 196, 99, .18);
}
/* Une division PASSEE reste lisible mais recule : elle n'est plus un objectif. */
body.ui-v2 .tdv.passee { opacity: .62; }
body.ui-v2 .tdv-ic { display: flex; gap: 2px; align-items: center; }
body.ui-v2 .tdv-ic .tour-star { width: 15px; height: 15px; }
body.ui-v2 .tdv-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
body.ui-v2 .tdv-txt b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 15px; color: #fff; }
body.ui-v2 .tdv-pool { font-size: 11.5px; font-weight: 800; color: rgba(255, 255, 255, .55); }
body.ui-v2 .tdv-etat { font-family: 'Nunito', sans-serif; font-weight: 900; font-size: 9.5px; letter-spacing: .08em; }
body.ui-v2 .tdv-etat.passee { color: var(--vv-gain, #6fe0a0); }
body.ui-v2 .tdv-etat.courante { color: var(--vv-gold-text); }
body.ui-v2 .tdv-rw { display: flex; align-items: center; gap: 3px; }
body.ui-v2 .tdv-rw b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 13px; color: var(--vv-gold-text); }

/* --- MONTEE DE DIVISION : le faisceau qui remonte l'echelle ---------------------------------------
   Demande owner 2026-08-29 : « une animation montre la montee de division en reprenant l'ecran qui
   montre les 5 divisions, et en pointant avec un effet de lumiere la montee de la division
   precedente a la suivante, assombrissant la division passee, avant de laisser le joueur cliquer
   pour continuer ».
   L'ecran est le MEME que celui des divisions - c'est tout l'interet : le joueur reconnait l'echelle
   qu'il consulte, et il y voit sa propre marche. Un ecran de felicitations inedit ne lui apprendrait
   rien sur ou il en est. */
body.ui-v2 .tdv-liste { position: relative; }
/* Le faisceau est place et dimensionne en JS (il relie DEUX lignes precises) ; le CSS ne porte que
   son apparence et sa course. */
body.ui-v2 .tdv-faisceau {
  position: absolute; left: 50%; transform: translateX(-50%); width: 34%;
  border-radius: 999px; pointer-events: none; opacity: 0; z-index: 0;
  background: linear-gradient(to top, transparent, var(--vv-gold-4) 34%, #fff 74%, transparent);
  filter: blur(9px); mix-blend-mode: screen;
}
body.ui-v2 .tdv-liste.promo-go .tdv-faisceau { animation: tdvBeam var(--a2-beam, 1.25s) cubic-bezier(.35, .75, .3, 1) both; }
@keyframes tdvBeam {
  0%   { opacity: 0;   transform: translateX(-50%) scaleY(.06); }
  22%  { opacity: .72; }
  70%  { opacity: .6;  transform: translateX(-50%) scaleY(1); }
  100% { opacity: 0;   transform: translateX(-50%) scaleY(1); }
}
/* La division QUITTEE s'assombrit - c'est ce qui fait lire une marche franchie plutot qu'une simple
   mise en avant de la nouvelle. */
body.ui-v2 .tdv { transition: opacity .6s ease, border-color .6s ease, background-color .6s ease, box-shadow .6s ease, transform .6s ease; }
body.ui-v2 .tdv-liste.promo-go .tdv.promo-de {
  opacity: .34; border-color: var(--vv-line); background: rgba(0, 0, 0, .3); box-shadow: none; transform: scale(.97);
}
body.ui-v2 .tdv-liste.promo-go .tdv.promo-vers {
  opacity: 1; border-color: var(--vv-gold-4); background: rgba(242, 196, 99, .16);
  box-shadow: 0 0 0 2px rgba(242, 196, 99, .28), 0 0 22px 4px rgba(242, 196, 99, .22);
  animation: tdvArrive .7s cubic-bezier(.2, 1.5, .35, 1) calc(var(--a2-beam, 1.25s) * .76) both;
}
@keyframes tdvArrive {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.055); }
  100% { transform: scale(1); }
}
/* Avant que le faisceau parte, la ligne d'arrivee ne doit RIEN annoncer : sinon la montee est jouee
   d'avance et l'animation ne raconte plus rien. */
body.ui-v2 .tdv-liste .tdv.promo-vers .tdv-etat { opacity: 0; transition: opacity .4s ease .95s; }
body.ui-v2 .tdv-liste.promo-go .tdv.promo-vers .tdv-etat { opacity: 1; }
/* La ligne QUITTEE porte ses deux etats superposes ; on n'en montre qu'un a la fois, sans jamais
   changer la hauteur de la ligne pendant que le faisceau la traverse. */
body.ui-v2 .tdv.promo-de .tdv-ap { display: none; }
body.ui-v2 .tdv-liste.promo-go .tdv.promo-de .tdv-av { display: none; }
body.ui-v2 .tdv-liste.promo-go .tdv.promo-de .tdv-ap { display: inline; }
body.ui-v2 .tdv-promo-sub { text-align: center; font-size: 13px; color: rgba(255,255,255,.75); margin: 0 0 8px; }
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 .tdv-liste.promo-go .tdv-faisceau,
  body.ui-v2 .tdv-liste.promo-go .tdv.promo-vers { animation: none; }
}

/* LES REGLES : la competition d'abord, le bareme ensuite. */
body.ui-v2 .tour-how { margin: 6px 0 0; padding: 0 0 0 18px; display: flex; flex-direction: column; gap: 7px; }
body.ui-v2 .tour-how li { font-size: 12.5px; line-height: 1.4; color: rgba(255, 255, 255, .8); }
body.ui-v2 .tour-how li::marker { color: var(--vv-gold-text); font-weight: 900; }
body.ui-v2 .tour-rules { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
body.ui-v2 .tour-rules li { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; color: rgba(255, 255, 255, .8); }
body.ui-v2 .tour-rules li b { color: var(--vv-gold-text); white-space: nowrap; }
body.ui-v2 .tour-rules-h { margin: 15px 0 0; font-family: 'Lilita One', 'Trebuchet MS', sans-serif; color: #fff; font-size: 14px; letter-spacing: .08em; }
body.ui-v2 .tour-day-title { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 20px; color: #fff; margin: 0 0 4px; text-align: center; }
body.ui-v2 .tour-day-sub { font-size: 12.5px; color: rgba(255, 255, 255, .7); margin: 0 0 10px; text-align: center; }
body.ui-v2 .tour-gains { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; margin: 6px 0 12px; }
body.ui-v2 .tour-gain { display: flex; flex-direction: column; align-items: center; gap: 4px; color: var(--vv-gold-text); font-family: 'Lilita One', 'Trebuchet MS', sans-serif; }

/* --- LES DEFIS ---------------------------------------------------------------------------------- */
body.ui-v2 .chal-tickets {
  padding: 11px 14px; margin: 4px 0 12px; border-radius: 16px;
  border: 1.5px solid transparent;
  background: var(--vv-panel-flat) padding-box,
              linear-gradient(rgba(242, 196, 99, .45), rgba(176, 126, 35, .35)) border-box;
}
body.ui-v2 .ct-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* LE COMPTEUR DE TICKETS (owner 2026-08-26). Cinq pastilles disaient « il t'en reste deux » ; elles
   ne disaient pas ce qu'est un ticket, et elles auraient cesse de tenir des qu'un pack en donne
   davantage. Un jeton et un nombre, exactement comme les pieces et les joyaux de la barre du haut :
   le joueur a deja appris a lire cette forme-la. */
body.ui-v2 .ct-jeton { display: inline-flex; align-items: baseline; gap: 7px; }
body.ui-v2 .ct-jeton img { width: 30px; height: 30px; object-fit: contain; align-self: center; }
body.ui-v2 .ct-jeton b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 22px; color: #fff; line-height: 1; }
body.ui-v2 .ct-max { font-size: 12px; color: rgba(255, 255, 255, .5); }
body.ui-v2 .ct-foot { font-size: 11.5px; color: rgba(255, 255, 255, .55); margin-top: 7px; }
/* --- LES CARTES D'EVENEMENT : PLEINE LARGEUR (owner 2026-08-26) ---------------------------------
   Une carte par ligne. La grille a deux colonnes coupait les noms en deux et n'avait plus de place
   pour le rail de recompenses, qui est pourtant ce qui donne envie d'ouvrir un defi. En pleine
   largeur, chaque carte tient : illustration, nom, avancement, jauge, et le rail entier. */
body.ui-v2 .chal-sec-head {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 13px; letter-spacing: .12em;
  color: rgba(255, 255, 255, .6);
  /* De l'air AVANT le titre de section, et un peu apres (owner) : sans marge, la premiere carte
     collait au titre et les sections se lisaient comme une seule liste. */
  margin: 22px 0 10px;
}
body.ui-v2 .chal-sec-head:first-child { margin-top: 4px; }
body.ui-v2 .chal-sections { padding: 0 14px; }
body.ui-v2 .chal-liste { display: flex; flex-direction: column; gap: 18px; }
body.ui-v2 .chal-card {
  position: relative; display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  width: 100%; padding: 13px 12px 11px; text-align: left;
  border: 1.5px solid transparent; border-radius: 18px; cursor: pointer;
  background: var(--vv-panel-flat) padding-box,
              linear-gradient(rgba(242, 196, 99, .45), rgba(176, 126, 35, .35)) border-box;
  box-shadow: 0 5px 14px rgba(0, 0, 0, .38); -webkit-tap-highlight-color: transparent;
  transition: transform .12s ease;
}
body.ui-v2 .chal-card:active { transform: scale(.985); }
body.ui-v2 .chal-card.bientot { opacity: .72; cursor: default; }
/* La carte NEUVE : monture d'or pleine et halo. C'est le « gros bouton dore » du design, a l'echelle
   d'une carte - il se distingue de ses voisines sans changer de forme, donc sans casser la liste. */
body.ui-v2 .chal-card.neuf {
  background: radial-gradient(120% 90% at 50% 8%, rgba(242, 196, 99, .20), rgba(30, 16, 49, 0) 72%) padding-box,
              var(--vv-panel-flat) padding-box,
              linear-gradient(var(--vv-gold-2), var(--vv-gold-4) 45%, var(--vv-gold-6)) border-box;
  box-shadow: 0 0 14px rgba(242, 196, 99, .34), 0 5px 14px rgba(0, 0, 0, .38);
}
/* LE TAG « NEW » EST ROUGE, comme une notification (owner 2026-08-30 : « pourrait etre en rouge, de la
   meme couleur que les notifications »). Il etait prune borde d'or, donc de la meme famille que tout
   le reste du panneau : il disait « decoration », pas « regarde ici ». Le rouge est le seul accent du
   skin qui signifie « une chose t'attend » - c'est celui des pastilles de la barre de navigation
   (`.nav-badge`, meme jeton `--coral`), et c'est exactement ce que ce tag veut dire.
   Il s'eteint deja au premier clic sur la carte (`dore: !ouverts.includes(id)`, domain/challenge-rules),
   et ce meme clic FIGE la puissance de reference du defi (openDefi -> chalCible -> referenceFigee) :
   les deux comportements demandes etaient donc deja en place, seule la couleur manquait. */
body.ui-v2 .chal-neuf {
  position: absolute; top: 0; left: 18px; transform: translateY(-52%);
  padding: 2.5px 9px 3px; border-radius: 99px; font-size: 8.5px; letter-spacing: .09em; line-height: 1;
  background: linear-gradient(180deg, var(--coral, #C4283F), var(--coral-deep, #8E1B2C));
  border: 1.5px solid rgba(255, 255, 255, .82);
  color: #fff; font-family: 'Lilita One', 'Trebuchet MS', sans-serif;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .45), 0 0 10px rgba(196, 40, 63, .5);
}
body.ui-v2 .chal-tete { display: flex; align-items: center; gap: 11px; }
body.ui-v2 .chal-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
body.ui-v2 .chal-ic { width: 42px; height: 42px; object-fit: contain; flex: 0 0 auto; }
body.ui-v2 .chal-nom { font-weight: 900; font-size: 14px; color: #fff; line-height: 1.15; }
body.ui-v2 .chal-type { font-size: 11px; color: rgba(255, 255, 255, .68); }
body.ui-v2 .chal-prog { flex: 0 0 auto; font-size: 11px; font-weight: 900; color: var(--vv-gold-text); white-space: nowrap; }
body.ui-v2 .chal-bar { width: 100%; height: 7px; border-radius: 999px; background: var(--vv-well); border: 1.5px solid var(--vv-line); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7); overflow: hidden; }
body.ui-v2 .chal-bar > i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--vv-gold-6), var(--vv-gold-2)); }
body.ui-v2 .chal-rw-ic { width: 15px; height: 15px; object-fit: contain; }
body.ui-v2 .chal-vide { text-align: center; color: rgba(255, 255, 255, .5); font-size: 13px; padding: 24px 12px; }

/* --- LA CARTE DE COMPTE A REBOURS --------------------------------------------------------------
   « Le Hub n'est jamais vide : il est soit en evenement, soit en attente VISIBLE d'un evenement »
   (calendrier live-ops SS3). Elle a la forme d'une carte mais pas sa monture d'or : elle n'est pas
   jouable, et rien ne doit inviter a la taper. */
body.ui-v2 .chal-teaser {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 13px 12px; border-radius: 18px;
  background: rgba(0, 0, 0, .26); border: 1.5px dashed var(--vv-line);
}
body.ui-v2 .chal-teaser .ct-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
body.ui-v2 .ct-horloge { flex: 0 0 auto; text-align: right; }
body.ui-v2 .ct-horloge b { display: block; font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 17px; color: var(--vv-gold-text); line-height: 1.1; }
body.ui-v2 .ct-horloge span { font-size: 10px; color: rgba(255, 255, 255, .5); }
body.ui-v2 .defi-obj { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: 13px; background: rgba(0, 0, 0, .26); border: 1.5px solid var(--vv-line); margin-bottom: 8px; }
body.ui-v2 .defi-obj span { font-size: 12px; color: rgba(255, 255, 255, .65); }
body.ui-v2 .defi-obj b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 19px; color: var(--vv-gold-text); }
body.ui-v2 .defi-mod { font-size: 12px; color: rgba(255, 255, 255, .6); margin: 4px 0; text-align: center; }
body.ui-v2 .defi-mod.defi-ko { color: #ff9a9a; }
body.ui-v2 .defi-rw { display: flex; align-items: center; justify-content: center; gap: 7px; margin: 10px 0; font-size: 12.5px; color: rgba(255, 255, 255, .7); }
body.ui-v2 .defi-go {
  width: 100%; margin-top: 20px;
  display: flex; align-items: center; justify-content: center; gap: 10px;
}
/* LE COUT, A GAUCHE DU VERBE. Sa pastille sombre le detache du bouton d'or : sans elle, un ticket
   dore sur un bouton dore disparaitrait. */
body.ui-v2 .defi-go .defi-go-cout {
  display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto;
  padding: 3px 9px 3px 7px; border-radius: 999px;
  background: rgba(20, 10, 34, .30); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .35);
}
body.ui-v2 .defi-go .defi-go-cout img { width: 20px; height: 20px; object-fit: contain; display: block; }
body.ui-v2 .defi-go .defi-go-cout b { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 14px; line-height: 1; }
body.ui-v2 .defi-go .defi-go-lbl { flex: 0 1 auto; }

/* --- LE CALQUE GENERIQUE ------------------------------------------------------------------------ */
body.ui-v2 .ov-simple { max-width: 380px; width: min(380px, calc(100vw - 32px)); max-height: 84vh; overflow-y: auto; }
body.ui-v2 .ov-simple .ovs-close { width: 100%; margin-top: 10px; }
/* LE CONTENU RESTE DANS LE CADRE (owner 2026-08-26 : « le nombre de recompenses fait s'agrandir le
   texte largement au-dela de l'encadre »).
   MESURE : le panneau fait 340 px de large, et `.ovs-body` en faisait 485 - il debordait des DEUX
   cotes, bouton PLAY compris. La cause n'est pas le rail : c'est que `.ovs-body` n'avait ni largeur
   ni `min-width: 0`. Un enfant de flex prend par defaut la largeur INTRINSEQUE de son contenu, et le
   rail de huit recompenses fait 489 px - le panneau ne le contraignait donc jamais. Le meme defaut
   se serait declare sur n'importe quel contenu large ; il se voyait ici parce que huit recompenses
   sont ce que le jeu a de plus large a montrer dans une feuille. */
body.ui-v2 .ov-simple .ovs-body { width: 100%; min-width: 0; box-sizing: border-box; }
/* ET LE RAIL PASSE SUR PLUSIEURS RANGEES ICI (owner). Sur la CARTE il reste une seule ligne qui
   defile - une progression se lit en ligne. Mais dans la feuille on ne progresse pas, on INVENTORIE
   ce qu'il y a a gagner : la lecture y est meilleure sur deux rangees que sur une ligne qu'il faut
   faire glisser, et la feuille a de la hauteur a revendre. */
body.ui-v2 .ov-simple .chal-rail:not(.compact) {
  flex-wrap: wrap; justify-content: center; overflow-x: visible; width: 100%; row-gap: 7px;
}

/* --- LES ONGLETS DU PROFIL ---------------------------------------------------------------------- */
body.ui-v2 #profileTabs { margin: 0 auto 8px; width: min(340px, 100%); }
body.ui-v2 #profilePaneBoard { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; gap: 10px; padding-bottom: 12px; }

/* ======================================================================================================
   48 · L'ECRAN D'OBJECTIF EST UNE PAGE, PAS UNE POP-UP (owner 2026-08-27)
   Ce n'est pas une interruption : c'est le SEUIL du niveau, le moment ou le joueur decide d'y entrer.
   Une pop-up dit « pardon de te deranger », une page dit « voici ou tu entres ». Il portait deja
   `data-noclose` pour cette raison - il n'a jamais eu de croix, et c'est voulu : on en sort par
   COMMENCER ou par « retour a l'accueil », pas en fermant une fenetre.
   ====================================================================================================== */
body.ui-v2 #ovObjective {
  padding: 0 !important; align-items: stretch !important;
  background: var(--vv-bg, #1E1031) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
body.ui-v2 #ovObjective .panel.obj-intro {
  width: 100% !important; max-width: 100% !important; height: 100% !important; max-height: 100% !important;
  border: 0 !important; border-radius: 0 !important; box-shadow: none !important;
  background: none !important;
  justify-content: center !important;
  /* owner 2026-08-31 : « le bouton pour demarrer et le bouton retour a l'accueil sont trop bas, ils
     passent derriere le menu ». Mesure : le retour a l'accueil couvrait 1480 -> 1513, la barre du bas
     commence a 1487 - vingt-six de ses trente-trois pixels etaient derriere elle, et c'est le bouton qui
     sert a RENONCER, donc celui qu'on cherche quand on ne veut plus jouer.
     La reserve de 2026-08-29 (54 px) prenait le BORD DE L'ECRAN pour reference. La bonne reference est
     la BARRE, qui mesure ~84 px et que `--nav-total` connait depuis qu'elle est mesuree au lieu d'etre
     devinee (majHauteurNav). Elle porte deja la safe-area : on ne l'ajoute donc pas deux fois. */
  padding: calc(env(safe-area-inset-top, 0px) + 28px) 26px calc(var(--nav-total) + 22px) !important;
  overflow-y: auto !important;
}
/* Le contenu reste dans une colonne lisible meme sur tablette : une page pleine largeur etirerait le
   texte en bandes, et l'ecran perdrait le calme qu'on vient de lui donner. */
body.ui-v2 #ovObjective .panel.obj-intro > * { max-width: 420px; width: 100%; margin-inline: auto; }
/* Les deux sorties sont ANCREES EN BAS, dans la zone du pouce - c'est un ecran ou l'on decide. */
body.ui-v2 #ovObjective .obj-intro-btns { margin-top: auto !important; padding-top: 26px; }
body.ui-v2 #ovObjective .obj-home-btn { margin-top: 12px !important; }

/* ======================================================================================================
   44 · LA CEREMONIE D'ECRIN : LE FOND (owner 2026-08-26)
   Trois demandes, une par couche.

   1. LE FOND, c'est le violet de l'accueil, SANS le motif de diamants qui derive. Le motif etait la
      pour donner de la matiere a un ecran vide ; l'ecran ne l'est plus (coffre, rayons, anneaux,
      etincelles, cartes), et un motif anime derriere tout cela ne fait qu'ajouter du bruit sous le
      seul objet qu'on veut regarder. On l'eteint plutot que de le retirer du DOM : la meteorite s'en
      sert encore pour SA scene spatiale, qui est une signature a part.

   2. LES RAYONS S'ELARGISSENT ET S'EFFACENT. « Plus larges, avec un centre plus grand, et aussi plus
      discrets, presque invisibles. » Les trois se reglent separement :
        - plus larges : la base de chaque pointe s'ouvre (le clip-path passe de 14 % a 40 % de la
          hauteur a l'origine), et les secteurs coniques des ecrans de revelation passent de 20 deg
          sur 40 a 28 sur 40 ;
        - centre plus grand : le disque noir qui masque les rayons (ils sont en `screen`, donc le
          noir les efface) recule de 42 % a 58 % du rayon - la zone ou ils vivent double presque ;
        - presque invisibles : les opacites sont divisees par trois, et le noir central passe de
          0,55 a 0,78, ce qui eteint encore ce qui reste.

   3. LES AUTRES ECRANS SUIVENT, en gardant leur couleur de rarete : les memes valeurs sont posees
      sur `.rays` et `#rrRays`, dont la teinte vient de `--rc` / `--rr-bg` et ne bouge pas.
   ====================================================================================================== */

/* 1 · plus de motif derriere la ceremonie ni derriere les revelations. La METEORITE garde le sien :
   c'est une scene spatiale voulue, pas la texture de fond generique qu'on retire ici. */
body.ui-v2 #ovBox.cer-on .cer-pat { opacity: 0; animation: none; }
body.ui-v2 #ovBox.cer-meteorite .cer-pat { opacity: .5; animation: cerPat 26s linear infinite; }
body.ui-v2 #ovReward::after { opacity: 0; }

/* 2 · les pointes de la ceremonie : base ouverte, et presque effacees. */
body.ui-v2 .cer-ray .rw {
  clip-path: polygon(0 30%, 0 70%, 100% 0, 100% 100%);
  background: linear-gradient(90deg, rgba(255, 255, 255, .46) 0%, rgba(255, 255, 255, .24) 38%,
                                     rgba(255, 255, 255, .08) 70%, rgba(255, 255, 255, 0) 94%);
}
body.ui-v2 #ovBox.cer-on .cer-rayA { opacity: .10; }
body.ui-v2 #ovBox.cer-on .cer-rayB { opacity: .06; }
body.ui-v2 #ovBox.cer-exotique .cer-rayA, body.ui-v2 #ovBox.cer-mythique .cer-rayA,
body.ui-v2 #ovBox.cer-meteorite .cer-rayA { opacity: .14; }
body.ui-v2 #ovBox.cer-exotique .cer-rayB, body.ui-v2 #ovBox.cer-mythique .cer-rayB,
body.ui-v2 #ovBox.cer-meteorite .cer-rayB { opacity: .09; }
/* LE DERNIER TEMPS : tout ce qui eclaire s'eteint AVEC le coffre (owner 2026-08-28).
   Le coffre s'efface en 0.4 s (`.box-stage.fade`) ; les pointes, elles, restaient allumees jusqu'a la
   fermeture. On les fait partir sur la meme duree - la source et sa lumiere disparaissent ensemble,
   ce qui est la seule facon de ne pas laisser une etoile suspendue dans le vide.
   Les anneaux et les etincelles suivent : ce sont aussi des effets de la ceremonie, et rien de ce qui
   celebrait le coffre ne doit lui survivre. */
body.ui-v2 #ovBox.cer-fin .cer-ray,
body.ui-v2 #ovBox.cer-fin #cerRings,
body.ui-v2 #ovBox.cer-fin #cerFx {
  opacity: 0 !important;
  transition: opacity .42s ease !important;
}
body.ui-v2 #ovBox.cer-sum .cer-rayA { opacity: .04; }
body.ui-v2 #ovBox.cer-sum .cer-rayB { opacity: .03; }

/* 3 · les rayons coniques des ecrans de revelation. Meme grammaire, la couleur reste celle de la
   rarete (`--ray-a` / `--rc`). Le disque noir est ce qui les EFFACE (ils sont en `mix-blend-mode:
   screen`) : le reculer agrandit le centre, le foncer les rend discrets. */
body.ui-v2 .rays {
  background:
    radial-gradient(circle, rgba(0, 0, 0, .78) 0%, rgba(0, 0, 0, .88) 30%, #000 58%, #000 100%),
    repeating-conic-gradient(from 0deg, var(--ray-a) 0deg 28deg, transparent 28deg 40deg);
}
body.ui-v2 #rrRays.rr-rays {
  background:
    radial-gradient(circle, rgba(0, 0, 0, .78) 0%, rgba(0, 0, 0, .88) 30%, #000 58%, #000 100%),
    repeating-conic-gradient(from 0deg, var(--rc, #fff) 0deg 28deg, transparent 28deg 40deg);
}
body.ui-v2 .tu-rays {
  background:
    radial-gradient(circle, rgba(0, 0, 0, .78) 0%, rgba(0, 0, 0, .88) 30%, #000 58%, #000 100%),
    repeating-conic-gradient(from 0deg, rgba(255, 255, 255, .6) 0deg 28deg, transparent 28deg 40deg);
}
body.ui-v2 .rays.on { opacity: .13; }

/* ======================================================================================================
   45 · LE RAIL DE RECOMPENSES D'UN DEFI (owner 2026-08-26)
   « Les recompenses de chaque niveau devraient etre visibles depuis la carte de l'evenement avant de
   lancer le defi. » Une marche par niveau, dans l'ordre : gagnee, en cours, a venir. La marche en
   cours porte la monture d'or - c'est la seule qu'on peut gagner maintenant.
   ====================================================================================================== */
/* UNE SEULE LIGNE, JAMAIS DEUX (owner 2026-08-26). Un rail replie sur deux rangees cesse de se lire
   comme une progression : on ne sait plus ou est le debut. Au-dela de ce que la largeur permet, la
   ligne DEFILE (le rendu y amene la marche en cours). `nowrap` est ce qui compte ici ; le reste n'est
   que de la mise en forme. */
body.ui-v2 .chal-rail {
  display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
  overflow-x: auto; overflow-y: hidden; scroll-behavior: smooth;
  padding: 2px 0; margin: 0 -2px;
}
body.ui-v2 .chal-rail.compact { gap: 4px; margin-top: 1px; }
body.ui-v2 .cr-pas { flex: 0 0 auto; }
/* La marque du COFFRE DU JOUR : un point d'or sur l'ecrin, pour qu'il ne se confonde pas avec un
   ecrin mythique ordinaire - c'est le seul canal qui le donne. */
body.ui-v2 .cr-jour { position: absolute; top: 2px; right: 2px; width: 6px; height: 6px; border-radius: 50%; background: var(--vv-gold-2); box-shadow: 0 0 5px var(--vv-gold-2); }
body.ui-v2 .cr-pas { position: relative; }
body.ui-v2 .cr-pas {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 4px 7px; border-radius: 10px; line-height: 1;
  background: rgba(0, 0, 0, .52); border: 1.5px solid var(--vv-line);
  font-size: 11px; font-weight: 900; color: var(--vv-gold-text);
}
body.ui-v2 .chal-rail.compact .cr-pas { padding: 3px 4px; border-radius: 8px; }
body.ui-v2 .cr-pas img, body.ui-v2 .cr-pas .box-img { width: 18px; height: 18px; object-fit: contain; }
body.ui-v2 .chal-rail:not(.compact) .cr-pas img, body.ui-v2 .chal-rail:not(.compact) .cr-pas .box-img { width: 22px; height: 22px; }
/* DEJA GAGNEE : on la garde visible mais en retrait. L'effacer ferait perdre la lecture du chemin
   parcouru, qui est la moitie de ce que ce rail raconte. */
body.ui-v2 .cr-pas.fait { opacity: .38; }
body.ui-v2 .cr-pas.ici { border-color: var(--vv-gold-4); background: rgba(0, 0, 0, .52); box-shadow: 0 0 10px rgba(242, 196, 99, .30); }
body.ui-v2 .defi-rail-h { font-size: 11.5px; letter-spacing: .1em; color: rgba(255, 255, 255, .72); margin: 12px 0 6px; text-align: center; }
/* Le SCORE atteint, sur l'ecran de fin d'un defi : le joueur vient de le voir grimper, il doit le
   retrouver ici plutot que d'etre felicite pour un chiffre qui a disparu. */
body.ui-v2 .defi-score { font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 26px; color: #fff; text-align: center; margin: 2px 0 8px; }
body.ui-v2 .ct-prix { display: inline-flex; align-items: center; gap: 3px; }


/* ======================================================================================================
   46 · LA FICHE D'UNE GEMME SOUS LA DYNAMIC ISLAND (owner 2026-08-26)
   « Le nom de la gemme est trop haut et est masque par la Dynamic Island sur iPhone, il faudrait
   descendre le nom et par la meme occasion remonter la gemme 3D qui me semble un peu basse. »

   La cause est nette : `#ovGemDetail.gd-ov` ouvre sur `padding: 16px 14px calc(16px + safe-bottom)`.
   Le BAS respecte l'encoche, le HAUT non - 16 px fixes, alors qu'un iPhone a Dynamic Island reserve
   ~59 px en haut. Le titre passait donc dessous. Le jeton `--safe-top` existe deja (style.css) et
   n'etait simplement pas utilise ici.
   Et puisque le titre descend, la pierre remonte : elle mord un peu plus sur le cadre (-26 -> -34 px)
   et regagne la place que le titre vient de prendre. Le bilan vertical est neutre.
   ====================================================================================================== */
body.ui-v2 #ovGemDetail.gd-ov {
  padding-top: calc(18px + var(--safe-top, 0px));
  justify-content: flex-start;   /* le contenu part du HAUT : sinon le centrage vertical annule le decalage */
}
body.ui-v2 #ovGemDetail .gd-stack { margin-top: auto; margin-bottom: auto; }
body.ui-v2 #ovGemDetail .gd-viewer { margin-bottom: -34px; }

/* ======================================================================================================
   47 · LES COMPTES A REBOURS D'EVENEMENT (owner 2026-08-26)
   « Un compte a rebours utilisant le meme tick d'horloge que pour l'ouverture des ecrins. » C'est
   litteralement le meme : `clocks.js` fabrique la meme pastille `.rd-timer` (chiffre + aiguille qui
   avance d'un quart de tour par seconde) dans tout element portant `data-echeance`. Ne restait qu'a
   l'habiller dans ce contexte-ci - sur une carte, pas dans un ecrin.
   ====================================================================================================== */
body.ui-v2 .chal-fin, body.ui-v2 .ct-horloge {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
}
body.ui-v2 .chal-fin { margin-top: 2px; }
body.ui-v2 .ev-lb { font-size: 10.5px; letter-spacing: .06em; color: rgba(255, 255, 255, .55); }
body.ui-v2 .chal-fin .rd-timer, body.ui-v2 .ct-horloge .rd-timer {
  position: static; display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px 3px 7px; border-radius: 99px;
  background: rgba(0, 0, 0, .42); border: 1.5px solid var(--vv-line);
  transform: none; box-shadow: none;
}
body.ui-v2 .chal-fin .rd-num, body.ui-v2 .ct-horloge .rd-num {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif; font-size: 13px; color: var(--vv-gold-text);
  -webkit-text-stroke: 0; text-shadow: none; white-space: nowrap;
}
body.ui-v2 .chal-fin .rd-clock, body.ui-v2 .ct-horloge .rd-clock {
  width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--vv-gold-text);
  position: relative; flex: 0 0 auto; background: none;
}
body.ui-v2 .chal-fin .rd-clock i, body.ui-v2 .ct-horloge .rd-clock i {
  position: absolute; left: 50%; top: 50%; width: 1.6px; height: 5px; background: var(--vv-gold-text);
  transform-origin: 50% 100%; transition: transform .3s cubic-bezier(.34, 1.56, .64, 1);
}
/* LA DERNIERE HEURE : le compte a rebours cesse d'informer et se met a PREVENIR. Rouge, et rien
   d'autre - pas d'animation en boucle sur un ecran que le joueur parcourt. */
body.ui-v2 .ev-urgent .rd-timer { border-color: rgba(255, 122, 122, .7); background: rgba(120, 20, 34, .38); }
body.ui-v2 .ev-urgent .rd-num, body.ui-v2 .ev-urgent .rd-clock { color: #ff9a9a; border-color: #ff9a9a; }
body.ui-v2 .ev-urgent .rd-clock i { background: #ff9a9a; }
body.ui-v2 .ct-lot { display: flex; align-items: center; gap: 5px; font-size: 11px; color: rgba(255, 255, 255, .68); }
body.ui-v2 .ct-lot img, body.ui-v2 .ct-lot .box-img { width: 20px; height: 20px; object-fit: contain; }
body.ui-v2 .ct-horloge { flex-direction: column; align-items: flex-end; gap: 3px; }


/* ================================================================================================
   ZONE DANGEREUSE DU PROFIL (owner 2026-08-30).
   Deux actions irreversibles, en bas de page. Le rouge du skin (`--coral`) est deja celui des
   notifications ; ici il ne dit pas « regarde », il dit « attention » - c'est le meme signal, et un
   jeu n'a pas besoin de deux rouges. Les boutons sont en CONTOUR et non pleins : un aplat rouge
   attire le pouce, et ce sont justement les deux boutons qu'on ne veut pas voir touches par erreur.
   ================================================================================================ */
body.ui-v2 .danger-zone {
  margin: 26px auto 8px; padding: 14px 14px 16px; max-width: 460px;
  border: 1.5px solid rgba(196, 40, 63, .45); border-radius: 16px;
  background: rgba(196, 40, 63, .07);
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
body.ui-v2 .danger-head {
  margin: 0 0 4px; font-family: 'Lilita One', 'Trebuchet MS', sans-serif;
  font-size: 12px; letter-spacing: .12em; color: var(--coral, #C4283F);
}
body.ui-v2 .btn-danger {
  background: transparent; border: 1.5px solid var(--coral, #C4283F);
  color: var(--coral, #C4283F); box-shadow: none; text-shadow: none;
}
body.ui-v2 .btn-danger:active { background: rgba(196, 40, 63, .16); transform: translateY(2px); }
/* La note qui SUIT chaque bouton : elle dit ce qui est perdu. Sans elle, « REMETTRE A ZERO » et
   « SUPPRIMER LE COMPTE » se ressemblent assez pour etre confondus. */
body.ui-v2 .danger-note {
  margin: 0 0 8px; max-width: 32ch; text-align: center;
  font-size: 11px; line-height: 1.35; color: rgba(255, 255, 255, .62);
}
body.ui-v2 .danger-note:last-child { margin-bottom: 0; }


/* ================================================================================================
   LE SELECTEUR D'AVATARS DEFILE (owner 2026-08-30 : « la liste des avatars est trop longue et deborde
   du cadre »). Mesure du defaut : panneau plafonne a 612 px, contenu a 971 px, et `overflow-y`
   calcule a `visible` - la liste sortait donc simplement du cadre.

   Le panneau NE PEUT PAS defiler lui-meme, et c'est voulu : sa croix de fermeture est posee en debord
   (`top:-14px; right:-10px`), et un conteneur qui defile CLIPPE ce qui deborde - la croix disparait et
   l'ecran devient sans sortie. C'est deja documente dans style.css, section « 1 - CROIX DE FERMETURE
   ROGNEE ». Le defilement passe donc a un ENFANT, comme le veut le motif du depot.

   `flex: 1 1 auto` + `min-height: 0` : sans le `min-height`, un enfant flex refuse de retrecir
   sous la taille de son contenu et le conteneur deborderait exactement comme avant - c'est le piege
   classique du defilement en flexbox.
   ================================================================================================ */
body.ui-v2 #ovAvatars .avatar-panel { overflow: visible; }
body.ui-v2 #avatarScroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;   /* le defilement ne se propage pas a la page derriere le calque */
  width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding-right: 2px;             /* la barre de defilement ne mord pas sur la derniere colonne */
}


/* L'ECRAN D'OBJECTIF DEFILE quand le mix de gemmes le fait deborder (owner 2026-08-30 : le mix a ete
   ajoute sous l'objectif, et cinq cartes suffisent a depasser la hauteur du panneau - mesure : 758 px
   de contenu pour 595 px de panneau).
   Meme motif que le selecteur d'avatars : le PANNEAU reste en overflow visible (sa croix de fermeture
   vit en debord), et c'est un ENFANT qui defile. Les boutons sont volontairement HORS de cet enfant :
   « JOUER » est l'action de l'ecran, elle ne doit jamais partir sous la ligne de flottaison.
   `min-height: 0` : sans lui, un enfant flex refuse de retrecir sous son contenu et rien ne defile. */
body.ui-v2 #ovObjective .panel { overflow: visible; }
body.ui-v2 #objIntroScroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}


/* ================================================================================================
   L'ECRAN DE NIVEAU TERMINE : LE SCORE EN VEDETTE, ET UNE FETE PROPORTIONNEE (owner 2026-08-30).

   Avant, « 8,54 M / 250 k » s'ecrivait d'un seul trait, les deux nombres a la meme taille : le regard
   ne savait pas lequel etait le sien. Et le titre changeait, mais rien d'autre - un objectif triple
   avait exactement la meme couleur qu'un objectif tout juste atteint.

   QUATRE PALIERS, poses par le moteur (`overshootTierOf` : +50 % / +100 % / +200 %) :
     t0  atteint      blanc, aucune animation - on a fait le travail, on ne fanfaronne pas
     t1  +50 %        or, une pulsation lente
     t2  +100 %       orange chaud, pulsation plus vive + lueur
     t3  +200 %       degrade anime, la seule fete qui bouge vraiment
   L'escalade est VISIBLE sans etre criarde : chaque palier ajoute une chose, il ne remplace pas tout.
   ================================================================================================ */
/* L'HABILLAGE DE VICTOIRE N'APPARTIENT PLUS A UN SEUL ECRAN (owner 2026-08-31 : « l'ecran de fin d'un
   defi devrait montrer le meme type d'ecran que lorsqu'un niveau normal est termine »).
   Ces regles etaient accrochees a #ovRound. C'est la CLASSE qui porte le contrat - `.rs-score`,
   `.rs-obj`, `.panel.win-tN` - et deux ecrans qui disent la meme chose doivent la dire de la meme
   facon, sans qu'on ait a recopier l'apparence de l'un dans l'autre. On retire donc l'identifiant :
   tout panneau qui porte ces classes est habille pareil, aujourd'hui l'ecran de niveau et celui des
   defis, demain les autres. */
/* `opacity: 1` REPRIS A LA MAIN, et c'est tout le defaut signale par l'owner : « le score est
   etrangement sombre par rapport au reste de l'interface ». `.round-sub` porte `opacity: .7` depuis
   l'ancienne mise en page (style.css), ou ce bloc n'etait qu'une legende de 13 px. Il porte
   aujourd'hui le SCORE en 40 px : la transparence heritee le rendait terne quelle que soit sa
   couleur - inutile de repeindre les paliers, ils etaient deja clairs, c'est le calque qui les
   eteignait. La cible garde sa discretion par sa PROPRE couleur (`.rs-obj`), pas par un calque. */
body.ui-v2 .round-sub { display: flex; align-items: baseline; justify-content: center; gap: 8px; flex-wrap: wrap; opacity: 1; }
body.ui-v2 .rs-score {
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif;
  font-size: 40px; line-height: 1; color: #fff;
  text-shadow: 0 3px 0 rgba(0, 0, 0, .35);
}
body.ui-v2 .rs-obj { font-size: 14px; color: rgba(255, 255, 255, .55); }

body.ui-v2 .panel.win-t1 .rs-score { color: var(--vv-gold-text); animation: winPulse 1.5s ease-in-out infinite; }
body.ui-v2 .panel.win-t2 .rs-score {
  color: #ffb44d; text-shadow: 0 3px 0 rgba(0, 0, 0, .35), 0 0 18px rgba(255, 150, 60, .55);
  animation: winPulse .95s ease-in-out infinite;
}
/* LE PALIER x3 EST REPEINT (owner 2026-08-31 : « pratiquement illisible, sombre sur sombre »).
   Il utilisait un degrade DECOUPE DANS LE TEXTE (`background-clip: text` + `color: transparent`). La
   technique est jolie quand elle marche, et invisible quand elle ne marche pas : si le decoupage
   echoue, il ne reste pas un texte terne, il reste un texte TRANSPARENT sur un panneau sombre. Et elle
   ne marche justement pas ici, parce que la MEME regle animait aussi un `transform: scale` (winPulse) :
   WebKit rasterise alors l'element sur sa propre couche et perd le fond decoupe. C'est la meme famille
   de piege que les deux deja documentes dans ce depot (masque + animation, filtre + couche imbriquee).
   On garde donc l'escalade - le palier le plus haut doit rester le plus spectaculaire - mais portee par
   une couleur PLEINE et une lueur, deux choses qui ne peuvent pas disparaitre. Blanc chaud vire a l'or,
   comme l'owner l'a demande. */
body.ui-v2 .panel.win-t3 .rs-score {
  color: #fff6d8;
  text-shadow: 0 3px 0 rgba(0, 0, 0, .4),
               0 0 10px rgba(255, 214, 122, .95),
               0 0 26px rgba(255, 154, 77, .75),
               0 0 46px rgba(255, 110, 168, .5);
  animation: winPulse .8s ease-in-out infinite;
}
/* Le TITRE suit la meme escalade, en plus sobre : c'est le score qu'on regarde. */
body.ui-v2 .panel.win-t1 #roundTitle, body.ui-v2 .panel.win-t1 .tour-day-title { color: var(--vv-gold-text); }
body.ui-v2 .panel.win-t2 #roundTitle, body.ui-v2 .panel.win-t2 .tour-day-title { color: #ffb44d; }
body.ui-v2 .panel.win-t3 #roundTitle, body.ui-v2 .panel.win-t3 .tour-day-title { color: #ffd9ea; text-shadow: 0 0 14px rgba(255, 110, 168, .6); }
@keyframes winPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }
@keyframes winSweep { to { background-position: 260% 0; } }
/* Mouvement reduit : on garde la COULEUR (elle porte l'information) et on coupe tout ce qui bouge. */
@media (prefers-reduced-motion: reduce) {
  body.ui-v2 .rs-score { animation: none !important; }
}


/* Les etoiles qui rejoignent la carte de tournoi (owner 2026-08-30). Hors flux, au-dessus de tout,
   et surtout `pointer-events: none` : elles traversent l'ecran d'accueil pendant que le joueur peut
   deja taper - une etoile qui avale un tap serait un defaut bien pire que l'absence d'animation. */
body.ui-v2 .tour-fly-star {
  position: fixed; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  z-index: 90; pointer-events: none; opacity: .95;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .5));
  transition: transform .58s cubic-bezier(.5, 0, .3, 1), opacity .58s ease-in;
  will-change: transform, opacity;
}
/* Le decorticage des etoiles reprend la mise en page du decorticage d'XP ; seule l'etoile du total
   est neuve. */
body.ui-v2 #ovRound .xpb-star { width: 16px; height: 16px; object-fit: contain; vertical-align: -3px; margin-right: 4px; }
body.ui-v2 #ovRound .xpb-n { font-style: normal; opacity: .6; font-size: .85em; }


/* ================================================================================================
   LA FICHE D'UN ECRIN : LES LIBELLES ETAIENT ILLISIBLES (owner 2026-08-30, capture a l'appui : « on
   voit des chiffres mais on ne sait pas a quoi ils correspondent »).

   Ils etaient bien la - « Pieces », « Cosmetique », « Gemmes » - mais peints en `rgba(42,26,77,.72)`,
   un violet sombre pense pour le panneau CLAIR de la V1 et jamais repris au passage au skin Velvet.
   Contraste mesure sur le panneau actuel : 1,35:1, quand WCAG en demande 4,5. La valeur, elle, est
   blanche a 21:1 - d'ou des chiffres qui flottaient sans etiquette.

   La ligne des GEMMES est en outre mise en avant : c'est ce pour quoi on ouvre un ecrin.
   ================================================================================================ */
body.ui-v2 #ovBoxDetail .ci-lbl,
body.ui-v2 .bd-rows .ci-lbl {
  color: rgba(255, 255, 255, .82) !important;
  font-size: 13px; letter-spacing: .01em;
}
body.ui-v2 #ovBoxDetail .ci-sub,
body.ui-v2 .bd-rows .ci-sub { color: rgba(255, 255, 255, .58); opacity: 1; }
body.ui-v2 #ovBoxDetail .ci-val,
body.ui-v2 .bd-rows .ci-val { color: #fff; }
/* La ligne des gemmes porte l'or du jeu et un filet au-dessus : elle ouvre la fiche et doit se
   distinguer des deux lignes qui la suivent sans crier. */
body.ui-v2 #ovBoxDetail .ci-row.ci-star .ci-lbl { color: var(--vv-gold-text) !important; font-weight: 900; }
body.ui-v2 #ovBoxDetail .ci-row.ci-star .ci-val { color: var(--vv-gold-text); font-size: 1.12em; }
body.ui-v2 #ovBoxDetail .ci-row + .ci-row,
body.ui-v2 #ovBoxDetail .ci-sub + .ci-row {
  border-top: 1px solid rgba(255, 255, 255, .10); padding-top: 7px; margin-top: 3px;
}


/* UNE RARETE PAR LIGNE dans la fiche d'un ecrin (owner 2026-08-30). Elles etaient enfilees sur une
   seule ligne separee par des points medians, qui repassait a la ligne au milieu d'un nombre sur un
   ecran de telephone. Empilees en petites lignes libelle/valeur, les quantites s'alignent en colonne
   et se comparent d'un coup d'oeil.
   Retrait leger a gauche : ces lignes DEPENDENT de la ligne « Gemmes » au-dessus, l'indentation le dit
   sans avoir a l'ecrire. */
body.ui-v2 #ovBoxDetail .ci-subrow {
  display: flex; align-items: baseline; gap: 8px;
  padding: 1px 0 1px 14px;
  font-size: 12px; color: rgba(255, 255, 255, .66);
}
body.ui-v2 #ovBoxDetail .ci-subrow > b { margin-left: auto; font-weight: 800; color: rgba(255, 255, 255, .86); }
/* Une rarete NON garantie : la meme ligne, en retrait de ton, et sa valeur en italique - « possible »
   n'est pas une quantite, il ne doit pas se lire comme les chiffres au-dessus. */
body.ui-v2 #ovBoxDetail .ci-subrow.ci-maybe { color: rgba(255, 255, 255, .45); }
body.ui-v2 #ovBoxDetail .ci-subrow.ci-maybe > b { font-style: italic; font-weight: 600; color: rgba(255, 255, 255, .55); }
/* Le filet de separation ne doit pas tomber entre une ligne et ses sous-lignes. */
body.ui-v2 #ovBoxDetail .ci-subrow + .ci-row { border-top: 1px solid rgba(255, 255, 255, .10); padding-top: 7px; margin-top: 5px; }
body.ui-v2 #ovBoxDetail .ci-row + .ci-subrow { border-top: 0; padding-top: 2px; margin-top: 2px; }


/* ================================================================================================
   OFFRES DU JOUR — refonte a neuf cases (owner 2026-08-30).
   La grille etant a trois colonnes, neuf cases font exactement trois rangees pleines : rien a ajuster
   de ce cote. Restent le bouton de rafraichissement, pose sous la grille, et la case CADEAU, qui doit
   se distinguer d'un deal payant SANS voler la vedette a la tuile gratuite (celle de la pub, doree).
   ================================================================================================ */
body.ui-v2 .deals-refresh { display: flex; justify-content: center; margin: 2px 0 8px; }
body.ui-v2 .deals-refresh .btn { padding: 7px 16px; }
/* La case CADEAU : deuxieme tuile a ne rien couter, elle doit donc se distinguer de la grille comme le
   fait la tuile gratuite - mais en VERT, la couleur que le jeu donne deja au bouton « gratuit », pour
   qu'on ne la confonde pas avec l'offre contre pub, qui est doree.
   Le `!important` n'est pas un ornement : la regle generale de .shop-deal-tile ci-dessus pose son fond
   en `!important`, et sans lui une declaration plus specifique perdrait quand meme. */
body.ui-v2 .shop-deal-tile.deal-gift-tile {
  background:
    linear-gradient(#3A2A12, #241804) padding-box,
    linear-gradient(var(--vv-gold-1), var(--vv-gold-3) 50%, var(--vv-gold-5)) border-box !important;
  border: 2px solid transparent !important;
}

/* Les gemmes debloquees a chaque rang (banniere de l'echelle, owner 2026-08-30). La rangee compte
   jusqu'a SEPT pierres au platine et au diamant : sans retour a la ligne, elle deborderait de la
   banniere sur les petits ecrans. Et l'art vient de gemArtStyle, qui pose un background-image : la
   taille doit donc etre portee ici, l'atlas ne l'apporte pas. */
body.ui-v2 .rt-gem-icons { flex-wrap: wrap; justify-content: center; max-width: 100%; row-gap: 6px; gap: 7px; }
/* L'ENCADRE DES GEMMES D'UN RANG (owner 2026-09-05). Il etait un filet de 1 px avec trois vignettes de
   la taille d'un caractere : sur une banniere qui prend toute la largeur, ca ne se lisait pas comme
   une annonce mais comme un reste de mise en page. Il reprend donc la matiere des autres encadres de
   l'echelle - creux sombre, liseré d'or, legende en petites capitales EN HAUT - et les pierres
   doublent de taille. Le filet superieur disparait : c'est le cadre qui separe, maintenant. */
body.ui-v2 .rt-gem-unlock {
  border-top: 0 !important;
  /* owner 2026-09-05 : « rapproche l'encadre de l'image de rang, et deplace la marge en bas ». Il
     annonce ce que le rang qu'on vient de franchir OUVRE : colle a la banniere, il se lit comme sa
     suite ; espace en dessous, il cesse de coller au premier niveau qui le suit. */
  margin: 2px auto 18px !important; padding: 8px 14px 10px !important;
  width: max-content; max-width: 92%;
  border-radius: 14px;
  background: var(--card-deep);
  box-shadow: inset 0 0 0 1.5px var(--vv-gold-4), inset 0 3px 7px hsl(var(--h) 70% 4% / .55), 0 3px 0 rgba(0,0,0,.28);
}
body.ui-v2 .rt-gem-cap {
  display: block; margin-bottom: 7px; text-align: center;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif;
  font-size: 10.5px; letter-spacing: .10em; text-transform: uppercase;
  color: var(--vv-gold-text);
}
body.ui-v2 .rt-gem-unlock .rt-gem { width: 34px !important; height: 34px !important; }
/* LE BAS DE L'ECHELLE PASSAIT SOUS LA PASTILLE (owner 2026-09-05 : « le bas de l'echelle est masque
   par la pastille qui indique le rang tout en bas »). `.route-foot` est en `position: absolute` et
   flotte AU-DESSUS de la liste - elle ne prend donc aucune place dans le flux, et les derniers
   noeuds du rang Bois finissaient dessous, atteignables mais illisibles. La liste se reserve la
   hauteur de la pastille en bas de son defilement ; la safe-area est comptee comme dans `.route-foot`
   elle-meme, sinon la reserve manquerait exactement de la hauteur de la barre d'accueil sur iPhone. */
body.ui-v2 #ovRoute .route-scroll { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 104px); }
/* Mesure : sept pierres de 22 px + leurs gouttieres font 172 px, la banniere en offre 177. Les sept
   du platine et du diamant tiennent donc sur UNE rangee sur cet ecran, et le retour a la ligne ne
   sert que de filet sur les telephones etroits - il ne s'invite plus a six pierres. */
body.ui-v2 .rt-gem.rt-gem-v2 {
  width: 22px; height: 22px; border-radius: 5px;
  background-size: cover; background-position: center;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .5));
}


/* ================================================================================================
   LA BOUTIQUE — trois corrections du 2026-08-30 (signalement owner : « il n'y a plus assez de place
   pour la partie des packs de joyaux payants », « mettre un peu plus de marge entre les titres et
   les cartes »).
   ================================================================================================ */

/* 1 · UN TITRE DE SECTION DOIT POUVOIR SE MASQUER. La regle du filet d'or pose `display: flex` en
   `!important` : un `style.display = 'none'` pose en JS ne pouvait pas la battre (entre un
   `!important` de feuille et un inline sans `!important`, la feuille gagne). « JEWEL PACKS » restait
   donc affiche au-dessus d'une rubrique vide partout ou les achats en monnaie reelle n'existent pas.
   La classe porte le meme poids ET une specificite superieure : elle gagne, et elle se lit. */
body.ui-v2 .shop-section.est-masque,
body.ui-v2 .sec-title.est-masque,
body.ui-v2 .coll-sec-head.est-masque { display: none !important; }

/* 2 · DE L'AIR ENTRE UN TITRE ET SES CARTES. Le titre etait colle a sa grille (margin-top 6, rien en
   dessous) : les rubriques se lisaient comme un seul bloc continu, et l'oeil ne savait plus quel
   titre commandait quelle rangee. On respire au-dessus (l'echelle de marges V2 : 22) et surtout EN
   DESSOUS (12) - c'est l'espace du bas qui rattache visuellement le titre a ce qui le suit. */
body.ui-v2 #view-shop .shop-section { margin: 22px 0 12px; }
body.ui-v2 #view-shop .shop-section:first-of-type { margin-top: 8px; }
/* La note de bas de rubrique et la rangee de rafraichissement ne doivent pas manger cet air. */
body.ui-v2 #view-shop .dl-note { margin-bottom: 2px; }

/* 2 bis · DE L'AIR SOUS LA DERNIERE RANGEE (owner 2026-08-30 : « meme tout en bas, les packs de
   joyaux sont trop proches du menu »).
   La reservation commune aux vues defilantes vaut `--nav-total + 10px`, soit une dizaine de pixels
   entre le dernier pixel de contenu et la barre. Ca suffit a une LISTE, dont la derniere ligne se
   termine par du blanc ; ca ne suffit pas a une grille de CARTES, dont le dernier element a un bord,
   une ombre et un bouton qui vont jusqu'en bas. La boutique se termine toujours par une rangee de
   cartes - c'est la seule vue dans ce cas, d'ou l'exception ici plutot qu'un changement global.
   Meme specificite que la regle commune, mais velvet.css est charge en DERNIER : elle gagne. */
body.ui-v2 #view-shop { padding-bottom: calc(var(--nav-total) + 48px); }

/* 3 · TROIS COLONNES RESTENT TROIS COLONNES. Sur large, la regle de la boutique remplace la grille
   par un `auto-fit` qui tient QUATRE cartes : six packs s'affichaient 4 + 2, une rangee pleine et
   une rangee bancale, alors que la classe `three` dit exactement le contraire. On la fait respecter,
   en gardant le plafond de largeur qui empeche les cartes de s'etaler. */
@media (min-width: 720px) {
  body.ui-v2[data-view="shop"] .shop-grid.three {
    grid-template-columns: repeat(3, minmax(0, 230px));
    justify-content: center;
  }
}


/* ================================================================================================
   LE DEFI DU JOUR : son ecrin sur l'ecran de fin, et son resultat sur la carte (owner 2026-08-31).
   ================================================================================================ */

/* L'ecrin gagne, montre en grand et pret a s'ouvrir : c'est lui la recompense du defi, pas une ligne
   de gains parmi d'autres. */
body.ui-v2 #ovDefiWin .defi-win-box { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 14px 0 2px; }
body.ui-v2 #ovDefiWin .defi-win-box .box-img { filter: drop-shadow(0 8px 22px rgba(0, 0, 0, .5)); animation: winPulse 1.8s ease-in-out infinite; }
body.ui-v2 #ovDefiWin .defi-win-box-lbl { font-size: 12.5px; letter-spacing: .4px; color: rgba(255, 255, 255, .66); }

/* LE RESULTAT DU JOUR, SUR LA CARTE. Le defi joue reste en liste (il ne disparait plus) : il lui faut
   donc dire d'un coup d'oeil ce qui s'est passe. Le VERT et le ROUGE portent l'information, mais pas
   SEULS - le libelle (« Reussi » / « Echoue ») la porte aussi, pour qui ne distingue pas les deux.
   La carte reste a .72 d'opacite comme toute carte inerte : elle appartient au passe. */
body.ui-v2 .chal-card.fait-reussi { border-color: rgba(126, 217, 87, .55) !important; }
body.ui-v2 .chal-card.fait-reussi .chal-prog { color: #8fe06a; font-weight: 900; }
body.ui-v2 .chal-card.fait-echoue { border-color: rgba(255, 110, 110, .5) !important; }
body.ui-v2 .chal-card.fait-echoue .chal-prog { color: #ff9c9c; font-weight: 900; }


/* ================================================================================================
   LA FORCE DE FRAPPE DU MIX, comparee a celle que le niveau conseille (owner 2026-08-31).
   Trois etats, et la COULEUR NE PORTE PAS SEULE l'information : chacun est double d'un mot
   (« À l'aise » / « Juste » / « Niveau difficile »). Un code purement chromatique laisserait de cote
   ceux qui ne distinguent pas le vert du rouge, sur l'ecran meme ou l'on decide de lancer ou non.
   ================================================================================================ */
body.ui-v2 .gg-power { display: flex; align-items: baseline; justify-content: center; gap: 7px; flex-wrap: wrap; }
body.ui-v2 .gg-power .gg-goal { font-size: 12.5px; color: rgba(255, 255, 255, .45); font-weight: 800; }
body.ui-v2 .gg-power .gg-verdict {
  font-size: 11px; font-weight: 900; letter-spacing: .4px; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; border: 1px solid currentColor;
}
body.ui-v2 .gg-power.gg-ok     b, body.ui-v2 .gg-power.gg-ok     .gg-verdict { color: #8fe06a; }
body.ui-v2 .gg-power.gg-juste  b, body.ui-v2 .gg-power.gg-juste  .gg-verdict { color: #ffb44d; }
body.ui-v2 .gg-power.gg-faible b, body.ui-v2 .gg-power.gg-faible .gg-verdict { color: #ff8b8b; }

/* ===== owner 2026-09-03 : stats colorees, collection (rarete / tri / trouvables / rebond), pastilles, defis, boutique ===== */
/* Une couleur par stat, partout (fiche de gemme, cartes du mix, ecran d'objectif) */
body.ui-v2 { --st-pow: #f3c14a; --st-mult: #a877ff; --st-rate: #3fd4c8; }
body.ui-v2 .gg-st { display: flex; align-items: center; gap: 5px; width: 100%; margin-top: 3px; font-size: 10px; line-height: 1.1; }
body.ui-v2 .gg-st em { font-style: normal; font-weight: 800; letter-spacing: .02em; opacity: .75; min-width: 3.4em; text-align: left; text-transform: uppercase; font-size: 9px; }
body.ui-v2 .gg-st b { min-width: 3em; text-align: right; font-weight: 900; font-size: 11px; }
body.ui-v2 .gg-bar { flex: 1; height: 6px; border-radius: 4px; background: rgba(255,255,255,.14); overflow: hidden; box-shadow: inset 0 1px 1px rgba(0,0,0,.35); }
body.ui-v2 .gg-bar i { display: block; height: 100%; border-radius: 4px; background: #fff; box-shadow: 0 0 6px rgba(255,255,255,.35); transition: width .35s ease-out; }
body.ui-v2 .gg-st.st-pow .gg-bar i, body.ui-v2 .gd-stat.st-pow .gd-g-now { background: var(--st-pow) !important; }
body.ui-v2 .gg-st.st-mult .gg-bar i, body.ui-v2 .gd-stat.st-mult .gd-g-now { background: var(--st-mult) !important; }
body.ui-v2 .gg-st.st-rate .gg-bar i, body.ui-v2 .gd-stat.st-rate .gd-g-now { background: var(--st-rate) !important; }
body.ui-v2 .gg-st.st-pow b { color: var(--st-pow); } body.ui-v2 .gg-st.st-mult b { color: var(--st-mult); } body.ui-v2 .gg-st.st-rate b { color: var(--st-rate); }
body.ui-v2 .gd-stat.st-pow .gd-stat-v { color: var(--st-pow) !important; } body.ui-v2 .gd-stat.st-mult .gd-stat-v { color: var(--st-mult) !important; } body.ui-v2 .gd-stat.st-rate .gd-stat-v { color: var(--st-rate) !important; }
body.ui-v2 .gg-card { padding-bottom: 8px; }

/* LA RARETE SE LIT SANS UN MOT (owner 2026-09-04 : « ajouter un cadre de la couleur correspondant a
   la rarete, ainsi qu'un leger halo de couleur en fond de chaque cadre de gemme, et donc retirer la
   pastille dans l'apercu de la collection »).
   Le cadre porte la couleur pleine, le halo la repose derriere la pierre. Deux signaux pour la meme
   information, aucun caractere a lire : sur une grille de trente vignettes, l'etiquette texte coutait
   plus d'attention qu'elle n'en rapportait. Le MOT, lui, reste sur la fiche (.gd-rar-tag), la ou on
   ne regarde qu'une pierre a la fois. */
body.ui-v2 .gemcard {
  position: relative;
  border-width: 2px !important;
  /* couleur PLEINE et non 80 % : c'est le seul trait qui distingue une commune d'une legendaire sur
     une grille de trente-quatre pierres, et il ne fait que 2 px de large. Une fois le defaut de
     cascade corrige ci-dessus, cette regle peint enfin ce qu'elle annonce. */
  border-color: var(--rc, #888) !important;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rc, #888) 30%, transparent),
              0 0 12px color-mix(in srgb, var(--rc, #888) 22%, transparent),
              0 6px 14px rgba(0,0,0,.35);
}
/* Le halo : DERRIERE tout le contenu de la carte, centre sur la pierre. En pseudo-element plutot
   qu'en `background-image` parce que la carte a deja un fond, et qu'un degrade pose dessus le
   remplacerait au lieu de s'y ajouter. */
body.ui-v2 .gemcard::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(78% 58% at 50% 34%,
    color-mix(in srgb, var(--rc, #888) 30%, transparent) 0%,
    color-mix(in srgb, var(--rc, #888) 11%, transparent) 46%,
    transparent 74%);
}
body.ui-v2 .gemcard > * { position: relative; z-index: 1; }
/* Une gemme qu'on ne possede pas garde sa couleur, en sourdine : elle annonce ce qu'on cherche. */
body.ui-v2 .gemcard.is-locked { border-color: color-mix(in srgb, var(--rc, #888) 42%, transparent) !important; }
body.ui-v2 .gemcard.is-locked::before { opacity: .45; }
/* Trouvable mais pas encore tombee : grisee, SANS cadenas */
body.ui-v2 .gemcard.is-unfound .gem-art.locked { filter: grayscale(.85) brightness(.75); opacity: .55; }
body.ui-v2 .gemcard.is-unfound .prev-name { opacity: .8; }
body.ui-v2 .gem-unfound { color: #b9b3d4 !important; }
body.ui-v2 .coll-sec-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
body.ui-v2 .coll-sec-head .coll-sort { margin-left: auto; font-size: 10px; padding: 4px 10px; }
body.ui-v2 .coll-sec-head .coll-sec-sub { font-size: 10px; opacity: .7; font-weight: 600; }
/* Rebond des cartes pendant le defilement (game feel) */
@keyframes gcBounce { 0% { transform: translateY(0) scale(1); } 45% { transform: translateY(-3px) scale(1.012); } 100% { transform: translateY(0) scale(1); } }
body.ui-v2 #gemGrid.is-scrolling .gemcard { animation: gcBounce .34s ease-out; }
body.ui-v2 #gemGrid.is-scrolling .prev-grid .gemcard:nth-child(3n+2) { animation-delay: .03s; }
body.ui-v2 #gemGrid.is-scrolling .prev-grid .gemcard:nth-child(3n) { animation-delay: .06s; }
@media (prefers-reduced-motion: reduce) { body.ui-v2 #gemGrid.is-scrolling .gemcard { animation: none; } }

/* Pastille « xM » sur la gemme de la piece EN MAIN (element de drag), meme habillage que sur une case */
body.ui-v2 #dragPiece .pcell { position: absolute; }
body.ui-v2 #dragPiece .pcell .gmul { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-weight: 900; font-size: 13px; color: #fff; text-shadow: 0 1px 0 #0a0618, 0 -1px 0 #0a0618, 1px 0 0 #0a0618, -1px 0 0 #0a0618, 0 2px 4px rgba(0,0,0,.6); pointer-events: none; z-index: 3; }

/* Le moment NOUVELLE GEMME */
@keyframes rrNewRing { 0% { transform: scale(.2); opacity: .9; } 100% { transform: scale(3.2); opacity: 0; } }
@keyframes rrNewTxt { 0% { transform: translate(-50%, -50%) scale(.4) rotate(-6deg); opacity: 0; } 30% { transform: translate(-50%, -50%) scale(1.25) rotate(2deg); opacity: 1; } 55% { transform: translate(-50%, -50%) scale(1) rotate(0); } 80% { opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1.05); opacity: 0; } }
.rr-newflash { position: absolute; inset: 0; pointer-events: none; z-index: 30; }
.rr-newflash .rr-newflash-ring { position: absolute; left: 50%; top: 42%; width: 160px; height: 160px; margin: -80px 0 0 -80px; border-radius: 50%; border: 6px solid var(--rc, #fff); box-shadow: 0 0 40px var(--rc, #fff); animation: rrNewRing 1.1s ease-out forwards; }
.rr-newflash .rr-newflash-txt { position: absolute; left: 50%; top: 42%; font-size: 30px; font-weight: 900; letter-spacing: .08em; color: #fff; -webkit-text-stroke: 1.5px #1a1030; paint-order: stroke fill; text-shadow: 0 0 18px var(--rc, #fff), 0 4px 0 #1a1030; white-space: nowrap; animation: rrNewTxt 1.5s ease-out forwards; }
.gp-total { display: block; font-size: 11px; opacity: .8; margin-top: 3px; font-weight: 700; }

/* Ecran d'objectif simplifie : le niveau, puis l'objectif en gros */
body.ui-v2 .obj-intro-target-big { font-size: 44px; line-height: 1.05; font-weight: 900; margin: 2px 0 10px; }
body.ui-v2 .obj-change-btn { margin-bottom: 8px; }
body.ui-v2 .obj-intro-btns { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }

/* Defis : recompenses en gemmes ; boutique : tickets */
.chal-rw-gem { display: inline-block; width: 22px; height: 22px; border-radius: 30%; background-size: cover; background-position: center; vertical-align: middle; box-shadow: 0 1px 3px rgba(0,0,0,.5); }
.chal-rw-gem.chal-rw-leg { background: radial-gradient(circle at 35% 30%, #fff 0%, var(--rc, #f3c14a) 55%, #3a2a10 100%); }
body.ui-v2 .ticket-card .ticket-art { width: 42px; height: 42px; object-fit: contain; display: block; margin: 0 auto 4px; }
body.ui-v2 .tickets-stock { font-size: 12px; opacity: .8; margin-left: 6px; }

/* ===== owner 2026-09-03 : lisibilite sur fond d'ecran personnalise (Velvet Vault) =====
   Le mecanisme existait (19-wallpapers.js : `--veil` calcule sur la LUMINANCE du fond, .04 sombre / .08 moyen / .12
   clair, + data-wp-band sur <body>) mais la peau Velvet Vault ne le consommait plus. Ici :
     1. un voile degrade sur l'image (plus fort en haut et en bas, la ou vivent le HUD et la barre de navigation),
        dose par --veil ;
     2. les surfaces qui portent du texte gagnent un fond plus dense quand le fond est clair ou charge ;
     3. les textes flottants (HUD, titres, noms de cartes) recoivent une ombre portee.
   Rien ne change sur le fond par defaut (body sans .wp-image). */
body.ui-v2.wp-image .ambient::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(10, 6, 24, .78) 0%, rgba(10, 6, 24, .38) 22%, rgba(10, 6, 24, .30) 62%, rgba(10, 6, 24, .80) 100%);
  opacity: clamp(.45, calc(var(--veil, .08) * 6), .95);   /* .04 -> .45 (fond sombre), .08 -> .48, .12 -> .72 (fond clair) */
}
body.ui-v2.wp-image[data-wp-band="light"] .ambient::after { opacity: .9; }
body.ui-v2.wp-image .hud-value, body.ui-v2.wp-image .hud-label, body.ui-v2.wp-image .view-title, body.ui-v2.wp-image .shop-section,
body.ui-v2.wp-image .coll-sec-head, body.ui-v2.wp-image .prev-name, body.ui-v2.wp-image .gem-lvl, body.ui-v2.wp-image .streak-title,
body.ui-v2.wp-image .home-id-name, body.ui-v2.wp-image .toast, body.ui-v2.wp-image .chal-nom, body.ui-v2.wp-image .ach-name {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85), 0 0 10px rgba(0, 0, 0, .55) !important;
}
body.ui-v2.wp-image[data-wp-band="light"] .card, body.ui-v2.wp-image[data-wp-band="light"] .gemcard, body.ui-v2.wp-image[data-wp-band="light"] .shop-card,
body.ui-v2.wp-image[data-wp-band="light"] .chal-card, body.ui-v2.wp-image[data-wp-band="light"] .daily-card, body.ui-v2.wp-image[data-wp-band="light"] .panel,
body.ui-v2.wp-image[data-wp-band="light"] .chip, body.ui-v2.wp-image[data-wp-band="light"] .nav {
  box-shadow: inset 0 0 0 999px rgba(12, 8, 28, .30), 0 6px 14px rgba(0, 0, 0, .45);
}
body.ui-v2.wp-image[data-wp-band="mid"] .card, body.ui-v2.wp-image[data-wp-band="mid"] .gemcard, body.ui-v2.wp-image[data-wp-band="mid"] .shop-card,
body.ui-v2.wp-image[data-wp-band="mid"] .chal-card, body.ui-v2.wp-image[data-wp-band="mid"] .daily-card {
  box-shadow: inset 0 0 0 999px rgba(12, 8, 28, .16), 0 6px 14px rgba(0, 0, 0, .4);
}

/* ===== owner 2026-09-03 : colonne desktop =====
   Sur grand ecran, l'interface reste dans une colonne centree (le fond d'ecran, lui, occupe tout l'ecran via
   .ambient en position fixed). #app est deja borne a 520 px (style.css) ; ce qui s'echappait : la barre du haut en
   jeu, l'echelle des niveaux (#routeScroll), les scenes plein ecran (cérémonies d'ecrins / revelation) et quelques
   panneaux etires. */
@media (min-width: 720px) {
  body.ui-v2[data-view="game"] #topbar { max-width: 520px !important; margin-left: auto !important; margin-right: auto !important; }
  body.ui-v2 #ovRoute #routeScroll { max-width: 560px !important; margin-left: auto !important; margin-right: auto !important; }
  body.ui-v2 .reward-sheet > *:not(.ambient):not(.rr-newflash) { max-width: 560px; margin-left: auto !important; margin-right: auto !important; }
  body.ui-v2 .overlay > .panel { max-width: 560px; }
  body.ui-v2 .ov-simple { max-width: 460px; }
  body.ui-v2 #gameHint { max-width: 560px; }
}

/* ============================================================================
   MODE EVENTS FERME — l'ecran « bientot » (owner 2026-09-04)
   Pas un ecran vide : une annonce. L'icone du mode en grand, une promesse en une
   phrase, puis la carte teaser du prochain evenement (elle porte deja son lot et
   son compte a rebours). Meme grammaire que les cartes du hub : fond sombre,
   bordure tiretee, or pour le titre.
   ========================================================================== */
body.ui-v2 .chal-soon {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 9px; margin: 6px 0 18px; padding: 26px 20px 24px;
  border-radius: 20px;
  background: rgba(0, 0, 0, .26);
  border: 1.5px dashed var(--vv-line);
}
body.ui-v2 .chal-soon-ic {
  width: 74px; height: 74px; object-fit: contain;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, .45));
  opacity: .92;
}
body.ui-v2 .chal-soon-t {
  margin: 2px 0 0;
  font-family: 'Lilita One', 'Trebuchet MS', sans-serif;
  font-size: 20px; letter-spacing: .5px; color: var(--vv-gold-text);
  text-shadow: 0 2px 0 rgba(0, 0, 0, .35);
}
body.ui-v2 .chal-soon-b {
  margin: 0; max-width: 30ch;
  font-size: 13px; line-height: 1.45; color: rgba(255, 255, 255, .68);
}

/* LA PASTILLE DE RARETE DE LA FICHE (owner 2026-09-04). Elle remplace la pastille doree, qui disait
   « RARE » sur un fond identique pour les huit raretes : la couleur y faisait tout le travail
   d'information et n'etait pas utilisee. Le texte bascule en sombre sur les deux raretes claires,
   sinon il disparait dans son propre fond. */
body.ui-v2 #ovGemDetail .gd-rar.gd-rar-tag {
  background: var(--rc, #888) !important;
  color: #14102a !important;
  box-shadow: 0 1px 4px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.28) !important;
}

/* LE CLASSEMENT AVAIT ETE OUBLIE (owner 2026-09-06 : « le degrade derriere le menu ne fonctionne
   toujours pas sur le leaderboard »).
   Le fondu des vues qui defilent est un MASQUE, pose sur quatre vues nommees une par une dans
   style.css (#view-collection / #view-board / #view-goals / #view-shop). Le classement n'est aucune
   des quatre : il vit dans un PANNEAU du profil, `#profilePaneBoard`, qui est le vrai conteneur de
   defilement (mesure : 593 px de haut pour 1489 de contenu). theme-tint.css lui definissait meme un
   `--fade`... que personne n'appliquait jamais. La variable existait, le masque non.

   Il est calibre sur SON PROPRE bord bas, et non sur `--nav-total` comme les quatre autres : le
   panneau ne passe pas derriere la barre, il s'arrete au-dessus. Une arithmetique empruntee a la
   barre y tomberait a cote des que la zone sure change - c'est-a-dire sur iPhone, et seulement la.
   Sans animation sur le conteneur, donc conforme a la regle iOS « jamais masque + animation ». */
body.ui-v2 #profilePaneBoard {
  --fade: linear-gradient(to bottom, #000 calc(100% - 34px), rgba(0, 0, 0, .5) calc(100% - 15px), transparent 100%);
  -webkit-mask-image: var(--fade); mask-image: var(--fade);
}
