/* ============================================================
   Skydex — app.css : coquille SPA, navigation, vues.
   Typo (convention actuelle des maquettes) :
   Barlow = cartes / viseur / hangar · Inter = paywall / profil.
   ============================================================ */

* { box-sizing: border-box; margin: 0; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body {
  background: #000; color: var(--txt);
  font-family: 'Barlow', sans-serif;
  display: flex; justify-content: center;
  -webkit-font-smoothing: antialiased;
}
/* le "téléphone" : plein écran mobile, cadré 430px sur desktop */
#app { position: relative; width: 100%; max-width: 430px; height: 100dvh; overflow: hidden; background: var(--bg-1); }

.hidden { display: none !important; }

.view { position: absolute; inset: 0; overflow-y: auto; padding-bottom: 96px; }
#view-scan { overflow: hidden; padding-bottom: 0; }

/* ⚠️ ZONE SÛRE DU HAUT EN NATIF — RIEN À FAIRE ICI, ET C'EST VOULU.
   Le décalage sous la barre d'état / Dynamic Island est obtenu par
   `StatusBar.setOverlaysWebView({ overlay: false })` dans le bloc Capacitor de
   config.js : iOS place lui-même la WebView sous la barre, en tenant compte du
   modèle (encoche, Dynamic Island, iPhone SE).
   ⚠️ NE PAS « compléter » avec un `padding-top: env(safe-area-inset-top)` : sans
   `viewport-fit=cover` dans le HTML, les `env()` valent zéro (essayé, mesuré,
   sans effet) — et AVEC le plugin, un padding s'ajouterait par-dessus le
   décalage système et creuserait un vide en haut de chaque écran.
   La classe `html.natif` reste posée par config.js et sert de crochet pour
   d'éventuelles retouches natives futures. */

/* ============ NAVIGATION (4 onglets, Scan à DROITE et proéminent) ============
   Passée de 3 à 4 onglets le 4 août 2026 : le radar devient une destination.
   ⚠️ Le Scan n'est plus central et ne DÉBORDE PLUS au-dessus de la barre.
   Avec 4 onglets, un bouton central tomberait entre deux ; et un rond qui
   déborde au bord droit de l'écran entrerait en conflit avec l'arrondi de
   l'écran et la zone sûre du bas sur iPhone. Il reste nettement plus gros et
   plus contrasté que les trois autres, donc toujours le geste évident.
   Les onglets se partagent l'espace restant à parts égales (flex: 1), ce qui
   évite la largeur fixe de 84 px qui débordait à 4 sur un écran de 375 px. */
.nav {
  position: absolute; bottom: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 2px;
  background: rgba(10,14,22,.92); backdrop-filter: blur(10px);
  border-top: 1px solid #1c2940; padding: 10px 10px calc(14px + env(safe-area-inset-bottom));
}
.nav .tab { text-align: center; font-size: .56rem; letter-spacing: .06em; text-transform: uppercase;
  color: #56637a; cursor: pointer; flex: 1; min-width: 0; padding-bottom: 2px; }
.nav .tab svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.8; margin-bottom: 2px; }
.nav .tab.on { color: var(--blue-soft); }
/* ⚠️ L'onglet Scan occupe une case de MÊME LARGEUR que les trois autres
   (flex: 1 hérité, aucune exception ici) : les quatre centres sont donc
   équidistants et la barre est régulière. Une première version le mettait en
   `flex: 0 0 auto`, ce qui rétrécissait sa case à 52 px et le collait au bord
   droit — la position paraissait « bizarre » sans qu'on voie pourquoi.
   Le Scan se distingue par la TAILLE de son rond, pas par sa place. */
/* ⚠️ LE ROND DÉBORDE AU-DESSUS DE LA BARRE (~30 %, demande de l'auteur du
   5 août 2026) : la bordure du menu passe DERRIÈRE le cercle, comme un bouton
   d'appareil photo posé sur le bord. Deux effets voulus : le Scan est mis en
   évidence, et la barre perd de la hauteur (le débordement sort le cercle du
   calcul de hauteur du flex, ce sont les trois autres onglets qui la fixent).
   La marge se décompose ainsi : 10px annulent le padding-top de .nav, et 16px
   passent au-dessus de la bordure — 16/52 ≈ 31 % du cercle.
   Le fond du cercle est OPAQUE : c'est lui qui « coupe » visuellement la
   bordure, aucun autre artifice n'est nécessaire. Ceci REVIENT partiellement
   sur la décision de la veille (« ne déborde plus ») : arbitrage esthétique de
   l'auteur, assumé. */
.nav .tab .scanbtn {
  width: 52px; height: 52px; border-radius: 50%; background: #12203a; border: 2px solid #2b62c4;
  display: flex; align-items: center; justify-content: center; margin: -26px auto 2px;
  box-shadow: 0 4px 14px rgba(0,0,0,.45);
}
.nav .tab.on .scanbtn { background: #1a3050; }
.nav .tab .scanbtn svg { width: 26px; height: 26px; margin: 0; stroke: #cfe0f5; }

/* ============ VUE SCAN (viseur) ============ */
#view-scan { background: #0b0f16; }
/* ⚠️⚠️ LE <video> EST MASQUE TANT QU'AUCUN FLUX N'Y EST ATTACHE, et ce n'est
   pas de la cosmetique. Vecu sur emulateur Android le 15 septembre 2026 : un
   <video> sans srcObject est peint par le WebView Android comme un GRAND
   RECTANGLE GRIS avec un triangle « play » noir, qui recouvre tout l'ecran et
   rend le HUD vert illisible. WKWebView, lui, ne peint rien du tout : c'est
   pour ca que le repli « fond sombre » de scan.js paraissait correct sur
   iPhone depuis le debut. Meme code, deux moteurs de rendu, deux resultats.
   ⛔ Ne pas retirer cette regle en la prenant pour un reliquat : c'est elle
   qui fait exister le repli sans camera, sur lequel tout scan.js s'appuie. */
#cam { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  display: none; }
#cam.on { display: block; }
.scrim { position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,.45) 0%, transparent 22%, transparent 68%, rgba(0,0,0,.55) 100%); }

#gate { position: absolute; inset: 0; z-index: 10; background: radial-gradient(ellipse at 50% 35%, #16233a, #070b12 75%);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; padding: 32px; text-align: center; }
#gate .logo { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800; font-size: 2.4rem; letter-spacing: .08em; }
#gate .logo b { color: var(--blue-soft); }
#gate p { opacity: .6; font-size: .9rem; max-width: 300px; line-height: 1.5; }
#activate { appearance: none; border: 0; border-radius: 16px; background: #2b62c4; color: #fff;
  font-size: 1.1rem; font-weight: 600; padding: 16px 34px; cursor: pointer; font-family: inherit; }

.hud { position: absolute; inset: 0; }

/* ⚠️ pointer-events: none EST INDISPENSABLE, ce n'est pas une optimisation.
   .compass est en absolute pleine largeur ET vient APRÈS .topbtns dans le DOM :
   son bloc .deg (le grand « 90° », un div donc pleine largeur malgré son texte
   centré) se peignait par-dessus les deux boutons ronds et avalait les clics.
   Il ne restait qu'un liseré de ~4 px cliquable en bas de chaque bouton, donc
   le radar ET le panneau de debug étaient inatteignables au doigt — bug signalé
   sur téléphone le 4 août 2026, et invisible en test souris précis.
   Rien dans le ruban boussole n'est interactif : il n'affiche que des valeurs. */
.compass { position: absolute; top: 0; left: 0; right: 0; padding: 46px 0 8px; text-align: center;
  pointer-events: none; }
/* ============ FINESSE DU VISEUR — v2 RETENUE (5 août 2026) ============
   Histoire en trois temps, à connaître avant de retoucher : v1 fine mais
   bleutée semi-transparente (langage du radar) → REJETÉE par l'auteur →
   restaurée → puis redemandée avec LE correctif : mêmes tailles fines, traits
   BLANCS ET QUASI OPAQUES, et réticule remonté (44 % → 40 %, il collait le
   bandeau d'indication).
   Sa raison est la bonne : le radar vit sur un fond sombre maîtrisé, la
   semi-transparence y est un outil ; le viseur vit SUR L'IMAGE CAMÉRA —
   nuages clairs, contre-jour — et un trait à 25-50 % y disparaît.
   RÈGLE DU VISEUR : la finesse se joue sur l'ÉPAISSEUR et la TAILLE,
   jamais sur l'OPACITÉ. */
.compass .deg { font-family: 'Barlow Condensed'; font-weight: 600; font-size: 1.25rem;
  line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: .04em;
  color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.6); }
.ribbon { position: relative; height: 22px; margin-top: 6px; overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent); }
.ribbon .tick { position: absolute; top: 0; transform: translateX(-50%); text-align: center; }
.ribbon .tick .bar { width: 1px; height: 5px; background: rgba(255,255,255,.85); margin: 0 auto; }
.ribbon .tick.maj .bar { height: 9px; background: #fff; }
/* demi-tirets des 5° : plus courts et à 50 % — la SEULE opacité tolérée sur le
   viseur, parce que leur discrétion est précisément leur fonction */
.ribbon .tick.min .bar { height: 3px; background: rgba(255,255,255,.5); }
.ribbon .tick .lab { font-family: 'Barlow'; font-size: .56rem; letter-spacing: .06em;
  color: rgba(255,255,255,.8); margin-top: 2px; text-shadow: 0 1px 3px rgba(0,0,0,.6); }
.ribbon .center { position: absolute; left: 50%; top: 0; width: 1.5px; height: 12px;
  background: var(--blue-soft); transform: translateX(-50%);
  box-shadow: 0 0 6px rgba(111,168,255,.6); }

/* top 40 % et non 44 : en bas de sa course, le réticule collait le bandeau
   d'indication (« 2 candidats — choisis ») — signalé par l'auteur. Le centre
   remonte, l'air revient. L'élévation (.elev) est alignée sur ce même 40 %. */
.reticle { position: absolute; left: 50%; top: 40%; transform: translate(-50%,-50%); width: 220px; height: 220px; pointer-events: none; }
/* fin (1,5 px) mais quasi opaque : voir la règle en tête de section */
.corner { position: absolute; width: 26px; height: 26px; border: 1.5px solid rgba(255,255,255,.92); }
.corner.tl { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.corner.tr { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.corner.bl { bottom: 0; left: 0; border-right: 0; border-top: 0; }
.corner.br { bottom: 0; right: 0; border-left: 0; border-top: 0; }
.cross { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 26px; height: 26px; }
.cross::before, .cross::after { content: ''; position: absolute; background: rgba(255,255,255,.9); }
.cross::before { left: 50%; top: 0; width: 1px; height: 100%; transform: translateX(-50%); }
.cross::after { top: 50%; left: 0; height: 1px; width: 100%; transform: translateY(-50%); }
.reticle.locked .corner { border-color: var(--green-check); }
.reticle.locked .cross::before, .reticle.locked .cross::after { background: var(--green-check); }

/* ancré au bord droit, pas à distance fixe du centre : sinon le libellé
   déborde de l'écran sur les téléphones plus étroits que 430 px */
/* alignée sur le CENTRE du réticule : si l'un remonte, l'autre suit */
.elev { position: absolute; right: 16px; top: 40%; transform: translateY(-50%); text-align: right; }
.elev .v { font-family: 'Barlow Condensed'; font-weight: 600; font-size: 1.05rem; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
  color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.6); }
.elev .l { font-family: 'Barlow'; font-size: .52rem; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.65); text-shadow: 0 1px 3px rgba(0,0,0,.6); }

.bottom { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 20px 100px; }
.detect { display: flex; align-items: center; gap: 12px; background: rgba(10,16,24,.6); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.1); border-radius: 16px; padding: 12px 14px; margin-bottom: 18px; }
.radar { flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,.25); position: relative; overflow: hidden; }
.radar .sweep { position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(111,168,255,.5), transparent 70%); animation: spin 2s linear infinite; }
.radar .dot { position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; border-radius: 50%; background: #566; transform: translate(-50%,-50%); }
.detect.on .radar { border-color: var(--green-check); }
.detect.on .radar .sweep { background: conic-gradient(from 0deg, rgba(74,222,128,.6), transparent 70%); }
.detect.on .radar .dot { background: var(--green-check); box-shadow: 0 0 8px var(--green-check); }
@keyframes spin { to { transform: rotate(360deg); } }
.detect .txt { flex: 1; }
.detect .txt .t { font-weight: 600; font-size: .95rem; }
.detect .txt .s { font-size: .72rem; opacity: .6; }
.detect .chev { flex: 0 0 auto; width: 18px; height: 18px; stroke: #fff; fill: none; stroke-width: 2; opacity: .5; transition: transform .2s; }

.candlist { max-height: 0; overflow: hidden; transition: max-height .25s ease; }
.sheet.open .candlist { max-height: 340px; overflow-y: auto; }
.sheet.open .chev { transform: rotate(180deg); }
.candhead { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; opacity: .55; padding: 12px 4px 6px; }
.cand { display: flex; align-items: center; gap: 12px; padding: 10px 12px; margin-bottom: 8px;
  background: rgba(10,16,24,.6); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,.1); border-radius: 14px; cursor: pointer; }
.cand:active { background: rgba(20,30,44,.8); }
.cand.top { border-color: var(--green-check); }
.cand .offset { flex: 0 0 46px; width: 46px; height: 46px; border-radius: 10px; border: 1px solid rgba(255,255,255,.2); position: relative; }
.cand .offset::before, .cand .offset::after { content: ''; position: absolute; background: rgba(255,255,255,.2); }
.cand .offset::before { left: 50%; top: 6px; bottom: 6px; width: 1px; transform: translateX(-50%); }
.cand .offset::after { top: 50%; left: 6px; right: 6px; height: 1px; transform: translateY(-50%); }
.cand .offset .pt { position: absolute; left: 50%; top: 50%; width: 9px; height: 9px; border-radius: 50%; background: var(--blue-soft); transform: translate(-50%,-50%); }
.cand.top .offset .pt { background: var(--green-check); box-shadow: 0 0 6px var(--green-check); }
.cand .info { flex: 1; min-width: 0; }
.cand .nm { font-weight: 600; font-size: .95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cand .mt { font-size: .72rem; opacity: .65; }
.cand .sepv { flex: 0 0 auto; text-align: right; }
.cand .sepv .n { font-family: 'Barlow Condensed'; font-weight: 700; font-size: 1.2rem; line-height: 1; }
.cand .sepv .u { font-size: .55rem; opacity: .5; letter-spacing: .05em; }

.controls { display: flex; align-items: center; justify-content: center; }
.shutter { position: relative; width: 76px; height: 76px; border-radius: 50%; border: 4px solid #fff;
  background: rgba(255,255,255,.15); cursor: pointer; transition: transform .1s, background .1s; }
.shutter:active { transform: scale(.92); }
.shutter.ready { background: #fff; }
.shutter.ready:active { background: #dbe4f0; }

/* ---- retour au déclenchement (4 août 2026) ----
   Le `:active` seul ne suffisait pas : sur mobile il s'annule au moindre
   glissement du doigt, et le déclencheur se confond avec le bouton Scan de la
   barre basse, juste en dessous. Trois signaux qui ne peuvent pas se rater :
   un enfoncement franc, une onde qui se propage, un éclair bref de l'écran. */
.shutter.fire { animation: shFire .34s cubic-bezier(.2,.9,.3,1); }
@keyframes shFire {
  0%   { transform: scale(1); }
  28%  { transform: scale(.86); }
  100% { transform: scale(1); }
}
/* onde circulaire émise depuis le déclencheur */
.shutter::after { content: ''; position: absolute; inset: -4px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,.9); opacity: 0; pointer-events: none; }
.shutter.fire::after { animation: shWave .55s ease-out; }
@keyframes shWave {
  0%   { transform: scale(1);   opacity: .9; }
  100% { transform: scale(2.1); opacity: 0; }
}
/* éclair de capture, par-dessus la caméra mais sous le HUD */
#shutter-flash { position: absolute; inset: 0; background: #fff; opacity: 0;
  pointer-events: none; z-index: 3; }
#shutter-flash.go { animation: shFlash .22s ease-out; }
@keyframes shFlash {
  0%   { opacity: 0; }
  18%  { opacity: .34; }
  100% { opacity: 0; }
}
/* Anneau qui respire quand un avion est verrouillé : distingue clairement le
   déclencheur du bouton de navigation, et dit qu'il est armé. */
.shutter.ready { box-shadow: 0 0 0 0 rgba(55,214,122,.55); animation: shArmed 1.9s ease-in-out infinite; }
@keyframes shArmed {
  0%, 100% { box-shadow: 0 0 0 0 rgba(55,214,122,.5); }
  50%      { box-shadow: 0 0 0 10px rgba(55,214,122,0); }
}
.shutter.fire.ready { animation: shFire .34s cubic-bezier(.2,.9,.3,1); }

.topbtns { position: absolute; top: 46px; left: 16px; right: 16px; display: flex; justify-content: space-between; align-items: center; }
.iconbtn { width: 40px; height: 40px; border-radius: 50%; background: rgba(10,16,24,.55); backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,.12); display: flex; align-items: center; justify-content: center; cursor: pointer; }
.iconbtn svg { width: 18px; height: 18px; stroke: #fff; fill: none; stroke-width: 2; }
.quota-chip { display: flex; align-items: baseline; gap: 5px; background: rgba(10,16,24,.55); backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,.12); border-radius: 999px; padding: 8px 14px; }
.quota-chip .n { font-family: 'Barlow Condensed'; font-weight: 700; font-size: 1.05rem; line-height: 1; }
.quota-chip .l { font-size: .58rem; letter-spacing: .1em; text-transform: uppercase; opacity: .6; }
.quota-chip.low .n { color: #fbbf24; }
.sim-chip { position: absolute; top: 100px; right: 16px; background: rgba(251,191,36,.15); border: 1px solid rgba(251,191,36,.5);
  color: #fbbf24; font-size: .6rem; letter-spacing: .18em; padding: 4px 10px; border-radius: 999px; }

.debug { position: absolute; top: 96px; left: 16px; background: rgba(6,10,16,.82); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.12); border-radius: 12px; padding: 10px 12px; font-size: .72rem; line-height: 1.7; font-variant-numeric: tabular-nums; z-index: 5; }
.debug b { color: var(--blue-soft); font-weight: 600; }
.debug .cal { color: #fbbf24; }

/* ============ VUE HANGAR ============ */
#view-hangar { background: #0a0e16; color: #e8ecf4; padding: 18px 14px 110px; }
.brand { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 4px; }
.brand .logo { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 1.9rem; letter-spacing: .08em; text-transform: uppercase; color: #efe9da; }
.brand .logo b { background: linear-gradient(120deg, #9dc4f0, #4a7fc9);
  -webkit-background-clip: text; background-clip: text; color: transparent; }
.brand .base { font-size: .58rem; letter-spacing: .3em; color: #56637a; text-transform: uppercase; }

.dexstats { display: flex; gap: 0; margin: 14px 0 16px;
  background: #101828; border: 1px solid #1c2940; border-radius: 16px; padding: 12px 4px; }
.dexstats .stat { flex: 1; text-align: center; padding: 0 6px; }
.dexstats .stat + .stat { border-left: 1px solid #1c2940; }
.dexstats .lbl { font-size: .56rem; letter-spacing: .1em; text-transform: uppercase; color: #6b7a94; margin-bottom: 3px; }
.dexstats .val { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800; font-size: 1.25rem; }
.dexstats .val small { font-size: .8rem; color: #6b7a94; font-weight: 600; }

.filters { display: flex; gap: 8px; margin-bottom: 16px; }
.filters button { appearance: none; border: 1px solid #22304a; background: #101828; color: #9fb0c8;
  font-family: 'Barlow'; font-size: .78rem; font-weight: 600; padding: 8px 14px; border-radius: 999px; cursor: pointer; }
.filters button.active { background: #2b62c4; border-color: #2b62c4; color: #fff; }
.count { font-size: .72rem; color: #56637a; letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }

.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
@media (min-width: 400px) { .grid { grid-template-columns: repeat(3, 1fr); } }

.mini { border-radius: 16px; padding: 5px; cursor: pointer; background: var(--frame); position: relative; transition: transform .12s ease; }
.mini:active { transform: scale(.96); }
.mini-inner { border-radius: 12px; overflow: hidden; background: var(--dark); }
.mini-top { display: flex; justify-content: space-between; align-items: center;
  padding: 6px 8px 4px; font-family: 'Barlow Condensed'; font-style: italic; }
.mini-top .num { font-weight: 800; font-size: .95rem; color: #fff; }
.mini-top .stars { color: #e3b23c; font-size: .7rem; letter-spacing: 1px; }
.mini.tier-commune .mini-top .stars { color: #9db7d4; }
.mini .ph { height: 92px; background: #1a2534; }
.mini .ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mini .ph .hero-ph { height: 100%; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(180deg, #2c4360, #1c2c42); }
.mini .ph .hero-ph svg { width: 40px; height: 40px; fill: rgba(255,255,255,.22); }
.mini .ph .hero-ph span { display: none; }
.mini .nm { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 700;
  font-size: .86rem; text-transform: uppercase; line-height: 1.05; padding: 7px 8px 2px; color: #fff; min-height: 34px; }
.mini .tier-nm { font-size: .58rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); padding: 0 8px 7px; }
.mini .foot { display: flex; border-top: 1px solid rgba(255,255,255,.12); padding: 6px 8px 8px; gap: 6px; }
.mini .foot div { flex: 1; }
.mini .foot .v { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800; font-size: .95rem; }
.mini .foot .l { font-size: .5rem; letter-spacing: .08em; text-transform: uppercase; color: rgba(255,255,255,.45); }

.mini.locked { --frame: #141b28; cursor: default; }
.mini.locked .mini-inner { background: #0e1420; }
.mini.locked .ghost { height: 92px; display: flex; align-items: center; justify-content: center; }
/* ⚠️ TUILE « VERSO » — légendaire capturée mais verrouillée (4 septembre 2026).
   Elle emprunte le fantôme de la case SECRET, mais GARDE le cadre du tier :
   on doit sentir qu'on tient une légendaire sans savoir laquelle. Voir le bloc
   dédié dans views/hangar.js. */
.mini.verso .ghost { height: 92px; display: flex; align-items: center; justify-content: center; }
.mini.verso .ghost img { width: 84px; height: auto; display: block; opacity: .5; }
/* Le libellé qui remplace le NOM de l'avion : sobre, pas alarmant — c'est un
   état à déverrouiller, pas une erreur. */
.mini.verso .nm.lock { color: #8fa5c4; font-style: normal; letter-spacing: .04em; }
/* ⛔ Le cadenas prend la place de la valeur en XP, qui n'est pas acquise. */
.mini.verso .foot .v.lockic { display: flex; align-items: center; justify-content: center; height: 1.1rem; }
.mini.verso .foot .v.lockic svg { width: 13px; height: 13px; fill: none; stroke: #8fa5c4; stroke-width: 2; }
.mini.locked .ghost svg { width: 54px; height: 54px; fill: #1c2637; }
.mini.locked .q { text-align: center; font-family: 'Barlow Condensed'; font-weight: 800; font-size: 1.5rem; color: #2b3a52; }
.mini.locked .secret { text-align: center; font-size: .58rem; letter-spacing: .28em; color: #2b3a52; text-transform: uppercase; padding: 4px 0 12px; }
.mini.locked .mini-top .num { color: #2b3a52; }
.mini.locked .lock svg { width: 13px; height: 13px; stroke: #2b3a52; fill: none; stroke-width: 2; }

/* ---- TAS DE CARTES DU HANGAR (10 août 2026, 2e version) ----
   Un APPAREIL = une carte. Les cartes sont EMPILÉES : on ne voit que celle du
   dessus, les autres dépassent vers le bas-droite. Swipe vers la gauche = la
   carte du dessus part à gauche en tournant. Maquette : cartes/apercu-tas.html.

   ⚠️ REMPLACE LE CARROUSEL LATÉRAL de la 1re version, qui montrait les voisines
   à GAUCHE ET À DROITE (padding 6 %, translateX ±30 %, scale .84). Il suggérait
   une pellicule, pas un paquet. Tout l'historique d'erreurs de peek de cette
   version (92vw, 84vw, flex-basis 84 %, marges négatives de 52 px) est devenu
   SANS OBJET : le peek ne vient plus du padding, il vient de l'empilement.

   ⚠️⚠️ DEUX COUCHES SUPERPOSÉES, ET C'EST LA RAISON D'ÊTRE DE LA FORME.
   Le geste reste du SCROLL NATIF (scroll-snap) — le projet a déjà payé pour
   cette leçon avec le bug de la levée de doute au doigt. Mais un scroller natif
   ne peut pas EMPILER ses enfants. Donc :
     .tas-piste   = scroller TRANSPARENT posé PAR-DESSUS, uniquement des zones de
                    calage vides. Il ne sert qu'à capter le geste. Le navigateur
                    garde inertie, rebond, clavier, trackpad, accessibilité.
     .tas-couches = la couche visible, cartes en position: absolute, qui lit
                    scrollLeft et n'applique que transform/opacity/filter.
   ⚠️ NE JAMAIS mettre de `transition` sur .couche : la position suit le doigt en
   continu via scrollLeft, une transition ajouterait un retard visible. */
/* ⚠️⚠️ MISE À L'ÉCHELLE DE LA CARTE ENTIÈRE (10 août 2026, dernier chantier).
   PROBLÈME RÉSOLU : le contenu de la carte a un PLANCHER EN PIXELS (polices,
   paddings, icônes en valeurs fixes). La boîte rétrécissait avec `vh`, le
   contenu non : sur un écran de 667 px de haut, le contenu débordait de 114 px
   et le bas de la carte était coupé. Mesuré. Le parc visé par les stores va de
   360×640 (Android d'entrée de gamme) à 430×932 (iPhone Pro Max), soit un
   rapport de 1,46 — aucun jeu de valeurs fixes ne tient sur cette plage.
   SOLUTION : la carte est dessinée à UNE taille de référence, 330 × 650, celle
   où son contenu tient exactement (vérifié : à 633 px la ligne de crédits était
   tronquée). Tout le tas est ensuite réduit par un facteur unique calculé en JS.
   ⚠️ Le `scale` est posé sur le CONTENEUR, pas sur chaque carte : les décalages
   du tas et le translateX(-118%) de la carte qui s'en va sont ainsi mis à
   l'échelle avec elle, sans recalcul.
   ⚠️ NE PAS remettre de `vh`/`vw` sur .couche : c'est précisément ce qui faisait
   rétrécir la boîte sans le contenu. La taille de référence doit rester en px.
   ⚠️ Cette approche RENFORCE la règle « une carte, un format » : une seule carte
   existe, dessinée une fois, projetée partout. */
#dex-modal .tas-couches { position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  pointer-events: none;              /* tout le tactile va au scroller */
  perspective: 1200px;
  transform: scale(var(--card-scale, 1)); transform-origin: center center; }
#dex-modal .tas-couches .couche { position: absolute;
  /* ⚠️ GÉOMÉTRIE 330 x 650, DÉJÀ ÉPROUVÉE sur le carrousel — ne pas la rogner.
     Essayé 86vw x 78vh (323 x 633) : 17 px de moins suffisaient à COUPER le bas
     de la carte, la ligne de crédits « Data: airplanes.live… » était tronquée en
     plein milieu. Le contenu de la carte a un plancher, et la photo ne peut pas
     absorber en dessous.
     ⚠️ RÈGLE PRODUIT : la hauteur d'une carte ne varie JAMAIS. Ratio 0,508
     contre 0,506 en plein écran, l'œil ne voit pas la différence. */
  /* TAILLE DE RÉFÉRENCE, en pixels fixes. Le facteur --card-scale fait le reste. */
  width: 330px;
  height: 650px;
  border-radius: 18px; }
/* ⚠️⚠️ `will-change` RETIRÉ le 11 août 2026 — c'était la cause du bug d'affichage
   signalé par un testeur sur Samsung S25+ (ticket #20), et il n'était visible que
   sur un GROS tas.
   Il était posé sur TOUTES les couches, donc sur 28 éléments pour un type vu 28
   fois, alors que 5 seulement sont visibles à un instant donné (mesuré). Or
   `will-change` force la création d'une couche de compositing GPU par élément :
   330 × 650 CSS px sur un écran à DPR 2,6, cela fait 28 textures d'environ
   865 × 1 700 px. Quand la mémoire GPU sature — ce qui arrive sur mobile bien
   avant le bureau — le navigateur laisse des couches NON PEINTES, et une couche
   non peinte s'affiche en GRIS UNIFORME. D'où les grands rectangles gris de la
   capture, et le fait que le défaut empire en défilant.
   ⚠️ Le retirer ne dégrade pas l'animation : les couches portent déjà un
   `transform` explicite, ce qui suffit à les faire promouvoir par le navigateur
   quand il le juge utile. Le gain de `will-change` était théorique ici, son coût
   était mesurable.
   ⚠️ NE PAS le remettre « pour la fluidité », et surtout ne pas le poser/retirer
   dynamiquement dans `syncPile()` : un `will-change` réappliqué à chaque frame
   annule son propre bénéfice et coûte plus cher que son absence. */
#dex-modal .tas-couches .couche .card { width: 100%; height: 100%; }
/* Coquille des couches PROFONDES, jamais rendues : on n'en voit qu'un liseré de
   ~13 px, donc un bloc de la couleur de la carte suffit à faire un bord de tas
   crédible — et ça évite de télécharger des dizaines de photos. */
#dex-modal .tas-couches .couche.vide { background: #f2efe6;
  box-shadow: 0 10px 34px rgba(0,0,0,.6); }

#dex-modal .tas-piste { position: absolute; inset: 0; z-index: 5;
  display: flex; overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; }
#dex-modal .tas-piste::-webkit-scrollbar { display: none; }
#dex-modal .tas-piste .snap { flex: 0 0 100%; min-width: 0; height: 100%;
  scroll-snap-align: center;
  /* ⚠️ `always` et non la valeur par défaut `normal` (11 août 2026) — second
     symptôme du ticket #20 : « le scroll horizontal est très sensible ».
     Avec `normal`, un geste rapide TRAVERSE plusieurs points de snap d'un seul
     mouvement : sur un tas de 28 appareils, un coup de pouce faisait sauter cinq
     ou six cartes, et le joueur perdait sa place dans son propre paquet.
     `always` force l'arrêt sur CHAQUE carte, donc un geste = une carte, ce qui
     est le contrat implicite d'un paquet qu'on feuillette.
     ⚠️ La différence est bien plus marquée sur Chrome Android que sur Safari iOS,
     dont l'inertie de snap est naturellement plus collante — d'où un défaut
     invisible pendant toute la phase de test, faite sur iPhone. */
  scroll-snap-stop: always; }

/* ⚠️ QUATRE PLACEMENTS ESSAYÉS pour le bouton « Fermer », à ne pas refaire :
   en bas dans le flux (42 px, la modale scrollait) ; croix de 40 px en haut
   (invisible pour l'auteur… mais la vraie cause était le CACHE, pas la taille) ;
   bouton libellé en bas avec 62 px réservés (carte à 506 px, photo à 123 px).
   Retenu : bouton LIBELLÉ, flottant en haut à droite. La zone du haut est libre
   (ligne de tête et puces sont SOUS la carte) et il ne coûte rien en hauteur. */
/* ⚠️⚠️ CROIX RONDE ET NON BOUTON TEXTE (13 août 2026, demande de l'auteur).
   Mesuré avant : le bouton « Fermer » faisait 88 × 38 px, dépassait de 12 px hors
   de la carte, et RECOUVRAIT 68 des 119 px du badge — donc le numéro, le tier et
   les étoiles, l'identité même de la carte à collectionner.
   ⚠️⚠️ MAIS IL NE FAUT PAS LE SUPPRIMER, et c'est documenté par un incident :
   le 10 août un bug BLOQUANT est né de sa disparition, parce que la piste de
   cartes occupe toute la surface de la modale et qu'il n'existe donc aucun
   « fond » atteignable. S'y ajoute une règle explicite de `hangar.js` : le tap
   SUR la carte ne ferme PAS, c'est la zone de lecture et un tap y est trop facile
   pendant qu'on fait défiler la pile. La croix est donc le seul moyen SÛR de
   sortir — on la rétrécit, on ne la retire pas.
   ⚠️ `top: 6px` et non 10 : la carte commence à 24 px, la croix mord donc à peine
   son coin au lieu de se poser au milieu du badge. Recouvrement résiduel mesuré
   dans le rapport de session.
   ⚠️ 34 px visuels mais zone tactile élargie par `::before` : sous 44 px une
   cible est difficile à viser au pouce, et ce bouton est le seul moyen de sortir.
   La zone dépasse le rond sans dépasser la carte. */
#dex-modal.avec-pile .close-btn { position: absolute; top: 6px; right: 8px; z-index: 8;
  width: 34px; height: 34px; padding: 0; border-radius: 999px;
  display: grid; place-items: center;
  background: rgba(10,17,29,.9); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.3); color: #e6edf7;
  box-shadow: 0 3px 14px rgba(0,0,0,.55); }
#dex-modal.avec-pile .close-btn svg { width: 15px; height: 15px;
  stroke: currentColor; fill: none; stroke-width: 2.1; stroke-linecap: round; }
/* zone tactile de 44 px centrée sur le rond, invisible */
#dex-modal.avec-pile .close-btn::before { content: ''; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 44px; height: 44px; border-radius: 999px; }
#dex-modal.avec-pile .close-btn:active { transform: scale(.92); }
/* ⚠️ AUCUN padding bas ni latéral : le bandeau du bas doit toucher les bords.
   La marge système (barre de geste iPhone) est portée par le bandeau lui-même,
   pas par la modale — voir le bloc #modalInstances plus bas. */
#dex-modal.avec-pile { padding: 10px 0 0; gap: 6px; }

/* modale carte complète */
/* ⚠️ Depuis le 6 août 2026, la carte doit tenir ENTIÈREMENT à l'écran, sans
   scroll : gaps et padding serrés pour que la carte + la ligne des appareils
   + le bouton tiennent dans 100dvh. Le scroll reste autorisé (overflow:auto)
   comme filet pour les très petits écrans et les titres sur deux lignes. */
.modal { position: absolute; inset: 0; z-index: 40; display: none;
  align-items: center; justify-content: center; flex-direction: column; gap: 8px;
  background: rgba(4,7,12,.9); backdrop-filter: blur(6px); padding: 12px; overflow: auto;
  perspective: 1400px; }
.modal.open { display: flex; }
/* la carte du Hangar s'incline elle aussi (holo.js) */
#modalCard { transform-style: preserve-3d; }
/* ⚠️ INDISPENSABLE AU TAS : ses couches sont en `position: absolute`, donc leur
   parent doit fournir le cadre. Sans ce bloc, #modalCard n'a aucune dimension
   propre et le tas se réduit à zéro. S'applique aussi au cas « une seule carte »
   (type non possédé), qui utilise la même couche .couche. */
/* ⚠️ `flex-shrink: 0` ET UN `min-height` ÉGAL À LA HAUTEUR DE CARTE.
   Mesuré le 10 août au soir : avec `flex: 1 1 auto; min-height: 0`, ajouter de la
   marge système sous le bandeau ne faisait PAS défiler la modale — #modalCard se
   comprimait, et la carte, centrée dans un cadre devenu trop court, débordait
   derrière le bandeau. C'est le rognage déjà signalé, revenu par une autre porte.
   Maintenant le cadre refuse de rétrécir : c'est la MODALE qui défile
   (`overflow: auto`), ce que l'auteur a explicitement accepté. */
#dex-modal #modalCard { position: relative; flex: 1 0 auto;
  align-self: stretch; width: 100%; min-height: calc(650px * var(--card-scale, 1)); }
/* ⚠️ MÊME CROIX RONDE DANS LES DEUX ÉTATS DE LA MODALE. Le mode « pile » n'est
   posé que si le type a des captures (`avec-pile`, cf. hangar.js) ; un type non
   capturé ouvre donc CET état-là. Garder un bouton texte ici et une croix
   là-bas donnerait deux gestes de sortie différents pour la même action.
   ⚠️ Ici la croix reste DANS LE FLUX sous la carte, pas en absolu : il n'y a pas
   de pile à recouvrir, donc rien à dégager — et un bouton dans le flux est plus
   facile à viser. */
.modal .close-btn { appearance: none; border: 1px solid #33415c; background: #101828; color: #ccd6e6;
  border-radius: 999px; width: 38px; height: 38px; padding: 0;
  display: grid; place-items: center; cursor: pointer; flex: 0 0 auto; }
.modal .close-btn svg { width: 16px; height: 16px;
  stroke: currentColor; fill: none; stroke-width: 2.1; stroke-linecap: round; }
.modal .close-btn:active { transform: scale(.92); }

/* ---- bandeau des appareils vus de ce type ----
   La ligne de tête est toujours là (elle porte la flotte mondiale) ;
   seule la rangée de puces disparaît quand on n'a vu qu'un appareil. */
/* ⚠️⚠️ LA RANGÉE DE PUCES PASSE AU-DESSUS DU TAS (10 août 2026, demande de
   l'auteur : « la navigation du bas doit passer au-dessus des cartes »).
   CAUSE DU DÉFAUT : #modalCard avait reçu `position: relative` (indispensable
   au tas, dont les couches sont absolues) mais AUCUN z-index. Or un élément
   POSITIONNÉ se peint au-dessus d'un élément qui ne l'est pas, même s'il le
   précède dans le DOM : les cartes recouvraient donc les puces.
   ⚠️ Ne PAS régler ça en ajoutant de l'espace sous les cartes : le tas dérive
   vers le bas à mesure qu'il s'épaissit, donc l'espace nécessaire dépendrait du
   nombre de cartes. C'est l'ordre d'empilement qui doit trancher, pas la marge.
   Le fond opaque est nécessaire : sans lui, on verrait les bords du tas à
   travers les interstices entre les puces. */
#modalInstances { display: none; width: min(400px, 92vw); flex: 0 0 auto; }
/* ---- LE BANDEAU DU BAS (auteur, 10 août 2026) ----
   La ligne de tête (« 4 appareils vus · 5 captures · 6 250 au registre mondial »)
   ET la rangée de puces partagent UN SEUL fond, qui prend TOUTE LA LARGEUR du
   téléphone et descend jusqu'au bord inférieur.
   ⚠️ ARRONDI EN HAUT SEULEMENT. Pas de rayon à gauche, à droite ni en bas :
   le bandeau touche les trois bords, un arrondi y laisserait des coins vides
   sur le fond de la modale. C'est un bandeau, pas une carte posée au milieu.
   ⚠️ La marge système est passée du padding de la MODALE à celui du BANDEAU :
   sinon le fond noir s'arrêtait au-dessus de la barre de geste et laissait une
   bande de fond de modale sous le bandeau. */
#dex-modal.avec-pile #modalInstances { position: relative; z-index: 6;
  width: 100%; max-width: none; align-self: stretch;
  /* ⚠️ FOND OPAQUE, ET UN LISERÉ EN HAUT. Premier essai à rgba(4,7,12,.94) :
     indiscernable du fond de la modale (rgba(4,7,12,.9)), donc l'arrondi
     demandé ne se voyait pas du tout — un coin arrondi n'existe que s'il
     sépare deux surfaces. Le liseré dessine l'arête ; l'opacité totale évite
     aussi de voir les bords du tas transparaître entre les puces. */
  background: #070b12;
  box-shadow: 0 -1px 0 rgba(255,255,255,.10), 0 -10px 26px rgba(0,0,0,.55);
  border-radius: 16px 16px 0 0;
  /* +10 px au-dessus : passée AU-DESSUS des cartes, la rangée rognait
     visuellement le bas de la carte du dessus. Elle passe devant, il faut donc
     qu'elle ne se superpose pas du tout. */
  /* ⚠️ 26 px, ET UN SCROLL ASSUMÉ (auteur, 10 août au soir : « pourrait être
     encore un peu en dessous, quitte à ce qu'il y ait besoin d'un léger scroll
     vertical pour la voir en entier »). C'est donc VOULU si le bandeau dépasse
     le bas de l'écran : ne pas « corriger » ce débordement en réduisant la
     marge, et ne pas toucher à `overflow: auto` sur .modal, qui le rend
     rattrapable. La carte, elle, reste entièrement visible sans scroll. */
  margin-top: 26px;
  /* +10 px sous les puces, en plus de la marge système : sur un écran sans
     barre de geste, l'inset vaut 0 et le bandeau collait le bord. */
  padding: 9px 12px calc(env(safe-area-inset-bottom, 0px) + 10px); }
#modalInstances.show { display: block; }
#modalInstances .inst-head { font-size: .62rem; letter-spacing: .14em; text-transform: uppercase;
  color: #7d8ca0; margin-bottom: 8px; text-align: center; }
/* la flotte mondiale ressort : c'est l'information de rareté */
#modalInstances .inst-head b { color: var(--blue-soft); font-weight: 700; }
#modalInstances .inst-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px;
  scrollbar-width: none; }
#modalInstances .inst-row::-webkit-scrollbar { display: none; }
#modalInstances .inst { flex: 0 0 auto; appearance: none; cursor: pointer; text-align: left;
  background: #101828; border: 1px solid #22304a; border-radius: 12px; padding: 8px 12px;
  font-family: 'Barlow'; color: #9fb0c8; }
#modalInstances .inst:active { background: #16223a; }
#modalInstances .inst.on { border-color: var(--blue-soft); background: #14243f; color: #fff; }
#modalInstances .inst .reg { display: block; font-family: 'Barlow Condensed'; font-style: italic;
  font-weight: 800; font-size: 1rem; letter-spacing: .02em; }
#modalInstances .inst .meta { display: block; font-size: .62rem; color: #6b7a90; margin-top: 1px; }
#modalInstances .inst.on .meta { color: #9db0c6; }

/* ============ OVERLAY RÉVÉLATION ============ */
#overlay-reveal { position: absolute; inset: 0; z-index: 50; display: none;
  background: radial-gradient(ellipse at 50% 30%, #1a2230, #0b0f16 70%);
  /* ⚠️ gap/padding serrés (6 août 2026) : la révélation empile la carte + la
     pastille de nouveauté + deux boutons, c'est le cas le plus contraint de
     l'app. Tout doit tenir dans 100dvh sans scroll. */
  flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 8px; overflow: auto; }
#overlay-reveal.open { display: flex; }
/* ⚠️⚠️ CROIX DE SORTIE DE LA REVELATION — ajoutee le 4 octobre 2026 apres un
   BLOCAGE COMPLET signale par l'auteur. Scenario : capture d'une legendaire en
   gratuit, la carte reste face cachee, le toucher ouvre le paywall, on revient
   en arriere... et il n'existe AUCUNE sortie. Les deux boutons de `#rv-actions`
   ne recoivent leur classe `show` qu'a l'interieur de `doFlip()`, or `reveal()`
   sort avant pour une carte verrouillee. Il fallait tuer l'application.
   ⚠️ La croix n'apparait QUE sur une carte verrouillee — `.show` est posee par
   reveal.js depuis `currentVerrouillee`, la meme source de verite que le verrou
   lui-meme, donc les deux ne peuvent pas diverger.
   ⛔ Ne pas la rendre permanente sans mesurer ce que ca fait a la revelation :
   sur une carte normale la sortie existe deja (un toucher retourne la carte,
   les deux boutons suivent), et une croix y abimerait le moment fort de l'app.
   ⚠️ 34 px visuels mais zone tactile de 44 px par `::before`, exactement comme
   la croix du Hangar : sous 44 px une cible est difficile a viser au pouce, et
   c'est ici le SEUL moyen de sortir. */
#overlay-reveal .close-btn { position: absolute; z-index: 60; display: none;
  top: calc(8px + env(safe-area-inset-top)); right: 12px;
  width: 34px; height: 34px; padding: 0; border-radius: 999px; appearance: none;
  place-items: center;
  background: rgba(10,17,29,.9); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.3); color: #e6edf7;
  box-shadow: 0 3px 14px rgba(0,0,0,.55); }
#overlay-reveal .close-btn.show { display: grid; }
#overlay-reveal .close-btn svg { width: 15px; height: 15px;
  stroke: currentColor; fill: none; stroke-width: 2.1; stroke-linecap: round; }
#overlay-reveal .close-btn::before { content: ''; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 44px; height: 44px; border-radius: 999px; }
#overlay-reveal .close-btn:active { transform: scale(.92); }
#overlay-reveal .hint { color: #7e8a9b; font-size: .85rem; letter-spacing: .04em; text-align: center; }
/* ⚠️⚠️ MISE À L'ÉCHELLE DE LA CARTE DE RÉVÉLATION (10 août 2026).
   Même problème que dans le Hangar : le contenu de la carte a un plancher en
   pixels, donc sur un écran de 667 px il débordait. Mais ici DEUX contraintes
   de plus, qui interdisent de recopier la solution du Hangar :
     1. `holo.js` pilote le `transform` de l'élément `.tilt` image par image —
        y ajouter un `scale` serait écrasé à la frame suivante. D'où le scale
        posé sur `.stage`, dont personne ne touche le transform.
     2. Ici la MISE EN PAGE doit rétrécir aussi : la scène est dans un flux avec
        l'indice, la pastille de nouveauté et deux boutons. Un `transform` seul
        ne réduit pas la boîte, l'écran aurait continué à défiler. D'où la boîte
        dimensionnée par le facteur ET l'origine en haut à gauche : le visuel et
        la mise en page rétrécissent ensemble.
   ⚠️ La taille de référence est 345 x 682 px, EN PIXELS FIXES : c'est exactement
   ce que rendait `min(400px,92vw)` / `min(700px,84vh)` sur un écran de 375x812,
   donc facteur 1 et zéro changement sur les grands écrans. */
/* ⚠️ LA BOÎTE RESTE À LA TAILLE DE RÉFÉRENCE, la place gagnée est reprise par une
   marge NÉGATIVE. Première version essayée : boîte dimensionnée à REF x facteur
   ET `transform: scale(facteur)` — la réduction s'appliquait DEUX FOIS
   (0,82² = 0,67), la carte sortait étroite et déformée. Repéré au screenshot, pas
   à la mesure : mes contrôles lisaient la boîte, qui était juste. */
#overlay-reveal .stage { perspective: 1400px;
  width: 345px; height: 682px;
  transform: scale(var(--card-scale, 1)); transform-origin: top center;
  margin-bottom: calc(682px * (var(--card-scale, 1) - 1)); }
#overlay-reveal .card { width: 345px; height: 682px; }
/* pas de transition CSS ici : l'inclinaison est lissée image par image
   par holo.js, une transition en plus rendrait le mouvement mou et en retard */
#overlay-reveal .tilt { transform-style: preserve-3d; }
#overlay-reveal .flipper { position: relative; transform-style: preserve-3d;
  transition: transform .85s cubic-bezier(.25,.7,.3,1.05); }
#overlay-reveal .flipper.revealed { transform: rotateY(180deg); }
#overlay-reveal .face { backface-visibility: hidden; -webkit-backface-visibility: hidden; }
#overlay-reveal .face.front { position: relative; transform: rotateY(180deg); }
#overlay-reveal .face.back { position: absolute; inset: 0; z-index: 2; cursor: pointer; }
#overlay-reveal .flipper.revealed .face.back { pointer-events: none; }
/* halo en box-shadow — jamais de filter (aplatit le contexte 3D, casse le flip) */
#overlay-reveal .stage.waiting .cardback { animation: breathe 2.6s ease-in-out infinite; border-radius: 24px; }
@keyframes breathe {
  0%, 100% { box-shadow: 0 0 18px var(--glow); }
  50%      { box-shadow: 0 0 46px var(--glow); }
}
#overlay-reveal .stage.revealed-glow .face.front .card { box-shadow: 0 12px 40px rgba(0,0,0,.5), 0 0 34px var(--glow); }
#overlay-reveal .flipper.shaking { animation: shake .5s ease-in-out; }
@keyframes shake {
  0%,100% { transform: rotate(0); }
  20% { transform: rotate(-2.2deg) translateX(-4px); }
  40% { transform: rotate(2.4deg) translateX(5px); }
  60% { transform: rotate(-2.6deg) translateX(-6px); }
  80% { transform: rotate(2deg) translateX(4px); }
}
#overlay-reveal .burst { position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: radial-gradient(circle at 50% 45%, var(--glow), transparent 55%); z-index: 5; }
#overlay-reveal .burst.go { animation: burstA .7s ease-out; }
@keyframes burstA { 0% { opacity: 0; } 25% { opacity: .85; } 100% { opacity: 0; } }
/* le reflet holographique lui-même est générique : voir .card.holo dans design-system.css */
#overlay-reveal .actions { display: flex; gap: 10px; opacity: 0; pointer-events: none; transition: opacity .4s; }
#overlay-reveal .actions.show { opacity: 1; pointer-events: auto; }
#overlay-reveal .actions button { appearance: none; border: 1px solid #33415c; background: #101828; color: #ccd6e6;
  border-radius: 999px; padding: 9px 18px; font-family: 'Barlow'; font-size: .86rem; cursor: pointer; }
#overlay-reveal .actions button.primary { background: #2b62c4; border-color: #2b62c4; color: #fff; }
#overlay-reveal .mute { position: absolute; top: 14px; right: 14px; z-index: 7; appearance: none;
  border: 1px solid #2c3a4d; background: #16202e; border-radius: 999px; width: 42px; height: 42px;
  font-size: 1.05rem; cursor: pointer; }

/* ============ VUE PROFIL (Inter) ============ */
#view-profile { font-family: 'Inter', -apple-system, sans-serif;
  background: linear-gradient(180deg, #0f1c30 0%, #0a111d 40%, #070c14 100%); padding: 0 0 110px; }
#view-profile .topbar { display: flex; align-items: center; justify-content: space-between; padding: 20px 22px 6px; }
#view-profile .topbar .back { width: 40px; height: 40px; border-radius: 50%; border: 1px solid rgba(255,255,255,.14);
  background: rgba(255,255,255,.03); display: flex; align-items: center; justify-content: center; cursor: pointer; }
#view-profile .topbar .back svg { width: 18px; height: 18px; stroke: #fff; stroke-width: 2; fill: none; }
#view-profile .topbar .title { font-size: 1.05rem; font-weight: 600; }
#view-profile .topbar .spacer { width: 40px; }
#view-profile .content { padding: 10px 22px 30px; }

.pf-head { display: flex; flex-direction: column; align-items: center; margin: 14px 0 6px; }
.pf-avatar { width: 104px; height: 104px; border-radius: 50%; position: relative;
  background: #16233a; border: 2px solid rgba(255,255,255,.12); overflow: hidden; }
.pf-avatar img { width: 100%; height: 100%; object-fit: cover; display: none; }
.pf-avatar.has-img img { display: block; }
.pf-avatar .ph { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.pf-avatar.has-img .ph { display: none; }
.pf-avatar .ph svg { width: 46px; height: 46px; fill: #43597a; }
.pf-avatar .cam { position: absolute; right: 2px; bottom: 2px; width: 32px; height: 32px; border-radius: 50%;
  background: var(--blue-btn); border: 3px solid #0a111d; display: flex; align-items: center; justify-content: center; }
.pf-avatar .cam svg { width: 15px; height: 15px; stroke: #fff; stroke-width: 2; fill: none; }
.pf-name { margin-top: 14px; font-size: 1.3rem; font-weight: 700; }
.pf-plan { margin-top: 8px; font-size: .62rem; font-weight: 700; letter-spacing: .14em; padding: 4px 12px; border-radius: 6px;
  background: rgba(255,255,255,.07); color: var(--txt-2); }
.pf-plan.pro { background: rgba(61,139,253,.15); color: var(--blue); }

.pf-stats { display: flex; margin: 22px 0 18px; background: rgba(255,255,255,.03);
  border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 14px 4px; }
.pf-stats .stat { flex: 1; text-align: center; padding: 0 6px; }
.pf-stats .stat + .stat { border-left: 1px solid rgba(255,255,255,.07); }
.pf-stats .val { font-size: 1.25rem; font-weight: 700; }
.pf-stats .lbl { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--txt-3); margin-top: 3px; }

/* ---- en-tête : emblème + bague de progression + identité ---- */
.pf-hero { display: flex; align-items: center; gap: 18px; padding: 6px 0 20px; }
.pf-hero .badge { position: relative; flex: 0 0 124px; width: 124px; height: 124px; }
.pf-hero .ring { position: absolute; inset: 0; width: 100%; height: 100%; }
.pf-hero .emb { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
/* photo + anneau de rang : le trou des 10 anneaux fait 60 % du canevas,
   donc UN seul réglage suffit pour les dix — la photo ne saute jamais
   d'un rang à l'autre. La photo est dessous, l'anneau par-dessus. */
/* ⚠️ 58 % n'est plus qu'un REPLI : la taille réelle est posée en style inline
   par profile.js, à partir de la table HOLE d'emblem.js, parce qu'elle varie
   d'un rang à l'autre depuis la série de 15. */
.pf-hero .badge .photo { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: 58%; height: 58%; border-radius: 50%; object-fit: cover; z-index: 1; }
.pf-hero .badge .emb-img { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: 90%; height: auto; z-index: 2; }
.pf-hero .badge.withphoto .emb-img { width: 100%; }
/* État verrouillé : on grise l'image livrée, pas besoin d'un fichier de plus.
   Assombrir davantage effacerait la silhouette — il faut qu'on devine encore
   la forme du rang à atteindre, c'est tout l'intérêt de le montrer. */
.emb-img.locked { filter: grayscale(1) brightness(.75) contrast(.75); opacity: .5; }
.pf-next .nextemb { position: relative; }
.pf-next .nextemb .lock { position: absolute; left: 50%; top: 52%; transform: translate(-50%,-50%); }
.pf-next .nextemb .lock svg { width: 17px; height: 17px; stroke: #9db0c6; fill: rgba(10,17,29,.7); stroke-width: 1.8; }
/* ⚠️ `z-index: 3` posé le 11 août 2026, et ce n'est pas cosmétique : l'anneau
   (`.emb-img`) est en `z-index: 2` et le SkyMaster est désormais agrandi de 35 %
   (voir la table SCALE d'emblem.js). Son métal recouvre donc la zone du losange,
   qui n'avait aucun z-index — le NUMÉRO DE RANG disparaissait derrière l'emblème.
   On aurait gagné une impression de puissance en perdant l'information du rang. */
.pf-hero .lvl { position: absolute; left: 50%; bottom: -4px; transform: translateX(-50%); z-index: 3;
  width: 30px; height: 27px; display: flex; align-items: center; justify-content: center;
  background: #16233a; border: 1px solid #2b62c4; color: #cfe0f5;
  font-size: .78rem; font-weight: 700; font-variant-numeric: tabular-nums;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); }
.pf-hero .who { flex: 1; min-width: 0; }
.pf-hero .rk { font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--blue); font-weight: 600; }
.pf-hero .nm { font-size: 1.45rem; font-weight: 700; line-height: 1.1; margin: 2px 0 7px; overflow-wrap: anywhere; }
.pf-hero .fl { margin-top: 9px; font-size: 1.05rem; font-weight: 700; color: var(--blue-soft); letter-spacing: .04em; }
.pf-hero .lv { font-size: .74rem; color: var(--txt-3); }
.pf-hero .pf-plan { display: inline-block; }

/* ---- prochaine étape ---- */
.pf-next { display: flex; align-items: center; gap: 14px;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 16px; padding: 14px 16px; margin-bottom: 12px; }
.pf-next .col { flex: 1; min-width: 0; }
.pf-next .top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.pf-next .lbl { font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--txt-3); }
.pf-next .xp { font-size: .74rem; font-weight: 600; color: var(--blue-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pf-next .nm { font-size: 1.12rem; font-weight: 700; margin: 3px 0 10px; }
.pf-next .bar { height: 6px; border-radius: 999px; background: rgba(255,255,255,.08); overflow: hidden; }
.pf-next .bar i { display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #2f7bf0, #6fa8ff); transition: width .5s ease; }
.pf-next .hint { margin-top: 7px; font-size: .7rem; color: var(--txt-hint); }
.pf-next .nextemb { flex: 0 0 62px; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 14px; padding: 6px 4px; }

/* ---- dernière découverte + planisphère ---- */
.pf-disc { position: relative; overflow: hidden; margin-bottom: 12px;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 13px 16px 15px; }
.pf-disc .lbl { position: relative; z-index: 1; font-size: .6rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--txt-3); margin-bottom: 11px; }
.pf-disc .worldmap { position: absolute; right: -6%; top: 6%; width: 74%; height: 92%; opacity: .5; pointer-events: none; }
.pf-disc .row { position: relative; z-index: 1; display: flex; align-items: center; gap: 13px; }
.pf-disc .thumb { flex: 0 0 66px; height: 66px; border-radius: 12px; overflow: hidden;
  background: #16233a; border: 1px solid rgba(255,255,255,.08); display: flex; align-items: center; justify-content: center; }
.pf-disc .thumb img { width: 100%; height: 100%; object-fit: cover; }
.pf-disc .thumb .ph svg { width: 30px; height: 30px; fill: #3a4c66; }
.pf-disc .txt { flex: 1; min-width: 0; }
.pf-disc .txt .nm { font-size: .96rem; font-weight: 600; }
.pf-disc .txt .ds { font-size: .74rem; color: var(--txt-3); margin-top: 2px; overflow-wrap: anywhere; }

/* ---- 4 tuiles de stats ---- */
.pf-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
.pf-tiles .tile { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 14px; padding: 11px 6px 10px; text-align: center; }
.pf-tiles .ic { display: block; margin: 0 auto 6px; width: 22px; height: 22px; }
.pf-tiles .ic svg { width: 22px; height: 22px; fill: none; stroke-width: 1.7; }
.pf-tiles .ic.b svg { stroke: #6fa8ff; } .pf-tiles .ic.g svg { stroke: #4ade80; }
.pf-tiles .ic.y svg { stroke: #fbbf24; } .pf-tiles .ic.p svg { stroke: #c084fc; }
.pf-tiles .v { font-size: 1.15rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.pf-tiles .v small { font-size: .62rem; font-weight: 600; color: var(--txt-3); }
.pf-tiles .l { font-size: .56rem; letter-spacing: .1em; text-transform: uppercase; color: var(--txt-2); margin-top: 5px; }
.pf-tiles .s { font-size: .54rem; color: var(--txt-hint); margin-top: 2px; }

/* ---- défi du jour ---- */
.pf-daily { display: flex; align-items: center; gap: 13px; margin-bottom: 12px;
  background: rgba(61,139,253,.06); border: 1px solid rgba(61,139,253,.24); border-radius: 16px; padding: 13px 16px; }
.pf-daily.done { background: rgba(74,222,128,.07); border-color: rgba(74,222,128,.3); }
.pf-daily .ic { flex: 0 0 34px; height: 34px; }
.pf-daily .ic svg { width: 34px; height: 34px; fill: none; stroke: var(--blue-soft); stroke-width: 1.5; }
.pf-daily.done .ic svg { stroke: var(--green-check); }
.pf-daily .txt { flex: 1; min-width: 0; }
.pf-daily .lbl { font-size: .58rem; letter-spacing: .14em; text-transform: uppercase; color: var(--txt-3); }
.pf-daily .nm { font-size: 1rem; font-weight: 600; margin-top: 1px; }
.pf-daily .xp { font-size: .72rem; color: var(--blue-soft); font-weight: 600; margin-top: 2px; }
.pf-daily.done .xp { color: var(--green-check); }
.pf-daily .pg { flex: 0 0 auto; min-width: 52px; text-align: center; font-size: .8rem; font-weight: 600;
  color: var(--txt-2); background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.1);
  border-radius: 999px; padding: 6px 10px; font-variant-numeric: tabular-nums; }
.pf-daily.done .pg { color: var(--green-check); border-color: rgba(74,222,128,.4); }


/* ---- hauts faits : filtres, réclamation, animation de gain ---- */
.pf-ach .tabs { display: flex; gap: 6px; margin-bottom: 9px; }
.pf-ach .tb { flex: 1; appearance: none; cursor: pointer; font-family: inherit;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 999px; padding: 7px 4px; color: var(--txt-3); font-size: .7rem; font-weight: 600; }
.pf-ach .tb.on { background: #2b62c4; border-color: #2b62c4; color: #fff; }
.pf-ach .tb b { display: inline-block; background: rgba(255,255,255,.22); border-radius: 999px;
  padding: 0 5px; margin-left: 3px; font-size: .64rem; }
.pf-ach .tb.on b { background: rgba(0,0,0,.28); }

.pf-ach .claim-all { display: block; width: 100%; appearance: none; cursor: pointer; font-family: inherit;
  background: linear-gradient(90deg, #2f7bf0, #6fa8ff); border: 0; color: #fff;
  border-radius: 13px; padding: 12px; font-size: .9rem; font-weight: 600; margin-bottom: 8px; position: relative; }
.pf-ach .more { display: block; width: 100%; appearance: none; cursor: pointer; font-family: inherit;
  background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.1); color: var(--txt-2);
  border-radius: 12px; padding: 10px; font-size: .78rem; font-weight: 600; margin-top: 8px; }

/* une carte réclamable est un bouton : elle doit se sentir cliquable */
.pf-ach .ach.todo { width: 100%; text-align: left; appearance: none; cursor: pointer; font-family: inherit;
  background: rgba(61,139,253,.1); border-color: rgba(61,139,253,.42); position: relative; }
.pf-ach .ach.todo:active { transform: scale(.985); }
.pf-ach .ach .tick.ready { background: linear-gradient(135deg, #2f7bf0, #6fa8ff); color: #fff; font-size: .8rem; }
.pf-ach .ach .xp { flex: 0 0 auto; align-self: center; font-size: .8rem; font-weight: 700; color: var(--blue-soft); }
.pf-ach .ach .xp.got { color: var(--green-check); }
.pf-ach .ach .xp.dim { color: var(--txt-hint); font-weight: 600; }
.pf-ach .ach .body { display: flex; flex-direction: column; }
.pf-ach .ach.empty { justify-content: center; color: var(--txt-hint); font-size: .8rem; padding: 18px 12px; }
/* la carte encaissée s'efface au lieu de sauter */
.pf-ach .ach.leaving { animation: achLeave .6s ease forwards; }
@keyframes achLeave {
  0% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(.9); }
}
/* « +80 XP » qui s'élève */
.xp-pop { position: absolute; right: 14px; top: 50%; pointer-events: none;
  font-size: 1rem; font-weight: 800; color: #6fa8ff; text-shadow: 0 2px 10px rgba(0,0,0,.6);
  animation: xpPop .9s ease-out forwards; }
@keyframes xpPop {
  0%   { opacity: 0; transform: translateY(0) scale(.8); }
  22%  { opacity: 1; transform: translateY(-10px) scale(1.12); }
  100% { opacity: 0; transform: translateY(-40px) scale(1); }
}

/* pastille de réclamation sur l'onglet Profil */
.nav .tab { position: relative; }
.nav .tab .badge-n { position: absolute; top: -2px; right: 18px; min-width: 16px; height: 16px;
  border-radius: 999px; background: #f87171; color: #fff; font-size: .58rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center; padding: 0 4px; box-shadow: 0 0 0 2px #0a111d; }

/* ---- percentile anonyme ---- */
.pf-pct { display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 16px; padding: 12px 16px; }
.pf-pct.high { background: rgba(61,139,253,.07); border-color: rgba(61,139,253,.26); }
.pf-pct .ic svg { width: 22px; height: 22px; fill: none; stroke: #7fa8dd; stroke-width: 1.8; }
.pf-pct.high .ic svg { stroke: var(--blue-soft); }
.pf-pct b { display: block; font-size: .9rem; font-weight: 600; }
.pf-pct small { display: block; font-size: .68rem; color: var(--txt-hint); margin-top: 1px; }

/* ---- ligne des scans ---- */
.pf-scans { display: flex; align-items: center; gap: 11px; margin-bottom: 18px; padding: 0 4px; }
.pf-scans .ic svg { width: 22px; height: 22px; fill: none; stroke: #7fa8dd; stroke-width: 1.8; }
.pf-scans b { display: block; font-size: .86rem; font-weight: 600; }
.pf-scans small { display: block; font-size: .72rem; color: var(--txt-hint); margin-top: 1px; }

/* ---- série de jours ---- */
.pf-streak { display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 16px; padding: 12px 16px; }
.pf-streak .flame { flex: 0 0 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.05); }
.pf-streak .flame svg { width: 18px; height: 18px; fill: #46566c; }
.pf-streak.hot .flame { background: rgba(251,146,60,.14); }
.pf-streak.hot .flame svg { fill: #fb923c; }
.pf-streak .txt { flex: 1; min-width: 0; }
.pf-streak .txt .lbl { font-size: .6rem; letter-spacing: .12em; text-transform: uppercase; color: var(--txt-3); }
.pf-streak .txt .val { font-size: 1.05rem; font-weight: 700; }
.pf-streak.hot .txt .val { color: #fb923c; }
.pf-streak .side { text-align: right; flex: 0 0 auto; max-width: 46%; }
.pf-streak .best { font-size: .7rem; font-weight: 600; color: var(--txt-2); }
.pf-streak .state { font-size: .62rem; color: var(--txt-hint); margin-top: 2px; }

/* ---- hauts faits ---- */
.pf-ach { margin-bottom: 18px; }
.pf-ach .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 9px; padding: 0 2px; }
.pf-ach .head span:first-child { font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--txt-3); }
.pf-ach .cnt { font-size: .74rem; font-weight: 600; color: var(--blue-soft); }
/* `.items` et non `.grid` : `.grid` est déjà la grille du Hangar, la réutiliser
   ici héritait de ses colonnes et tronquait les cartes. Deux colonnes comme la
   maquette ; la pastille de pourcentage remplace la barre de progression, qui
   n'a plus la place. */
.pf-ach .items { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.pf-ach .items .pg { display: none; }
.pf-ach .items .ach { padding: 10px 11px; gap: 9px; }
.pf-ach .items .tick { flex: 0 0 26px; height: 26px; font-size: .54rem; }
.pf-ach .items .ach.done .tick { font-size: .8rem; }
.pf-ach .items .nm { font-size: .8rem; line-height: 1.15; }
.pf-ach .items .ds { font-size: .62rem; line-height: 1.25; margin-top: 2px; }
.pf-ach .ach { display: flex; gap: 11px; align-items: flex-start;
  background: rgba(255,255,255,.025); border: 1px solid rgba(255,255,255,.07);
  border-radius: 13px; padding: 11px 13px; }
.pf-ach .ach.done { background: rgba(61,139,253,.07); border-color: rgba(61,139,253,.3); }
.pf-ach .tick { flex: 0 0 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.06); color: var(--txt-hint); font-size: .58rem; font-weight: 700; }
.pf-ach .ach.done .tick { background: var(--blue-btn); color: #fff; font-size: .9rem; }
.pf-ach .body { flex: 1; min-width: 0; }
.pf-ach .nm { font-size: .88rem; font-weight: 600; }
.pf-ach .ach:not(.done) .nm { color: var(--txt-2); }
.pf-ach .ds { font-size: .68rem; color: var(--txt-hint); margin-top: 1px; }
.pf-ach .pg { height: 3px; border-radius: 999px; background: rgba(255,255,255,.08); margin-top: 7px; overflow: hidden; }
.pf-ach .pg i { display: block; height: 100%; background: #46566c; border-radius: 999px; }

/* ---- compagnies rencontrées (ticket testeur #22, 13 août 2026) ----
   ⚠️ En-tête et bouton « +N autres » reprennent EXACTEMENT les règles de
   `.pf-ach` : même graisse, même casse, même rayon. Ce bloc arrive juste
   au-dessus des hauts faits, donc toute divergence se verrait d'un coup d'œil.
   ⚠️ Liste en UNE colonne, pas deux comme les hauts faits : un nom de compagnie
   est du texte libre venu d'une API (« Brussels Airlines », « DRF Luftrettung »),
   il n'a pas la longueur bornée d'un libellé i18n et se ferait couper sur
   deux colonnes de ~190 px. */
.pf-air { margin-bottom: 18px; }
.pf-air .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 9px; padding: 0 2px; }
.pf-air .head span:first-child { font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--txt-3); }
.pf-air .cnt { font-size: .74rem; font-weight: 600; color: var(--blue-soft); }
.pf-air .items { display: flex; flex-direction: column; gap: 8px; }
.pf-air .air { display: flex; align-items: center; gap: 11px; padding: 10px 12px;
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 13px; }
.pf-air .air .ic { flex: 0 0 26px; height: 26px; display: grid; place-items: center;
  border-radius: 999px; background: rgba(61,139,253,.13); }
.pf-air .air .ic svg { width: 15px; height: 15px; stroke: #7fa8dd; fill: none; stroke-width: 1.7; }
/* min-width:0 est ce qui autorise l'ellipse : sans lui un flex-item refuse de
   descendre sous la largeur de son contenu et pousse le compteur hors du cadre. */
.pf-air .air .body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pf-air .air .nm { font-size: .86rem; font-weight: 600; color: var(--txt-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pf-air .air .ds { font-size: .66rem; color: var(--txt-hint); margin-top: 1px; }
.pf-air .air .n { flex: 0 0 auto; font-size: .72rem; font-weight: 600; color: var(--blue-soft);
  font-variant-numeric: tabular-nums; }
.pf-air .more { display: block; width: 100%; appearance: none; cursor: pointer; font-family: inherit;
  background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.1); color: var(--txt-2);
  border-radius: 12px; padding: 10px; font-size: .78rem; font-weight: 600; margin-top: 8px; }
.pf-air .air-empty { font-size: .78rem; line-height: 1.4; color: var(--txt-hint);
  background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 13px; padding: 14px 13px; }
.pf-air .air-note { font-size: .66rem; line-height: 1.35; color: var(--txt-hint); margin-top: 8px; padding: 0 2px; }

.pf-rows { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 16px; padding: 4px 16px; margin-bottom: 18px; }
.pf-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; }
.pf-row + .pf-row { border-top: 1px solid rgba(255,255,255,.06); }
.pf-row svg { width: 19px; height: 19px; stroke: #7fa8dd; fill: none; stroke-width: 1.7; flex: 0 0 auto; }
.pf-row .lbl { flex: 1; font-size: .8rem; color: var(--txt-3); }
.pf-row .val { font-size: .88rem; font-weight: 500; }

.pf-quota { text-align: center; font-size: .78rem; color: var(--txt-hint); margin-bottom: 20px; }

.pf-actions { display: flex; flex-direction: column; gap: 10px; }
.pf-actions button { width: 100%; border: 0; border-radius: 13px; padding: 15px; font-family: inherit;
  font-size: 1rem; font-weight: 600; color: #fff; cursor: pointer; }
.pf-actions .edit-btn { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.12); }
.pf-actions .pro-btn { background: var(--blue-btn); line-height: 1.25; }
.pf-actions .pro-btn small { display: block; font-size: .7rem; font-weight: 400; opacity: .85; margin-top: 2px; }
/* libelle sur deux lignes dans les rangees de preferences */
.pf-row.two .lbl { display: flex; flex-direction: column; }
.pf-row.two .lbl small { font-size: .66rem; color: var(--txt-hint); margin-top: 1px; }

/* — édition (portée de profile-edit.html) — */
#profile-edit .field,
#onboarding .ob-form .field { margin-bottom: 20px; }
#profile-edit .field label,
#onboarding .ob-form .field label { display: block; font-size: .74rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--txt-3); margin-bottom: 8px; }
#profile-edit .field .wrap,
#onboarding .ob-form .field .wrap { display: flex; align-items: center; gap: 11px; background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.1); border-radius: 13px; padding: 0 14px; }
#profile-edit .field .wrap:focus-within,
#onboarding .ob-form .field .wrap:focus-within { border-color: var(--blue); }
#profile-edit .field .wrap svg,
#onboarding .ob-form .field .wrap svg { width: 19px; height: 19px; stroke: #7fa8dd; fill: none; stroke-width: 1.7; flex: 0 0 auto; }
#profile-edit .field input,
#onboarding .ob-form .field input { flex: 1; background: none; border: 0; outline: none; color: #fff; font-family: inherit;
  font-size: 1rem; padding: 14px 0; }
#profile-edit .field input::placeholder,
#onboarding .ob-form .field input::placeholder { color: #56657d; }
#profile-edit .field .help,
#onboarding .ob-form .field .help { font-size: .72rem; color: var(--txt-hint); margin-top: 6px; }
#profile-edit .avatar-wrap,
#onboarding .ob-form .avatar-wrap { display: flex; flex-direction: column; align-items: center; margin: 18px 0 30px; }
#profile-edit .avatar-wrap .pf-avatar,
#onboarding .ob-form .avatar-wrap .pf-avatar { cursor: pointer; }
#profile-edit .avatar-wrap .hint,
#onboarding .ob-form .avatar-wrap .hint { margin-top: 12px; font-size: .82rem; color: var(--txt-3); }
#profile-edit .save button,
#onboarding .ob-form .save button { width: 100%; border: 0; border-radius: 13px; padding: 15px; font-family: inherit;
  font-size: 1rem; font-weight: 600; color: #fff; background: var(--blue-btn); cursor: pointer; }
#profile-edit .toast,
#onboarding .ob-form .toast { text-align: center; font-size: .84rem; color: var(--green); font-weight: 500; height: 18px;
  margin-top: 8px; opacity: 0; transition: opacity .2s; }
#profile-edit .toast.show,
#onboarding .ob-form .toast.show { opacity: 1; }
/* gain d'XP : on le distingue du simple accuse d'enregistrement */
#profile-edit .toast.xp,
#onboarding .ob-form .toast.xp { color: var(--blue-soft); font-weight: 600; }
#profile-edit input[type=file],
#onboarding .ob-form input[type=file] { display: none; }

/* ============ OVERLAY PAYWALL (Inter) ============ */
#overlay-paywall { position: absolute; inset: 0; z-index: 60; display: none; overflow-y: auto;
  font-family: 'Inter', -apple-system, sans-serif;
  background: linear-gradient(180deg, #14273f 0%, #0d1626 30%, #0a111d 56%, #070c14 100%); }
#overlay-paywall.open { display: block; }
#overlay-paywall .hero { position: absolute; top: 0; left: 0; right: 0; height: 440px; z-index: 0; }
#overlay-paywall .hero img { width: 100%; height: 100%; object-fit: cover; object-position: center 34%; }
#overlay-paywall .hero::after { content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(7,12,20,.15) 0%, rgba(7,12,20,.22) 32%, rgba(9,15,26,.62) 62%, rgba(9,15,26,.92) 84%, #070c14 100%); }
#overlay-paywall .content { position: relative; z-index: 1; padding: 22px 24px 26px; }
#overlay-paywall .close { width: 42px; height: 42px; border-radius: 50%; border: 1px solid rgba(255,255,255,.16);
  background: rgba(10,17,29,.35); backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; cursor: pointer; }
#overlay-paywall .close svg { width: 17px; height: 17px; stroke: #fff; stroke-width: 2; fill: none; }
#overlay-paywall .kicker { display: flex; align-items: center; gap: 8px; margin: 76px 0 18px; }
#overlay-paywall .kicker svg { width: 19px; height: 19px; fill: var(--blue); }
#overlay-paywall .kicker span { color: var(--blue); font-weight: 600; font-size: .74rem; letter-spacing: .16em; }
#overlay-paywall h1 { font-size: 2.05rem; font-weight: 700; line-height: 1.14; letter-spacing: -.015em; }
#overlay-paywall h1 .blue { color: var(--blue); }
#overlay-paywall .sub { margin-top: 16px; color: #a9b8cc; font-size: .92rem; font-weight: 400; line-height: 1.5; max-width: 320px; }
#overlay-paywall .social { display: inline-flex; align-items: center; gap: 11px; margin-top: 24px;
  background: rgba(255,255,255,.035); border: 1px solid rgba(255,255,255,.08); border-radius: 999px; padding: 7px 16px 7px 8px; }
#overlay-paywall .social .av { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #0d1626; margin-right: -10px;
  display: flex; align-items: center; justify-content: center; }
#overlay-paywall .social .av svg { width: 13px; height: 13px; fill: rgba(255,255,255,.7); }
#overlay-paywall .social .txt { font-size: .84rem; font-weight: 400; color: #c7d3e2; margin-left: 5px; }
#overlay-paywall .social .txt b { color: var(--blue); font-weight: 600; }
#overlay-paywall .plans { display: flex; gap: 12px; margin-top: 28px; }
/* ⚠️⚠️ `min-width: 0` EST INDISPENSABLE, ET SON ABSENCE A ETE MESUREE (PRX-02,
   7 octobre 2026). Un element flex vaut `min-width: auto` par defaut : il refuse
   de descendre sous la largeur de son contenu, et `.price` porte `white-space:
   nowrap`. Tant que le prix tenait en « 24,99 € » le defaut existait sans se voir
   (`.plans` debordait deja de 13 px dans son conteneur) ; des que le paywall a
   affiche les vrais prix du magasin, « R$ 119,90 » a pousse la carte annuelle
   45 px HORS DE L'ECRAN sur un 375 px.
   ⛔ Ne pas retirer cette ligne en croyant simplifier : c'est exactement le meme
   piege que le slot du Hangar, documente le 13 aout 2026. */
#overlay-paywall .plan { flex: 1; min-width: 0; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08);
  border-radius: 18px; padding: 18px 16px 16px; position: relative; }
#overlay-paywall .plan.reco { border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue), 0 8px 30px rgba(61,139,253,.15);
  background: rgba(61,139,253,.05); }
#overlay-paywall .popular { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); white-space: nowrap;
  background: var(--blue); font-size: .6rem; font-weight: 600; letter-spacing: .1em; padding: 5px 12px; border-radius: 999px; }
#overlay-paywall .row1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
#overlay-paywall .pname { font-size: .75rem; font-weight: 600; letter-spacing: .05em; color: #c7d3e2; }
#overlay-paywall .chip { font-size: .58rem; font-weight: 600; padding: 4px 8px; border-radius: 6px; }
#overlay-paywall .chip.grey { background: rgba(255,255,255,.07); color: var(--txt-2); }
#overlay-paywall .chip.green { background: rgba(55,214,122,.13); color: var(--green); }
#overlay-paywall .price { margin-top: 13px; font-size: 1.85rem; font-weight: 700; letter-spacing: -.02em; line-height: 1; white-space: nowrap; }
#overlay-paywall .price small { font-size: .82rem; font-weight: 400; color: var(--txt-2); letter-spacing: 0; }
#overlay-paywall .permo { margin-top: 7px; font-size: .8rem; font-weight: 400; color: var(--txt-2); }
#overlay-paywall .btn { display: block; width: 100%; text-align: center; border: 0; border-radius: 12px; padding: 14px;
  margin-top: 18px; font-family: inherit; font-size: .92rem; font-weight: 600; color: #fff; cursor: pointer; }
#overlay-paywall .btn.blue { background: var(--blue-btn); }
#overlay-paywall .btn.green { background: linear-gradient(90deg, #34d17e, #21b6c4); }
#overlay-paywall .foot { text-align: center; margin-top: 11px; font-size: .78rem; font-weight: 400; }
#overlay-paywall .foot.grey { color: #7d8ca0; }
#overlay-paywall .foot.green { color: var(--green); font-weight: 500; }
#overlay-paywall .compare { margin-top: 26px; background: rgba(255,255,255,.025); border: 1px solid rgba(255,255,255,.07);
  border-radius: 18px; padding: 18px 18px 8px; }
#overlay-paywall .chead { display: grid; grid-template-columns: 1fr 58px 58px; align-items: center; padding-bottom: 14px; }
#overlay-paywall .chead .t { font-size: .68rem; font-weight: 600; letter-spacing: .06em; color: var(--txt-3); }
#overlay-paywall .chead .c { text-align: center; font-size: .68rem; font-weight: 600; color: var(--txt-3); }
#overlay-paywall .chead .c.pro span { background: var(--blue-btn); color: #fff; padding: 3px 9px; border-radius: 6px; font-size: .62rem; }
#overlay-paywall .frow { display: grid; grid-template-columns: 1fr 58px 58px; align-items: center; padding: 12px 0;
  border-top: 1px solid rgba(255,255,255,.06); }
#overlay-paywall .frow .lbl { display: flex; align-items: center; gap: 13px; }
#overlay-paywall .frow .ic svg { width: 21px; height: 21px; stroke: #7fa8dd; fill: none; stroke-width: 1.5; }
#overlay-paywall .frow .nm { font-size: .86rem; font-weight: 500; }
#overlay-paywall .frow .ds { font-size: .68rem; font-weight: 400; color: #7d8ca0; margin-top: 2px; }
#overlay-paywall .frow .cell { text-align: center; font-size: .82rem; font-weight: 400; }
#overlay-paywall .frow .cell.free { color: #c7d3e2; }
#overlay-paywall .frow .cell.unl { color: var(--green); font-weight: 600; }
/* ⚠️ « Verrouillées » côté Free (2 septembre 2026). Volontairement SOBRE et non
   rouge : c'est un état à débloquer, pas une erreur. Un rouge d'alerte sur la
   colonne gratuite ferait passer l'offre libre pour un produit cassé. */
#overlay-paywall .frow .cell.lock { color: #8fa5c4; font-size: .82rem; }
#overlay-paywall .frow .cell svg { width: 17px; height: 17px; stroke: var(--green-check); fill: none; stroke-width: 2.2; }
#overlay-paywall .frow .cell .dash { color: #46566c; }
#overlay-paywall .legal { margin-top: 22px; text-align: center; font-size: .74rem; font-weight: 400; color: #7d8ca0; line-height: 1.5; }
#overlay-paywall .legal a { color: var(--blue); text-decoration: none; }
#overlay-paywall .pwtoast { position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%); z-index: 70;
  background: rgba(55,214,122,.14); border: 1px solid rgba(55,214,122,.45); color: var(--green);
  font-size: .8rem; padding: 10px 18px; border-radius: 999px; opacity: 0; transition: opacity .25s; pointer-events: none; white-space: nowrap; }
#overlay-paywall .pwtoast.show { opacity: 1; }
/* ⚠️ « Restaurer mes achats » — EXIGE par Apple. Volontairement DISCRET : c'est
   un recours, pas un appel a l'action, et il ne doit pas concurrencer les deux
   boutons d'abonnement juste au-dessus. */
#overlay-paywall .restore {
  display: block; margin: 14px auto 0; background: none; border: 0;
  color: #7d8ca0; font-family: inherit; font-size: .78rem; text-decoration: underline;
  cursor: pointer; padding: 6px 10px;
}
#overlay-paywall .restore:disabled { opacity: .5; }
/* ⚠️ Les boutons d'achat se GRISENT pendant la transaction (verrou anti-double
   toucher de paywall.js). Sans ce style, rien ne signale que c'est en cours. */
#overlay-paywall .btn:disabled { opacity: .55; cursor: default; }

/* ============ OVERLAY MONTÉE EN GRADE ============
   Célèbre le changement de rang. Réutilise le vocabulaire de la révélation
   (fond radial, burst, halo en box-shadow) pour ne pas inventer un second
   langage visuel. z-index 55 : au-dessus de la révélation (50), sous le
   paywall (60) — mais le guetteur ne déclenche jamais par-dessus un autre
   overlay, donc les deux ne se croisent pas en pratique. */
#overlay-rankup { position: absolute; inset: 0; z-index: 55; display: none;
  background: radial-gradient(ellipse at 50% 34%, #1b2436, #080c13 72%);
  align-items: center; justify-content: center; padding: 22px; overflow: auto; }
#overlay-rankup.open { display: flex; }

#overlay-rankup .ru-burst { position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: radial-gradient(circle at 50% 42%, rgba(111,168,255,.85), transparent 58%); }
#overlay-rankup.swap .ru-burst { animation: ruBurst .85s ease-out; }
@keyframes ruBurst { 0% { opacity: 0; } 22% { opacity: .9; } 100% { opacity: 0; } }

/* perspective : sans elle, l'inclinaison 3D de l'emblème (holo.js) ne se voit pas */
#overlay-rankup .ru-box { position: relative; display: flex; flex-direction: column;
  align-items: center; text-align: center; gap: 0; perspective: 900px; }

#overlay-rankup .ru-kicker { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: .74rem; letter-spacing: .3em; text-transform: uppercase; color: var(--blue-soft);
  opacity: 0; transform: translateY(6px); animation: ruIn .5s .1s ease-out forwards; }
@keyframes ruIn { to { opacity: 1; transform: none; } }

/* ---- l'emblème : anneau + échange ancien/nouveau ---- */
#overlay-rankup .ru-emb { position: relative; width: 168px; height: 168px; margin: 16px 0 4px;
  display: grid; place-items: center; }
#overlay-rankup .ru-ring { position: absolute; inset: 0; width: 100%; height: 100%; }
#overlay-rankup .ru-old,
#overlay-rankup .ru-new { position: absolute; display: grid; place-items: center; z-index: 1; }
#overlay-rankup .ru-old img, #overlay-rankup .ru-new img,
#overlay-rankup .ru-old svg, #overlay-rankup .ru-new svg { display: block; }
/* ---- dissolution de l'ancien grade, puis forge du nouveau ----
   L'idée vient de la maquette du 4 août : le nouveau grade se FABRIQUE à
   partir de l'ancien, au lieu d'un fondu entre deux images — ça raconte la
   progression au lieu de la constater.

   ⚠️ POURQUOI UN MASQUE ET PAS DE VRAIES PARTICULES : faire éclater un PNG en
   particules impose de lire ses pixels dans un canvas (ou du WebGL) et d'en
   animer des centaines — coûteux, et ça rame sur un vieil iPhone. Ici un
   masque en dégradé balaie le badge de bas en haut : on obtient la lecture
   « ça se dissout / ça se reforge » en CSS pur, sans risque de performance.
   On anime mask-position (interpolé nativement) et non le gradient lui-même,
   dont l'interpolation est inégale d'un navigateur à l'autre. */
#overlay-rankup .ru-old,
#overlay-rankup .ru-new {
  -webkit-mask-size: 100% 300%;  mask-size: 100% 300%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* L'ANCIEN se désintègre en partant du bas. Masque 3× plus haut que le badge :
   à la position 0 0 le badge voit le tiers OPAQUE du dégradé (donc visible),
   à 0 100% il voit le tiers transparent (donc effacé). Désaturé au départ,
   comme les rangs verrouillés du Profil : c'est ce qu'on quitte. */
#overlay-rankup .ru-old { filter: grayscale(1) brightness(.75) contrast(.75); opacity: .9;
  -webkit-mask-image: linear-gradient(to top, transparent 46%, #000 54%);
          mask-image: linear-gradient(to top, transparent 46%, #000 54%);
  -webkit-mask-position: 0 0; mask-position: 0 0;
  transition: -webkit-mask-position .5s ease-in, mask-position .5s ease-in, opacity .5s ease-in; }
#overlay-rankup.dissolve .ru-old { -webkit-mask-position: 0 100%; mask-position: 0 100%; opacity: .5; }

/* LE NOUVEAU se forge dans l'autre sens : dégradé inversé, donc à 0 0 il est
   masqué et à 0 100% il est complet — et il se remplit du bas vers le haut. */
#overlay-rankup .ru-new {
  -webkit-mask-image: linear-gradient(to bottom, transparent 46%, #000 54%);
          mask-image: linear-gradient(to bottom, transparent 46%, #000 54%);
  -webkit-mask-position: 0 0; mask-position: 0 0; }
#overlay-rankup.forge .ru-new {
  -webkit-mask-position: 0 100%; mask-position: 0 100%;
  transition: -webkit-mask-position .62s cubic-bezier(.3,.7,.2,1), mask-position .62s cubic-bezier(.3,.7,.2,1); }
/* verrouillage : léger à-coup d'échelle, l'équivalent visuel du clic métallique */
#overlay-rankup.swap .ru-new { animation: ruLock .42s cubic-bezier(.2,1.6,.35,1); }
@keyframes ruLock { 0% { transform: scale(1); } 45% { transform: scale(1.07); } 100% { transform: scale(1); } }

/* le front lumineux qui accompagne la forge : une barre qui monte avec le masque */
#overlay-rankup .ru-emb::before { content: ''; position: absolute; left: 8%; right: 8%; height: 22px;
  bottom: -6%; z-index: 2; pointer-events: none; opacity: 0; border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(190,222,255,.95), rgba(111,168,255,.35) 45%, transparent 72%); }
#overlay-rankup.dissolve .ru-emb::before { animation: ruFront 1.3s ease-in-out forwards; }
@keyframes ruFront {
  0%   { opacity: 0; bottom: -6%; }
  18%  { opacity: 1; }
  85%  { opacity: .9; bottom: 96%; }
  100% { opacity: 0; bottom: 104%; }
}
/* halo pulsant sous le nouvel emblème */
#overlay-rankup.swap .ru-emb::after { content: ''; position: absolute; width: 118px; height: 118px;
  border-radius: 50%; z-index: 0; pointer-events: none;
  box-shadow: 0 0 40px 12px rgba(111,168,255,.35); animation: ruGlow 2.4s ease-in-out infinite; }
@keyframes ruGlow {
  0%, 100% { box-shadow: 0 0 26px 6px rgba(111,168,255,.24); }
  50%      { box-shadow: 0 0 52px 16px rgba(111,168,255,.42); }
}

/* ---- les étoiles, sous le badge, allumées une par une ----
   Dessinées à part et non par-dessus l'emblème : les étoiles sont déjà
   gravées dans les PNG livrés, se caler dessus au pixel casserait à la
   première nouvelle planche. */
#overlay-rankup .ru-stars { display: flex; gap: 9px; height: 26px; align-items: center; margin-top: 6px; }
#overlay-rankup .ru-star svg { width: 22px; height: 22px; display: block; }
#overlay-rankup .ru-star svg path { fill: #2c3648; }
#overlay-rankup .ru-star { opacity: .5; transform: scale(.7); transition: opacity .3s, transform .3s; }
#overlay-rankup .ru-star.on { opacity: 1; transform: scale(1); animation: ruStar .5s cubic-bezier(.2,1.6,.35,1); }
#overlay-rankup .ru-star.on svg path { fill: #f3d878; filter: drop-shadow(0 0 6px rgba(243,216,120,.65)); }
@keyframes ruStar { 0% { transform: scale(.6); } 60% { transform: scale(1.45); } 100% { transform: scale(1); } }

/* ---- textes ---- */
#overlay-rankup .ru-lvl, #overlay-rankup .ru-name, #overlay-rankup .ru-fl,
#overlay-rankup .ru-next { opacity: 0; transform: translateY(8px); transition: opacity .45s ease, transform .45s ease; }
#overlay-rankup.text .ru-lvl  { opacity: 1; transform: none; transition-delay: 0s; }
#overlay-rankup.text .ru-name { opacity: 1; transform: none; transition-delay: .1s; }
#overlay-rankup.text .ru-fl   { opacity: 1; transform: none; transition-delay: .2s; }
#overlay-rankup.text .ru-next { opacity: 1; transform: none; transition-delay: .3s; }
#overlay-rankup .ru-lvl { margin-top: 14px; font-size: .64rem; letter-spacing: .22em;
  text-transform: uppercase; color: #8fa1b8; }
#overlay-rankup .ru-name { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 2.5rem; line-height: 1.05; color: #fff; margin-top: 4px; }
#overlay-rankup .ru-fl { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 700;
  font-size: .95rem; letter-spacing: .16em; color: var(--blue-soft); margin-top: 2px; }
#overlay-rankup .ru-next { font-size: .78rem; color: #7e8a9b; margin-top: 12px; max-width: 260px; }

#overlay-rankup .ru-btn { margin-top: 22px; appearance: none; border: 0; cursor: pointer;
  background: var(--blue); color: #fff; border-radius: 999px; padding: 12px 34px;
  font-family: 'Barlow'; font-size: .92rem; opacity: 0; pointer-events: none; transition: opacity .4s; }
#overlay-rankup.cta .ru-btn { opacity: 1; pointer-events: auto; }

/* écrans courts : on réduit l'emblème et le titre plutôt que de faire défiler */
@media (max-height: 680px) {
  #overlay-rankup .ru-emb { width: 132px; height: 132px; margin: 10px 0 2px; }
  #overlay-rankup .ru-name { font-size: 2rem; }
  #overlay-rankup .ru-next { display: none; }
}

/* ---- bandeau de nouveauté à la révélation (4 août 2026) ----
   Rend visible le barème d'XP : un nouveau type paie plein tarif, un nouvel
   appareil du même type une fraction, un doublon presque rien. Affiché APRÈS le
   flip seulement — le viseur ne dévoile rien avant la capture. */
#rv-novelty { display: none; align-items: center; gap: 10px; padding: 7px 16px;
  border-radius: 999px; font-family: 'Barlow'; font-size: .8rem; letter-spacing: .02em;
  opacity: 0; transform: translateY(6px); transition: opacity .4s ease, transform .4s ease; }
#rv-novelty[class] { display: inline-flex; }
#rv-novelty.show { opacity: 1; transform: none; }
#rv-novelty .lbl { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  text-transform: uppercase; letter-spacing: .1em; }
#rv-novelty .xp { font-weight: 600; opacity: .9; }
/* nouveau type : l'événement majeur, on le marque en vert franc */
#rv-novelty.nv-newType { background: rgba(55,214,122,.16); border: 1px solid rgba(55,214,122,.5); color: #6ee79c; }
/* nouvel appareil : la collection par immatriculation, en bleu */
#rv-novelty.nv-newAircraft { background: rgba(47,123,240,.16); border: 1px solid rgba(111,168,255,.45); color: #8fbaff; }
/* déjà vu : discret, sans culpabiliser — ça rapporte quand même un peu */
#rv-novelty.nv-repeat { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14); color: #9db0c6; }

/* ============ ONBOARDING — écran de bienvenue (4 août 2026) ============
   Fond en image, texte en HTML par-dessus : le lettrage doit pouvoir être
   traduit et s'adapter à toutes les hauteurs d'écran, ce qu'une maquette
   aplatie en une seule image interdirait. */
#onboarding { position: absolute; inset: 0; z-index: 90; display: flex;
  background: #06111f; overflow: hidden; }
#onboarding.hidden { display: none; }
#onboarding.closing { animation: obOut .42s ease forwards; }
@keyframes obOut { to { opacity: 0; transform: scale(1.04); } }

#onboarding .ob-bg { position: absolute; inset: 0;
  background: url('../../assets/brand/onboarding-bg.webp') center bottom / cover no-repeat; }
/* voile haut : l'image générée n'est pas assez sombre en haut pour garantir la
   lisibilité du wordmark sur tous les écrans */
#onboarding .ob-scrim { position: absolute; inset: 0; background:
  linear-gradient(180deg, rgba(4,12,24,.82) 0%, rgba(4,12,24,.35) 34%, rgba(4,12,24,0) 52%,
                  rgba(4,12,24,.55) 84%, rgba(4,12,24,.9) 100%); }

#onboarding .ob-content { position: relative; z-index: 2; flex: 1; display: flex;
  flex-direction: column; align-items: center; padding: 8vh 26px calc(26px + env(safe-area-inset-bottom)); }
#onboarding .ob-badge { width: min(260px, 68vw); height: auto; display: block;
  filter: drop-shadow(0 10px 26px rgba(0,0,0,.55)); }
#onboarding .ob-wordmark { width: min(320px, 78vw); height: auto; display: block; margin-top: 18px; }
#onboarding .ob-baseline { margin-top: 10px; font-family: 'Barlow Condensed'; font-weight: 700;
  font-size: .82rem; letter-spacing: .3em; text-transform: uppercase; color: #9dc4f0;
  text-align: center; }
#onboarding .ob-spacer { flex: 1; min-height: 24px; }

/* choix de la langue : deux boutons côte à côte, pas un menu déroulant —
   deux options se montrent, elles ne se déplient pas. Chaque langue est
   nommée DANS sa langue, seule façon d'être compris avant d'avoir choisi. */
#onboarding .ob-lang { display: flex; gap: 10px; margin-bottom: 18px; }
#onboarding .ob-langbtn { appearance: none; cursor: pointer; display: flex; align-items: center;
  gap: 8px; padding: 10px 18px; border-radius: 999px; font-family: 'Barlow'; font-size: .9rem;
  color: #cfe0f5; background: rgba(8,18,32,.55); border: 1px solid rgba(255,255,255,.16);
  backdrop-filter: blur(8px); transition: border-color .2s, background .2s, color .2s; }
#onboarding .ob-langbtn .fl { font-size: 1.15rem; line-height: 1; }
#onboarding .ob-langbtn.on { color: #fff; border-color: var(--blue-soft);
  background: rgba(47,123,240,.28); }

#onboarding .ob-cta { appearance: none; border: 0; cursor: pointer; width: 100%;
  max-width: 420px; padding: 17px 24px; border-radius: 16px;
  background: linear-gradient(180deg, #4f97ff, #2f7bf0); color: #fff;
  font-family: 'Barlow Condensed'; font-weight: 700; font-size: 1.18rem;
  letter-spacing: .12em; text-transform: uppercase;
  box-shadow: 0 10px 30px rgba(47,123,240,.45); }
#onboarding .ob-cta:active { transform: scale(.985); }

/* écrans courts : on réduit l'emblème plutôt que de faire défiler */
@media (max-height: 700px) {
  #onboarding .ob-content { padding-top: 5vh; }
  #onboarding .ob-badge { width: min(200px, 58vw); height: auto; }
  #onboarding .ob-wordmark { width: min(260px, 70vw); margin-top: 12px; }
}

/* ---- onboarding : gestion des étapes ----
   Une seule étape visible à la fois, et le fond CHANGE d'une étape à l'autre :
   c'est ce qui donne la sensation d'avancer sans barre de progression. */
#onboarding .ob-content { display: none; }
#onboarding[data-step="1"] .ob-content[data-for="1"],
#onboarding[data-step="2"] .ob-content[data-for="2"],
#onboarding[data-step="3"] .ob-content[data-for="3"],
#onboarding[data-step="4"] .ob-content[data-for="4"] { display: flex; animation: obIn .45s ease both; }
@keyframes obIn { from { opacity: 0; transform: translateY(10px); } }
#onboarding[data-step="2"] .ob-bg {
  background-image: url('../../assets/brand/onboarding-bg-2.webp'); }

/* ---- étape 2 : les autorisations ---- */
#onboarding .ob-perm { justify-content: flex-start; }
#onboarding .ob-head { text-align: center; margin-bottom: 26px; }
#onboarding .ob-title { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 2rem; color: #fff; letter-spacing: .02em; }
#onboarding .ob-sub { margin-top: 8px; font-family: 'Barlow'; font-size: .92rem;
  color: #b9cde6; max-width: 300px; margin-inline: auto; line-height: 1.45; }

#onboarding .ob-perms { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: 420px; }
#onboarding .ob-perm-row { display: flex; align-items: flex-start; gap: 13px;
  padding: 14px 16px; border-radius: 16px; background: rgba(8,18,32,.62);
  border: 1px solid rgba(255,255,255,.12); backdrop-filter: blur(8px); }
#onboarding .ob-perm-row .ic { flex: 0 0 34px; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(47,123,240,.2); border: 1px solid rgba(111,168,255,.35); }
#onboarding .ob-perm-row .ic svg { width: 18px; height: 18px; stroke: var(--blue-soft);
  stroke-width: 1.8; fill: none; stroke-linecap: round; stroke-linejoin: round; }
#onboarding .ob-perm-row .tx { flex: 1; display: flex; flex-direction: column; gap: 3px; }
#onboarding .ob-perm-row .nm { font-family: 'Barlow'; font-weight: 600; font-size: .95rem; color: #fff; }
#onboarding .ob-perm-row .ds { font-family: 'Barlow'; font-size: .8rem; color: #9db0c6; line-height: 1.35; }
/* Indispensable vs recommandé : l'app fonctionne sans caméra (viseur sur fond
   sombre), et le dire augmente l'acceptation au lieu de tout exiger en bloc. */
#onboarding .ob-perm-row .tag { flex: 0 0 auto; align-self: center; font-size: .6rem;
  letter-spacing: .1em; text-transform: uppercase; padding: 4px 8px; border-radius: 999px;
  font-family: 'Barlow'; font-weight: 600; }
#onboarding .ob-perm-row .tag.req { color: #6ee79c; background: rgba(55,214,122,.14);
  border: 1px solid rgba(55,214,122,.4); }
#onboarding .ob-perm-row .tag.opt { color: #9db0c6; background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.14); }

#onboarding .ob-privacy { font-family: 'Barlow'; font-size: .74rem; line-height: 1.5;
  color: #8fa1b8; text-align: center; max-width: 340px; margin: 0 auto 16px; }
/* Étape 2 : le bas de l'image (nuages éclairés) rend le texte légal illisible.
   On renforce le voile là, sans toucher au reste de la photo. */
#onboarding[data-step="2"] .ob-scrim { background:
  linear-gradient(180deg, rgba(4,12,24,.86) 0%, rgba(4,12,24,.55) 30%, rgba(4,12,24,.35) 52%,
                  rgba(4,12,24,.82) 78%, rgba(4,12,24,.96) 100%); }
#onboarding .ob-privacy { color: #a9bcd2; text-shadow: 0 1px 6px rgba(0,0,0,.7); }

/* ---- onboarding étapes 3 et 4 : champs de profil ----
   On réutilise .field / .wrap / .avatar-wrap / .pf-avatar de l'écran d'édition
   du Profil : rien n'est redéfini ici sauf ce qui est propre à l'onboarding
   (fond translucide, bouton secondaire), pour que les deux écrans ne puissent
   pas diverger visuellement. */
#onboarding[data-step="3"] .ob-bg,
#onboarding[data-step="4"] .ob-bg {
  background-image: url('../../assets/brand/onboarding-bg-2.webp'); }
#onboarding[data-step="3"] .ob-scrim,
#onboarding[data-step="4"] .ob-scrim { background:
  linear-gradient(180deg, rgba(4,12,24,.9) 0%, rgba(4,12,24,.72) 40%,
                  rgba(4,12,24,.86) 76%, rgba(4,12,24,.97) 100%); }
#onboarding .ob-form { justify-content: flex-start; }
#onboarding .ob-form .field { width: 100%; max-width: 420px; margin-bottom: 16px; }
#onboarding .ob-form .avatar-wrap { margin-bottom: 22px; }
#onboarding .ob-form input[type="file"] { display: none; }
/* les champs de saisie doivent rester lisibles par-dessus la photo */
#onboarding .ob-form .wrap { background: rgba(8,18,32,.72); }

#onboarding .ob-xp { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 700;
  font-size: .84rem; letter-spacing: .12em; text-transform: uppercase; color: #6ee79c;
  margin-bottom: 14px; text-align: center; }
/* bouton secondaire : présent mais discret — passer une étape facultative ne
   doit pas ressembler à un échec */
#onboarding .ob-skip { appearance: none; background: none; border: 0; cursor: pointer;
  margin-top: 12px; padding: 8px 14px; font-family: 'Barlow'; font-size: .86rem;
  color: #9db0c6; text-decoration: underline; text-underline-offset: 3px; }

/* ---- sélecteur de langue dans le Profil ---- */
.pe-langs { display: flex; gap: 10px; }
.pe-lang { flex: 1; appearance: none; cursor: pointer; display: flex; align-items: center;
  justify-content: center; gap: 8px; padding: 12px 10px; border-radius: 14px;
  font-family: 'Inter', sans-serif; font-size: .92rem; color: #9db0c6;
  background: #101828; border: 1px solid #22304a; transition: border-color .2s, color .2s, background .2s; }
.pe-lang .fl { font-size: 1.1rem; line-height: 1; }
.pe-lang.on { color: #fff; border-color: var(--blue-soft); background: #14243f; }

/* ============ LOGO DE MARQUE EN IMAGE (4 août 2026) ============
   Le wordmark était du TEXTE CSS (Barlow Condensed italic) à trois endroits :
   gate du Scan, bandeau du Hangar, dos de carte. Il est désormais le vrai
   lettrage de marque, identique à l'écran d'accueil et à l'icône d'app.
   Les tailles reprennent celles des textes remplacés pour ne rien décaler. */
.logo-img { display: block; height: auto; }
.logo-img.gate  { width: min(268px, 72vw); margin: 0 auto 4px; }   /* remplaçait 2,4rem */
.logo-img.brand { width: 148px; }                                   /* remplaçait 1,45rem */
.logo-img.back  { width: 150px; margin-top: 2px; }                  /* dos de carte, 2rem */

/* Dos de carte : le badge réel remplace le dessin qui l'imitait (cadre + avion
   + étincelle + traînée redessinés en CSS). Même emplacement, même gabarit. */
.cardback-inner .mark-img { width: 150px; height: auto; display: block;
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.45)); }

/* Slot verrouillé du Hangar : la silhouette du badge, très atténuée. Un slot
   vide reste ainsi reconnaissable comme une carte Skydex, au lieu d'un avion
   générique. On garde la teinte sombre d'origine (#1c2637) via un filtre plutôt
   qu'une seconde image. */
.mini.locked .ghost img { width: 84px; height: auto; display: block;
  filter: grayscale(1) brightness(.42) contrast(.85); opacity: .55; }

/* ============ RADAR « LE CIEL MAINTENANT » (4 août 2026) ============
   Sonar PPI : un faisceau tourne, les points s'allument à son passage.
   ⚠️ L'illumination n'est PAS calculée en JavaScript : chaque point porte la
   même animation, décalée par un animation-delay proportionnel à son azimut
   relatif au cap (posé en style inline par radar.js). Faisceau et points
   partagent ainsi la même horloge CSS et restent synchronisés sans une seule
   frame de JS — ce qui compte, car rAF est throttlé en arrière-plan. */
/* Devenu une VUE (onglet) et non plus un overlay : plus de z-index, plus de
   display:none piloté par .open — c'est .view/.hidden du routeur qui décide,
   comme pour les trois autres onglets. On garde le fond en dégradé, qui fait
   l'identité de l'écran. */
#view-radar { background: radial-gradient(ellipse at 50% 38%, #0d1b2c, #05090f 72%);
  padding: 20px 20px 96px; }
#view-radar .rd-box { display: flex; flex-direction: column; align-items: center;
  width: 100%; max-width: 440px; margin: 0 auto; }
#view-radar .rd-head { text-align: center; margin-bottom: 18px; }
#view-radar .rd-title { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 1.75rem; color: #fff; }
#view-radar .rd-sub { font-family: 'Barlow'; font-size: .84rem; color: #8fa1b8; margin-top: 3px; }

/* ============ LE CADRAN, EN DEUX COUCHES (5 août 2026) ============
   Demande de l'auteur : des traits plus subtils, un fond qui n'est pas uni,
   une lueur légère, une gradation fine sur le grand cercle, et les distances
   écrites sur les cercles. Tout en CSS, aucune image — rien à régénérer si la
   palette change.

   ⚠️ ARCHITECTURE : CE QUI TOURNE ET CE QUI NE TOURNE PAS.
     .rd-stage  statique  → taille, fond, lueur. Le cadre du cadran.
     .rd-disc   TOURNE    → faisceau, axes, avions. Eux seuls dépendent du cap.
     .rd-dial   statique  → cercles de distance, étiquettes, point central.
   Un cercle est symétrique : le faire tourner ne change rien à l'écran mais
   coûte une recomposition par image. Une première version plaçait les cercles
   DANS le disque avec une contre-rotation pour garder le reflet fixe : cinq
   couches masquées retransformées 60 fois par seconde, et l'auteur a signalé
   que la rotation n'était « toujours pas hyper fluide » sur son téléphone.
   Maintenant une seule couche bouge. Et les étiquettes restent droites par
   construction, alors que dans le disque elles auraient tourné à l'envers.

   ⚠️ LA TECHNIQUE DES ANNEAUX À DÉGRADÉ : un `border` ne peut pas porter de
   dégradé angulaire. On peint donc un DISQUE entier en `conic-gradient`, puis
   on n'en garde que le bord avec un `mask` en `radial-gradient` qui rend
   transparent tout ce qui est à moins de (rayon − épaisseur). Résultat : un
   anneau fin dont l'intensité varie sur le tour.
   ⚠️ `-webkit-mask` est indispensable : Safari, donc l'iPhone, la cible.
   ⚠️ Dégradés SYMÉTRIQUES et brillants EN HAUT (`from 0deg`) : la lumière vient
   d'au-dessus, comme partout ailleurs. Un essai à `from 200deg` mettait la
   brillance en bas à gauche — l'œil le trouve faux sans savoir le nommer. */

#view-radar .rd-stage { position: relative; width: min(320px, 82vw); aspect-ratio: 1;
  border-radius: 50%;
  /* FOND EN DEUX COUCHES, et non un dégradé centré : une lueur DÉCENTRÉE vers le
     haut, puis l'assombrissement vers le bord. C'est le décentrage qui enlève
     l'effet « plat ». ⚠️ Il est sur la couche STATIQUE, sinon la source de
     lumière tournerait avec le cadran. */
  background:
    radial-gradient(circle at 50% 26%, rgba(104,158,224,.22), transparent 62%),
    radial-gradient(circle at 50% 50%, rgba(24,52,82,.66) 0%, rgba(13,30,49,.8) 52%, rgba(6,14,24,.97) 100%);
  /* LUEUR : deux diffusions externes froides de portées différentes, plus un
     assombrissement interne du bord. Pas de `filter`, plus coûteux, et que la
     doc du projet interdit ailleurs (il aplatit les contextes 3D). */
  box-shadow: 0 0 34px rgba(61,139,253,.16), 0 0 90px rgba(61,139,253,.07),
              inset 0 0 38px rgba(6,14,24,.55); }

/* la couche qui tourne : le haut de l'écran = la direction regardée */
#view-radar .rd-disc { position: absolute; inset: 0; border-radius: 50%; overflow: hidden;
  /* Promotion explicite en couche GPU : c'est le seul élément transformé à
     chaque image, donc autant dire au navigateur de ne pas le re-rastériser.
     À ne PAS étendre au reste du cadran — `will-change` sur des éléments qui
     ne bougent pas consomme de la mémoire vidéo pour rien. */
  will-change: transform;
  /* ⚠️ AUCUNE transition sur le transform. `--cap` est posé à chaque image par
     boucleCap() : une transition de .6 s serait relancée 60 fois par seconde et
     le disque traînerait loin derrière le poignet. Le lissage est fait en JS
     (EASE_CAP), pas en CSS. */
  transform: rotate(var(--cap, 0deg)); }

/* la couche statique des distances, par-dessus le faisceau */
#view-radar .rd-dial { position: absolute; inset: 0; border-radius: 50%;
  pointer-events: none; }

/* CERCLES DE DISTANCE. L'inset est calculé par radar.js depuis la distance, pas
   écrit ici : les cercles et leurs étiquettes viennent du même tableau, donc ils
   ne peuvent pas se contredire. */
#view-radar .rd-dial .ring { position: absolute; border-radius: 50%; border: 0;
  background: conic-gradient(from 0deg,
    rgba(126,180,255,.52), rgba(126,180,255,.26) 22%, rgba(126,180,255,.09) 50%,
    rgba(126,180,255,.26) 78%, rgba(126,180,255,.52));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px)); }

/* LE PLUS GRAND CERCLE (40 km, inset 0) : le plus marqué, avec la gradation la
   plus franche — c'est le bord du cadran, il porte la finition. */
#view-radar .rd-dial .ring.bord {
  background: conic-gradient(from 0deg,
    rgba(150,196,255,.85), rgba(140,186,255,.42) 20%, rgba(120,170,255,.13) 50%,
    rgba(140,186,255,.42) 80%, rgba(150,196,255,.85));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1.5px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1.5px)); }

/* ⚠️ LIMITE DE SCAN (30 km) — d'une AUTRE COULEUR que les autres cercles, avec
   un voile très léger vers l'intérieur : ce qui est dedans est atteignable, ce
   qui est dehors ne l'est pas. Demandé par l'auteur le 4 août 2026, parce que
   rien ne disait au joueur où s'arrête le scannable.
   ⚠️ MAIS CE CERCLE EST UNE APPROXIMATION, ET C'EST DOCUMENTÉ : le vrai critère
   n'est pas la distance, c'est la TAILLE APPARENTE (MIN_ARCMIN). Un 747 de 64 m
   reste capturable à 30 km ; un Learjet de 12 m est écarté dès ~21 km, donc bien
   à l'intérieur du cercle. Le cercle donne le plafond absolu (MAX_DIST_M), et
   c'est l'ÉTAT DE CHAQUE POINT qui porte la vérité par avion — voir .blip.petit.
   ⚠️ Il REPREND UN VRAI BORDER, en tirets : c'est sa signature, et un anneau
   masqué en conic-gradient ne peut pas être pointillé. Il annule donc le fond et
   le masque radial de `.ring`, et utilise un masque ANGULAIRE qui fait respirer
   les tirets sur le tour — même intention, autre moyen. */
#view-radar .rd-dial .ring.limite {
  background: none;
  border: 1.25px dashed rgba(74,222,128,.75);
  box-shadow: inset 0 0 30px rgba(55,214,122,.09), 0 0 16px rgba(55,214,122,.13);
  -webkit-mask: conic-gradient(from 0deg, #000, rgba(0,0,0,.62) 24%, rgba(0,0,0,.4) 50%, rgba(0,0,0,.62) 76%, #000);
          mask: conic-gradient(from 0deg, #000, rgba(0,0,0,.62) 24%, rgba(0,0,0,.4) 50%, rgba(0,0,0,.62) 76%, #000); }

/* ============ COURONNE GRADUÉE (5 août 2026) ============
   Tirets tous les 15° et quatre points cardinaux, sur le bord du plus grand
   cercle. Elle vit DANS le disque, donc elle tourne avec le cap : c'est ce qui
   fait qu'elle indique le nord réel pendant qu'on pivote.

   ⚠️ Le rayon est exprimé en CSS et non mesuré en JS : `min(320px, 82vw)` est la
   taille du cadran, donc `/2` en donne le rayon. Aucune lecture de layout, donc
   aucun risque de valeur périmée au redimensionnement.

   ⚠️ POSITIONNEMENT PAR ORBITE : on tourne à l'azimut voulu, puis on translate
   vers l'extérieur. C'est ce qui évite de calculer un sinus et un cosinus par
   élément, et ça reste une seule propriété `transform` — donc compositable. */
#view-radar .rd-bezel { position: absolute; inset: 0; pointer-events: none;
  --rr: calc(min(320px, 82vw) / 2); }

/* TIRETS — aucune transformation propre au-delà de leur placement : ils tournent
   avec le disque, ce qui est exactement le comportement voulu pour un trait
   radial. Couleur du grand cercle à 20 %, comme demandé. */
#view-radar .rd-bezel .rd-tick { position: absolute; left: 50%; top: 50%;
  width: 1px; height: 4px; margin: -2px 0 0 -.5px;
  background: rgba(150,196,255,.2);
  transform: rotate(calc(var(--az) * 1deg)) translateY(calc(-1 * (var(--rr) - 5px))); }
/* trois longueurs : c'est la hiérarchie qui rend une graduation lisible.
   ⚠️ LA MARGE DU HAUT DOIT VALOIR LA MOITIÉ DE LA HAUTEUR, sinon le centre de
   rotation n'est plus le centre du disque et le tiret se retrouve déplacé d'une
   distance qui dépend de son azimut. Mesuré avant correctif, avec la marge de
   -2px héritée des tirets courts : les 4 tirets majeurs tombaient à 151 px du
   centre au nord, 153 au nord-est, 155 au sud — 4 px d'écart, donc une couronne
   visiblement décentrée là où elle est la plus lisible. Les 16 tirets courts,
   eux, étaient tous exactement à 155 px.
   Les translations sont réglées pour que les trois longueurs se terminent sur le
   MÊME rayon extérieur et poussent vers l'intérieur, comme sur un cadran. */
#view-radar .rd-bezel .rd-tick.mid { height: 6px; margin-top: -3px;
  transform: rotate(calc(var(--az) * 1deg)) translateY(calc(-1 * (var(--rr) - 6px))); }
#view-radar .rd-bezel .rd-tick.maj { height: 8px; margin-top: -4px;
  background: rgba(150,196,255,.32);
  transform: rotate(calc(var(--az) * 1deg)) translateY(calc(-1 * (var(--rr) - 7px))); }

/* POINTS CARDINAUX — petits, gris, discrets (demande de l'auteur).
   ⚠️ La dernière rotation annule DEUX choses : l'orbite (`--az`) et la rotation
   du disque (`--cap`). Sans elle, la lettre part à l'envers dès qu'on se tourne.
   C'est la seule raison pour laquelle ces quatre éléments sont retransformés à
   chaque image ; ne pas la « simplifier ». */
#view-radar .rd-bezel .rd-card { position: absolute; left: 50%; top: 50%;
  width: 16px; height: 16px; margin: -8px 0 0 -8px;
  line-height: 16px; text-align: center;
  font-family: 'Barlow'; font-size: .56rem; font-weight: 600; letter-spacing: .04em;
  color: rgba(160,176,196,.72);
  /* ⚠️ 21px et non 15px : à 15, la lettre touchait le tiret majeur de son propre
     azimut et « —N » se lisait comme un trait d'union. Il faut de l'air entre la
     graduation et la lettre. */
  transform: rotate(calc(var(--az) * 1deg)) translateY(calc(-1 * (var(--rr) - 21px)))
             rotate(calc(-1 * var(--az) * 1deg - var(--cap, 0deg))); }
/* le nord se distingue à peine des trois autres : assez pour le trouver d'un
   coup d'œil, pas assez pour devenir un élément d'interface.
   ⚠️ Classe posée par radar.js, PAS `:first-of-type` — les tirets sont aussi des
   <span>, donc le premier de ce type est un tiret et la règle ne matchait rien. */
#view-radar .rd-bezel .rd-card.nord { color: rgba(200,216,236,.9); }

/* ÉTIQUETTES DE DISTANCE (demande de l'auteur : « la numérotation dans les
   cercles concentriques »). Posées juste sous chaque cercle, décalées de l'axe
   vertical pour ne pas s'asseoir sur la croix. Elles restent droites parce
   qu'elles vivent sur la couche statique. */
#view-radar .rd-dial .rd-lab { position: absolute; left: 50%; margin-left: 7px;
  font-family: 'Barlow'; font-size: .58rem; letter-spacing: .06em;
  color: rgba(150,180,215,.65); font-variant-numeric: tabular-nums;
  /* l'ombre porte n'est pas cosmétique : sans elle, le chiffre posé sur un
     cercle ou sur un axe devient illisible */
  text-shadow: 0 1px 3px rgba(0,0,0,.85), 0 0 6px rgba(0,0,0,.6); }
#view-radar .rd-dial .rd-lab.limite { color: rgba(110,231,156,.9); font-weight: 600; }

#view-radar .rd-disc .rd-cross { position: absolute; }
#view-radar .rd-disc .rd-cross.v { left: 50%; top: 0; bottom: 0; width: 1px;
  background: linear-gradient(to bottom, transparent 2%, rgba(111,168,255,.17) 26%,
    rgba(111,168,255,.17) 74%, transparent 98%); }
#view-radar .rd-disc .rd-cross.h { top: 50%; left: 0; right: 0; height: 1px;
  background: linear-gradient(to right, transparent 2%, rgba(111,168,255,.17) 26%,
    rgba(111,168,255,.17) 74%, transparent 98%); }
#view-radar .rd-dial .me { position: absolute; left: 50%; top: 50%; width: 8px; height: 8px;
  margin: -4px 0 0 -4px; border-radius: 50%; background: #6ee79c;
  box-shadow: 0 0 10px rgba(110,231,156,.9); }
#view-radar .rd-disc .beam { position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(111,168,255,.42), rgba(111,168,255,.06) 22%, transparent 34%);
  animation: rdSweep 4s linear infinite; }
@keyframes rdSweep { to { transform: rotate(360deg); } }

/* les points : invisibles, puis flash au passage du faisceau */
#view-radar .blips { position: absolute; inset: 0; }
#view-radar .blip { position: absolute; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 50%; opacity: 0; animation: rdPing 4s linear infinite; }
@keyframes rdPing {
  0%   { opacity: 1; transform: scale(1.9); }
  10%  { opacity: .95; transform: scale(1); }
  70%  { opacity: .28; }
  100% { opacity: .12; }
}
#view-radar .blip.ok      { background: #6fa8ff; box-shadow: 0 0 10px rgba(111,168,255,.95); }
/* une NOUVEAUTÉ se distingue au premier regard : c'est ce qui fait sortir */
#view-radar .blip.neuf    { background: #6ee79c; box-shadow: 0 0 14px rgba(110,231,156,1);
  width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; }
#view-radar .blip.inconnu { background: #8fa1b8; box-shadow: 0 0 8px rgba(143,161,184,.7); }
#view-radar .blip.loin    { background: #46566b; box-shadow: none; }
/* Dans le cercle de 30 km mais TROP PETIT pour être vu à l'œil (taille
   apparente sous MIN_ARCMIN) : le déclencheur le refuserait. Creux plutôt que
   plein — il est là, mais on ne peut pas l'avoir. C'est ce qui rattrape
   l'approximation du cercle de limite. */
#view-radar .blip.petit   { background: transparent; box-shadow: none;
  border: 1.5px solid rgba(143,161,184,.75); }

/* légende du disque : sans elle, les couleurs des points sont une devinette */
#view-radar .rd-legend { display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px 14px; margin-top: 14px; font-family: 'Barlow'; font-size: .66rem; color: #8fa1b8; }
#view-radar .rd-legend span { display: inline-flex; align-items: center; gap: 5px; }
#view-radar .rd-legend i { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
#view-radar .rd-legend i.neuf { background: #6ee79c; }
#view-radar .rd-legend i.ok { background: #6fa8ff; }
/* La légende n'a plus que 3 entrées (neuf / ok / loin) : « pas encore encodé »
   a été retiré comme jargon interne, et ces points adoptent le gris de « loin ».
   Les puces « petit » et « inconnu » n'ont donc plus lieu d'être. */
#view-radar .rd-legend i.loin { background: #46566b; }

#view-radar .rd-stats { display: flex; align-items: baseline; gap: 12px; margin: 18px 0 4px; }
#view-radar .rd-count { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 1.5rem; color: #fff; }
#view-radar .rd-new { font-family: 'Barlow'; font-size: .84rem; color: #8fa1b8; }
#view-radar .rd-new.has { color: #6ee79c; font-weight: 600; }

#view-radar .rd-list { width: 100%; margin-top: 12px; display: flex; flex-direction: column; gap: 7px; }
#view-radar .rd-row { display: flex; align-items: center; gap: 10px; padding: 10px 13px;
  border-radius: 12px; background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.09);
  font-family: 'Barlow'; font-size: .88rem; color: #cfe0f5; }
#view-radar .rd-row.neuf { border-color: rgba(55,214,122,.42); background: rgba(55,214,122,.09); }
/* Lignes SANS le nom de l'avion depuis le 4 août 2026 (décision de l'auteur) :
   le rang à gauche, la distance à droite. Le nom en disait trop — le viseur ne
   dévoile pas l'appareil avant la capture, c'est une règle du projet, et le
   radar ne doit pas la contourner. Le rang suffit à faire sortir de chez soi. */
#view-radar .rd-row .tier { font-size: .68rem; letter-spacing: .1em; padding: 4px 9px;
  border-radius: 999px; background: rgba(255,255,255,.07); flex: 0 0 auto; }
#view-radar .rd-row .grow { flex: 1; min-width: 0; }
#view-radar .rd-row .tier.rare { color: #5fd0a4; } #view-radar .rd-row .tier.epique { color: #f096c8; }
#view-radar .rd-row .tier.legendaire { color: #d9b64a; } #view-radar .rd-row .tier.commune { color: #9db7d4; }
#view-radar .rd-row .km { color: #8fa1b8; font-variant-numeric: tabular-nums; }
#view-radar .rd-empty { text-align: center; font-family: 'Barlow'; font-size: .86rem;
  color: #8fa1b8; padding: 14px; }

/* capteurs pas démarrés : on explique au lieu de montrer un disque vide */
#view-radar .rd-gate { margin-top: 22px; text-align: center; }
#view-radar .rd-gate.hidden { display: none; }
#view-radar .rd-gate-t { font-family: 'Barlow'; font-size: .88rem; color: #9db0c6;
  line-height: 1.45; margin-bottom: 14px; }
#view-radar .rd-gate button { appearance: none; cursor: pointer; border: 0;
  background: var(--blue); color: #fff; border-radius: 999px; padding: 13px 28px;
  font-family: 'Barlow Condensed'; font-weight: 700; font-size: 1rem;
  letter-spacing: .08em; text-transform: uppercase; }

/* ============ VOLET D'ALERTE RARETÉ (5 août 2026) ============
   Prévient qu'un épique ou un légendaire vient d'entrer dans le rayon du radar.
   Glisse depuis le haut, se referme seul, et reste atteignable depuis n'importe
   quel onglet — c'est pour ça qu'il vit hors des vues.
   ⚠️ z-index sous les overlays de révélation et de montée en grade : une carte
   qu'on vient de retourner a la priorité (alerte.js s'en abstient aussi côté JS,
   les deux gardes sont volontaires). */
#alerte { position: absolute; top: 0; left: 0; right: 0; z-index: 40;
  padding: calc(10px + env(safe-area-inset-top)) 12px 0;
  pointer-events: none;
  transform: translateY(-130%); transition: transform .42s cubic-bezier(.2,.9,.25,1); }
#alerte.on { transform: translateY(0); pointer-events: auto; }
#alerte .al-box { display: flex; align-items: center; gap: 11px;
  background: rgba(13,22,38,.94); backdrop-filter: blur(10px);
  border: 1px solid var(--frame-solid, rgba(255,255,255,.14));
  border-left: 3px solid var(--accent, #f096c8);
  border-radius: 14px; padding: 11px 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,.55); }
/* l'étoile reprend la couleur du tier : on reconnaît la rareté avant de lire */
#alerte .al-star { font-size: .82rem; letter-spacing: -1px; color: var(--accent, #f096c8);
  flex: 0 0 auto; }
#alerte .al-tx { flex: 1; min-width: 0; }
#alerte .al-t { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 1.02rem; color: #fff; line-height: 1.1; }
#alerte .al-s { font-family: 'Barlow'; font-size: .72rem; color: #9db0c6; margin-top: 1px; }
#alerte .al-go { appearance: none; cursor: pointer; flex: 0 0 auto;
  border: 0; background: var(--blue, #2f7bf0); color: #fff; border-radius: 999px;
  padding: 8px 15px; font-family: 'Barlow Condensed'; font-weight: 700;
  font-size: .82rem; letter-spacing: .08em; text-transform: uppercase; }
#alerte .al-x { appearance: none; cursor: pointer; flex: 0 0 auto;
  border: 0; background: transparent; color: #6b7a90; font-size: .9rem;
  padding: 6px 2px 6px 4px; line-height: 1; }

/* ============ RETOURS TESTEURS (temporaire, phase de test — 5 août 2026) ====
   Bouton discret au-dessus de TOUTE l'app + volet en 3 étapes, DA Skydex,
   AUCUN emoji (consigne). Préfixe fb- (règle des modules). Se coupe par
   CFG.FEEDBACK.ENABLED = false — ne pas supprimer, une prochaine phase de
   test le réutilisera. */
#fb-btn { position: absolute; top: calc(8px + env(safe-area-inset-top)); right: 12px;
  z-index: 39;   /* sous le volet d'alerte (40) : une alerte qui glisse passe devant */
  width: 34px; height: 34px; border-radius: 50%; padding: 0; cursor: pointer;
  background: rgba(10,16,24,.55); backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,.14); color: #8fa1b8;
  display: flex; align-items: center; justify-content: center; }
#fb-btn.hidden { display: none; }
#fb-btn svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 1.8; }

#fb-overlay { position: absolute; inset: 0; z-index: 66; display: none;
  background: rgba(4,8,14,.72); backdrop-filter: blur(4px);
  align-items: flex-end; justify-content: center; }
#fb-overlay.open { display: flex; }
.fb-box { position: relative; width: 100%; max-width: 480px;
  background: #0d1626; border: 1px solid rgba(255,255,255,.1); border-bottom: 0;
  border-radius: 22px 22px 0 0; padding: 22px 20px calc(26px + env(safe-area-inset-bottom));
  animation: fbUp .32s cubic-bezier(.2,.9,.25,1); }
@keyframes fbUp { from { transform: translateY(40px); opacity: 0; } }

.fb-t { font-family: 'Barlow Condensed'; font-style: italic; font-weight: 800;
  font-size: 1.4rem; color: #fff; margin-bottom: 14px; }
.fb-x { position: absolute; top: 12px; right: 12px; appearance: none; border: 0;
  background: transparent; color: #6b7a90; font-size: .95rem; cursor: pointer; padding: 6px; }

.fb-choices { display: flex; flex-direction: column; gap: 9px; }
.fb-choice { display: flex; align-items: center; gap: 13px; text-align: left;
  appearance: none; cursor: pointer; background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.1); border-radius: 14px; padding: 13px 14px;
  color: #e6edf6; font-family: 'Barlow'; }
.fb-choice .ic { flex: 0 0 38px; height: 38px; border-radius: 50%;
  background: #14273f; display: flex; align-items: center; justify-content: center; }
.fb-choice .ic svg { width: 19px; height: 19px; stroke: #7fa8dd; fill: none; stroke-width: 1.8; }
.fb-choice .tx { display: flex; flex-direction: column; gap: 1px; }
.fb-choice .tx b { font-size: .95rem; font-weight: 600; }
.fb-choice .tx i { font-style: normal; font-size: .74rem; color: #8fa1b8; }

.fb-msg { width: 100%; min-height: 120px; resize: vertical;
  background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.12);
  border-radius: 12px; color: #fff; font-family: 'Barlow'; font-size: .95rem;
  padding: 12px 13px; line-height: 1.45; }
.fb-msg:focus { outline: none; border-color: rgba(61,139,253,.55); }

.fb-shot { display: flex; align-items: center; gap: 11px; cursor: pointer;
  background: rgba(255,255,255,.04); border: 1px dashed rgba(255,255,255,.2);
  border-radius: 12px; padding: 12px 13px; color: #9db0c6;
  font-family: 'Barlow'; font-size: .86rem; margin-bottom: 14px; }
.fb-shot .ic svg { width: 18px; height: 18px; stroke: #7fa8dd; fill: none; stroke-width: 1.8; }
.fb-shot i { font-style: normal; color: #6b7a90; }
.fb-lab { display: block; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase;
  color: #8fa1b8; margin: 2px 0 6px; }
.fb-name { width: 100%; background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.12); border-radius: 12px; color: #fff;
  font-family: 'Barlow'; font-size: .95rem; padding: 12px 13px; }
.fb-name:focus { outline: none; border-color: rgba(61,139,253,.55); }

.fb-row { display: flex; gap: 10px; margin-top: 16px; }
.fb-sec { flex: 0 0 auto; appearance: none; cursor: pointer;
  border: 1px solid #33415c; background: transparent; color: #9db0c6;
  border-radius: 999px; padding: 12px 20px; font-family: 'Barlow'; font-size: .88rem; }
.fb-cta { flex: 1; appearance: none; cursor: pointer; border: 0;
  background: var(--blue, #2f7bf0); color: #fff; border-radius: 999px; padding: 13px 22px;
  font-family: 'Barlow Condensed'; font-weight: 700; font-size: 1rem;
  letter-spacing: .08em; text-transform: uppercase; }
.fb-cta:disabled { opacity: .35; cursor: default; }
.fb-err { margin-top: 12px; font-family: 'Barlow'; font-size: .82rem; color: #fda4af; }

.fb-done { text-align: center; padding: 8px 0 4px; }
.fb-done .ic { display: inline-flex; width: 52px; height: 52px; border-radius: 50%;
  background: rgba(55,214,122,.12); border: 1px solid rgba(55,214,122,.4);
  align-items: center; justify-content: center; margin-bottom: 12px; }
.fb-done .ic svg { width: 24px; height: 24px; stroke: #4ade80; fill: none; stroke-width: 2.2; }
.fb-done p { font-family: 'Barlow'; font-size: .88rem; color: #9db0c6;
  margin: 6px 0 18px; line-height: 1.5; }
