/* Discover — feuille de style unique du front (D-01 : zéro dépendance, CSS natif, zéro build). */

:root {
  /* ===== Marque — charte Tinder, refonte Porto Rocha du 17 juillet 2026 =====
     Les hex officiels ne sont PAS publics (la charte de marque n'est pas
     diffusée) : ces valeurs reprennent le dégradé signature documenté de
     longue date et la direction annoncée du rebrand — flamme affûtée,
     wordmark tout en capitales, serif éditorial, et surtout une palette
     ÉLARGIE où le bleu et le vert deviennent des couleurs de marque à part
     entière aux côtés du rouge/rose/orange (« le spectre émotionnel du
     dating »). C'est cette dernière décision qui distingue la charte 2026
     de l'ancienne : le bleu et le vert ci-dessous ne sont pas des couleurs
     d'état empruntées au système, ce sont des couleurs de la marque. */
  --brand-pink: #fd267d;
  --brand-red: #fe3c72;
  --brand-coral: #ff4e63;
  --brand-orange: #ff6036;
  --brand-blue: #21c0fa;
  --brand-green: #24e5a0;
  --brand-purple: #a45cff;
  --brand-gold: #ffb33b;

  /* Le dégradé signature. `--brand-gradient` pour les grandes surfaces
     (login, en-têtes), `--brand-gradient-flat` pour les traits fins et les
     textes en `background-clip: text` où un angle de 135° se lirait mal. */
  --brand-gradient: linear-gradient(135deg, #fd267d 0%, #ff4e63 52%, #ff6036 100%);
  --brand-gradient-flat: linear-gradient(90deg, #fd267d, #ff6036);

  /* Colors — CLAIR par défaut, comme Tinder.
     L'appli était intégralement sombre. Tinder, lui, est clair : carte
     photo flottante posée sur un fond blanc, boutons d'action en dessous.
     C'est ce contraste carte/fond qui fait la signature — dans un habillage
     tout noir, la carte n'a plus de bord et l'écran devient un fond perdu.
     Le bloc `prefers-color-scheme: dark` plus bas rétablit un canvas sombre
     quand le téléphone est en mode sombre : c'est exactement ce que fait
     Tinder, et ça évite un écran blanc éblouissant la nuit — usage réel de
     cette appli. */
  --color-bg: #ffffff;
  --color-surface: #ffffff;
  --color-surface-2: #f3f2f4;
  --color-border: #e5e3e7;
  --color-text: #17161a;
  --color-text-muted: #6f6b73;
  --color-primary: #17161a;
  --color-primary-hover: #000000;
  --color-accent: var(--brand-red);   /* état actif : onglet courant, filtre sélectionné, focus clavier, lien externe au survol */
  --color-danger: var(--brand-coral); /* nope */
  --color-success: var(--brand-green);/* like */

  /* Typography */
  /* --font-display : le serif éditorial de la charte 2026. Porto Rocha a
     dessiné un serif sur mesure, non diffusé ; `ui-serif` résout vers New
     York sur iOS/macOS — le serif système d'Apple, éditorial et réellement
     beau — ce qui couvre exactement le parc de Thomas et de ses potes.
     AUCUNE webfont : D-01 impose zéro dépendance, zéro build, et une police
     distante ferait en plus une requête sortante depuis une appli NSFW. */
  --font-display: ui-serif, 'New York', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, 'SF Mono', monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --text-3xl: 1.875rem;

  /* Spacing */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;

  /* Shapes — arrondis franchement augmentés : la carte très arrondie et le
     bouton parfaitement circulaire sont deux signatures fortes de Tinder,
     et l'ancien maximum de 18px donnait des angles quasi carrés. */
  --radius-sm: 8px;
  --radius-md: 14px;
  /* Relevé sur les captures officielles Tinder 17.29.1 : les cartes ont un
     arrondi nettement plus généreux que le nôtre (≈28-32px sur une largeur
     de ~360px). 22px donnait des angles encore trop secs. */
  --radius-lg: 30px;
  --radius-xl: 30px;
  --radius-full: 9999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
  --shadow-md: 0 4px 10px rgba(0,0,0,0.45);
  --shadow-lg: 0 12px 32px rgba(0,0,0,0.55);
  /* Halo chaud : sert aux CTA en dégradé, jamais une ombre noire dessous —
     une ombre neutre sous un bouton rose le ternit. */
  --shadow-brand: 0 6px 20px rgba(253,38,125,0.38);
  /* Ombre de la carte de swipe : nette et rapprochée sur fond clair, sinon
     la carte flotte dans le vide. Redéfinie en mode sombre (voir plus bas),
     où une ombre noire ne se verrait pas. */
  --shadow-card: 0 4px 18px rgba(0,0,0,0.16), 0 1px 4px rgba(0,0,0,0.10);
}

/* Mode sombre — suit le réglage du téléphone, comme Tinder. Seules les
   NEUTRES basculent : le rose/orange de la marque et les couleurs d'action
   (corail, menthe, or) sont identiques dans les deux modes, sinon la marque
   changerait de teinte selon l'heure. */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #12100f;
    --color-surface: #1c1917;
    --color-surface-2: #262220;
    --color-border: #35302d;
    --color-text: #faf7f5;
    --color-text-muted: #a29a96;
    --color-primary: #faf7f5;
    --color-primary-hover: #ffffff;
    --shadow-card: 0 4px 22px rgba(0,0,0,0.55);
  }
}

/* ---- Reset et body ---- */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--color-bg); color: var(--color-text); font-family: var(--font-sans); }
a { color: inherit; }
button { font-family: inherit; cursor: pointer; }
/* [hidden] GLOBAL, !important nécessaire : n'importe quelle règle d'auteur
   posant une propriété `display` (ex. la classe icône bouton plus bas dans
   ce fichier, Plan 08-01 feedback checkpoint) bat sinon l'attribut hidden —
   c'est exactement ce qui a laissé la roue crantée ET la cloche de
   validation (#admin-btn, fuite D-01/Plan 06-03) visibles hors de l'onglet
   Swipe malgré l'attribut posé par switchTab()/renderShell(). #banner[hidden]
   (plus bas) est devenue redondante avec cette règle mais reste inoffensive,
   laissée en place. */
[hidden] { display: none !important; }

/* ---- App-shell plein écran (iOS safe-area) ---- */
/* Trois replis, dans cet ordre — le CSS ne devine plus la hauteur visible,
   il la reçoit :
   1) 100vh : repli universel pour les moteurs qui ne reconnaissent pas svh.
   2) 100svh : petit viewport là où il existe vraiment (Safari iOS, PWA
      standalone) — nécessaire comme repli sans JS, ne PAS le retirer.
   3) --app-height (repli 100svh) : la mesure JS (viewport.js, Plan 04-20)
      quand elle existe — c'est elle qui l'emporte en dernier.
   On a appris (session tabbar-clipped-swipe-screen.md) que sur les
   navigateurs iOS tiers (Chrome/Firefox, tous WKWebView), les navigateurs
   hôtes n'ont jamais adopté les API WKWebView minimumViewportInset/
   maximumViewportInset : WebKit ne rapporte alors qu'UNE seule hauteur de
   viewport pour toutes les unités, svh dégénère vers le grand viewport, et
   la tabbar (dernier enfant flex, 44px + 3px de bordures) passe entièrement
   sous la toolbar déployée du navigateur (~44px) — le fix svh du Plan 04-07
   était donc partiel. --app-height (viewport.js) corrige ce cas en mesurant
   la hauteur réellement visible en JS. Ne PAS revenir à un repli en
   « grand viewport » systématique — session tabbar-clipped-ios.md.
   Voir .planning/debug/tabbar-clipped-swipe-screen.md pour le diagnostic
   complet. */
.shell {
  /* Bloc conteneur des calques plein cadre montés dans la coque (feuille
     d'aide). SANS z-index : `position: relative` seul ne crée PAS de contexte
     d'empilement, donc l'ordre de peinture existant (tabbar 10, bannière 20)
     est strictement préservé. */
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100svh;
  height: var(--app-height, 100svh);
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  overflow: hidden;
  background: var(--color-bg);
}
@media (min-width: 480px) {
  .shell {
    max-width: 420px;
    height: min(780px, 100vh);
    height: min(780px, 100svh);
    height: min(780px, var(--app-height, 100svh));
    margin: 0 auto;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }
}

/* ---- Barre du haut ---- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 52px;
  flex: none;
  padding: 0 var(--space-4);
}
/* Bloc de marque de la topbar : flamme + logotype. C'est ici, et seulement
   ici, que la flamme apparaît — jamais sur l'écran de login. */
.brand { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
.flame { flex: none; display: block; }
/* Logotype tout en capitales (refonte 2026) dans le serif éditorial, rempli
   par le dégradé signature. `background-clip: text` marche sur du texte —
   c'est ce qui permet un logotype dégradé sans image ni SVG de texte. */
.topbar .title {
  font-family: var(--font-sans);   /* sans gras, comme le logotype 2026 */
  font-size: var(--text-lg);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  background: var(--brand-gradient-flat);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Repli : si background-clip:text n'est pas supporté, `color: transparent`
     rendrait le titre INVISIBLE. On force donc une couleur pleine partout où
     le support manque. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .topbar .title { background: none; color: var(--brand-red); }
}
/* La sonde #vp (viewport.js) réécrit .title en texte de diagnostic : illisible
   en dégradé sur une longue chaîne, donc on la neutralise dans ce cas. */
.topbar .title:not(:only-child) { max-width: 60vw; }
.icon-btn {
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  min-width: 44px;
  min-height: 44px;
  padding: var(--space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
}
.icon-btn:hover { color: var(--color-text); }
/* Les pictogrammes SVG (ICONS, deck.js) remplacent les anciens glyphes texte.
   `currentColor` fait le reste : la couleur vient du bouton porteur. */
.icon-btn svg { width: 22px; height: 22px; display: block; }

/* ---- Zones d'écran ---- */
.screen { display: none; flex: 1; position: relative; min-height: 0; }
.screen.active { display: block; }
.deck-area { position: absolute; inset: 0; }
.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  text-align: center;
  padding: var(--space-6);
}

/* ---- Barre d'onglets (3 onglets fixes : Swipe / Matchs / Potes) ---- */
/* `position: relative` + `z-index: 10` : la barre était statique (dernier
   enfant flex du shell) et se faisait peindre par-dessus par tout élément
   positionné de la couche carte — voir le volet ⓘ replié plus bas
   (`.card-a .sheet`) et .planning/debug/tabbar-masked-by-collapsed-info-
   sheet.md. La géométrie du repli (-100%) suffit déjà sur toutes les
   hauteurs plausibles, mais le z-index rend l'occlusion structurellement
   impossible, quelle que soit la géométrie de l'écran (paysage court) ou un
   futur élément de la couche carte. Ne PAS donner de z-index à
   .screen/.deck-area : ça créerait un contexte d'empilement en conflit avec
   #banner (z-index 20) et les tampons de swipe. */
/* Dimensions relevées sur les captures de Thomas (Tinder 17.29.1, iPhone) et
   converties en points : barre de ~56pt de haut hors safe-area, icônes de
   ~24pt, libellés de ~11pt sous l'icône, et surtout un GRAND rayon de
   pastille sur l'onglet actif. Aucune bordure haute chez lui : la barre se
   détache par sa couleur de fond, pas par un filet.
   Trois onglets conservés (décision de Thomas) — seules les DIMENSIONS sont
   alignées, pas le nombre. */
.tabbar {
  display: flex;
  flex: none;
  position: relative;
  z-index: 10;
  height: 56px;
  padding: 0 var(--space-2);
  background: var(--color-surface);
  border-top: none;
}
.tab {
  flex: 1;
  position: relative;
  background: transparent;
  border: none;
  border-top: none;
  color: var(--color-text-muted);
  font-size: 11px;
  min-height: 44px;
  padding: var(--space-1) var(--space-2);
  /* Icône au-dessus du libellé — gabarit d'onglet de Tinder. Le libellé est
     conservé (l'appli a trois onglets aux rôles peu devinables : une barre
     100 % icônes serait un pari inutile). */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  transition: color 0.15s ease;
}
.tab-ico { display: block; width: 24px; height: 24px; }
.tab-lbl { line-height: 1; font-weight: 600; letter-spacing: -0.01em; }
/* Actif : la couleur de marque prend TOUT l'onglet, icône comprise, et le
   filet supérieur passe en dégradé. L'ancien état actif (texte blanc + trait
   bleu 2px) ne disait rien de la marque. */
/* Actif = PASTILLE DE FOND autour de l'icône et du libellé, relevé sur les
   captures de Thomas. Ce n'était pas un filet dégradé au-dessus de l'onglet
   (mon invention), et le libellé actif reste de la couleur du texte, pas
   rose. */
.tab.active { color: var(--color-text); border-top-color: transparent; }
.tab.active::before {
  content: "";
  position: absolute;
  inset: 4px 6px;
  border-radius: var(--radius-full);
  background: var(--color-surface-2);
  z-index: -1;
}
.tab > * { position: relative; z-index: 1; }

/* ---- Écran de login ----
   Première impression, donc traité comme une page de marque et non comme un
   formulaire : dégradé signature plein cadre, wordmark en capitales dans le
   serif éditorial, formulaire en verre dépoli par-dessus.

   DÉLIBÉRÉMENT SANS FLAMME NI LOGO TINDER ICI (décision de Thomas) : cet
   écran est le seul que voit quelqu'un qui n'est pas connecté. L'identité y
   passe par la couleur, la typo et le ton — pas par une marque empruntée.
   La flamme n'apparaît qu'une fois la session ouverte (voir .flame). */
/* APLAT ROUGE VIF, pas le dégradé signature — relevé sur les captures de
   l'écran de connexion Tinder fournies par Thomas (06/08). Le dégradé
   rose->orange existe bien dans la charte, mais Tinder ne le sort PAS sur
   cet écran-là : la connexion est un aplat unique, et c'est ce qui lui
   donne son côté massif. J'avais extrapolé la page depuis la charte au lieu
   de regarder l'écran — d'où le rose bonbon, puis un brique trop terne,
   corrigés ici. Ne pas le « raccorder » à --brand-red : ce sont deux rouges
   différents, et c'est celui-ci qui est sur l'écran. */
.shell-login {
  --login-red: #cd130a;   /* valeurs donnees par Thomas, releve officiel */
  --login-white: #f1e6ef; /* le blanc Tinder tire vers le rose : ce n'est pas #fff.
                             Reserve au LOGOTYPE et au TEXTE ; les pastilles,
                             elles, sont blanc pur — c'est ce contraste qui
                             fait lire la nuance rosee du mot. */
  background: var(--login-red);
  border: none;
}
/* PAS de voile sombre ici. Une première version posait un
   `radial-gradient(… rgba(0,0,0,0.28))` par-dessus pour « garantir le
   contraste » : à l'écran, il assombrissait tout le bas et faisait virer
   l'orange au BRUN SALE — l'inverse de la vivacité qui fait la marque. Le
   contraste du texte est déjà assuré autrement (ombres portées sur le
   logotype et le slogan, pastille sombre sous le message d'erreur). Si un
   besoin de contraste ressurgit, le traiter sur l'élément concerné, jamais
   en ternissant le dégradé entier. */
/* Composition en DEUX BLOCS, pas un pavé centré : la marque occupe le tiers
   haut, le formulaire est ancré en bas de l'écran. C'est le gabarit de
   l'accueil Tinder — et accessoirement le bon geste sur mobile, les champs
   tombent sous le pouce. Une version précédente centrait les trois blocs
   verticalement : la page paraissait tassée au milieu avec un vide mort
   sous la mention de bas de page. */
.login {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  height: 100%;
  /* AUCUNE hauteur en `vh` ici, et c'est le point important : sur Safari
     iOS, `vh` se rapporte au GRAND viewport, celui qu'on aurait sans la
     barre d'adresse ni la barre d'outils. Un `padding-top: 30vh` y vaut
     donc bien plus de 30 % de ce qu'on voit vraiment — resultat, tout
     debordait, la page se mettait a defiler, et l'espace libre que le
     logotype doit avoir sous lui disparaissait. C'est exactement le piege
     que --app-height (viewport.js) existe pour eviter.
     La position du logotype est desormais ELASTIQUE : .login-hero prend
     tout l'espace libre et centre le mot dedans (voir plus bas). Elle
     s'adapte donc au navigateur avec ses barres comme au mode plein
     ecran, sans jamais deborder. */
  padding: var(--space-6) var(--space-6) var(--space-8);
  gap: var(--space-4);
  overflow-y: auto;
}
/* Le hero PREND tout l'espace libre (`flex: 1`) et centre le logotype
   dedans : c'est ce qui plaque le formulaire en bas ET pose le mot dans le
   haut de la page, sans hauteur codee en dur, sans position absolue (qui
   casserait le defilement quand le clavier iOS s'ouvre).
   `min-height` garde un minimum d'air sur les tres petits ecrans. */
.login-hero {
  flex: 1 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 88px;
  text-align: center;
}
/* Wordmark tout en capitales — le changement le plus visible de la refonte
   2026 (le logotype était en bas de casse depuis dix ans).
   SANS-SERIF, et c'est important : le logotype 2026 est un sans gras: le
   serif introduit par la refonte sert aux titres ÉDITORIAUX, pas au
   logotype. Une première version mettait le wordmark en serif — ça donnait
   un titre de roman, pas une marque d'appli. Le serif reste sur le slogan,
   qui est bien de la voix éditoriale. */
.wordmark {
  font-family: var(--font-sans);
  font-size: clamp(2rem, 12vw, 3rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.035em;
  line-height: 0.92;
  color: var(--login-white);
}
/* NOTE — .login-mark (la flamme) et .login-slogan ont été supprimés d'ici
   le 06/08 avec les éléments qu'ils habillaient. Verdict de Thomas :
   « vire le slogan il est de trop, pareil pour le logo flamme ». L'écran de
   connexion ne porte plus que le logotype et le bloc de texte du bas —
   ce qui est aussi, exactement, ce que porte le leur. */

/* Champs POSÉS À MÊME LE DÉGRADÉ, sans carte de verre autour.
   Une première version enfermait le formulaire dans une carte en verre
   dépoli : à l'écran, la carte rose pâle et les champs rose pâle se
   confondaient — on ne voyait plus où taper, et c'était accessoirement
   l'élément le moins Tinder de la page (l'accueil de Tinder est un dégradé
   plein, sans panneau). Retirer la carte règle les deux d'un coup. */
/* Bloc de « mentions », a la place exacte du pave juridique de l'ecran
   Tinder : petit corps, bas de casse, gris blanc. Il se lit comme du texte
   qu'on ne lit pas — c'est tout l'effet recherche. */
.login-legal {
  margin: var(--space-1) auto var(--space-2);
  max-width: 30em;
  text-align: center;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: var(--login-white);
}
.login-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.field { display: flex; flex-direction: column; gap: var(--space-1); }
.field label { font-size: var(--text-xs); color: var(--color-text-muted); }
.field input {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  color: var(--color-text);
  min-height: 44px;
  font-size: 16px; /* sous 16px, iOS Safari zoome au focus et casse le plein écran */
}
/* Sur le dégradé, les champs repassent en blanc translucide : un champ gris
   anthracite y ferait un trou mort. */
/* Intitules retires visuellement (jamais du DOM : les lecteurs d'ecran et
   `for`/`id` en dependent). Au-dessus d'un champ blanc, deux libelles en
   capitales espacees alourdissaient l'ecran pour une information que le
   placeholder porte deja. */
.login-form .field label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Contour blanc franc : c'est ce qui dit « tape ici ». Le remplissage reste
   discret pour ne pas ternir le dégradé, mais la BORDURE, elle, est nette —
   à 0.3 d'opacité le champ était invisible à l'écran. */
/* Champs BLANCS PLEINS a texte sombre. Les pastilles translucides etaient
   delavees : sur un fond sature, un remplissage blanc plein est a la fois
   plus lisible et beaucoup plus net — et c'est ce que fait Tinder de ses
   champs sur fond de couleur. */
.login-form .field input {
  background: #fff;
  border: none;
  color: #1a1418;
  border-radius: var(--radius-full);
  min-height: 52px;
  padding-left: var(--space-5, 20px);
  padding-right: var(--space-5, 20px);
  box-shadow: 0 2px 10px rgba(0,0,0,0.10);
}
.login-form .field input::placeholder { color: #8b8189; }
.login-form .field input:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255,255,255,0.55), 0 2px 10px rgba(0,0,0,0.10);
}
/* CTA : pastille blanche pleine à texte NOIR GRAS, casse normale. C'est
   littéralement le bouton « Se connecter avec Apple » de l'écran de
   connexion Tinder. Le texte rose en capitales espacées de la version
   précédente ne vient d'aucun de leurs écrans. */
.btn-primary {
  background: var(--color-primary);
  color: var(--color-bg);
  border: none;
  border-radius: var(--radius-full);
  min-height: 48px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--text-sm);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.btn-primary:hover { background: var(--color-primary-hover); }
.btn-primary:active { transform: scale(0.98); }
.btn-primary:disabled { opacity: 0.65; cursor: default; transform: none; }
.login-form .btn-primary {
  background: #fff;
  color: #111;
  min-height: 56px;
  font-size: 1.0625rem;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
  box-shadow: none;
}
.login-form .btn-primary:hover:not(:disabled) { transform: translateY(-1px); }
.form-error { font-size: var(--text-sm); color: var(--color-danger); }
/* Sur le dégradé, le corail de --color-danger n'a plus assez de contraste :
   le message d'erreur passe en blanc sur pastille sombre. Vide, il ne doit
   occuper aucune place — d'où :not(:empty). */
.login-form .form-error:not(:empty) {
  color: #fff;
  background: rgba(0,0,0,0.34);
  border: 1px solid rgba(255,255,255,0.24);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  text-align: center;
}
/* Pied de page en BLANC PLEIN ET GRAS, pas en gris translucide : sur
   l'écran Tinder, « Des problèmes de connexion ? » occupe cette place avec
   exactement ce poids. */
.login-foot {
  text-align: center;
  font-size: var(--text-base);
  font-weight: 700;
  color: #fff;
  margin-top: var(--space-2);
}

/* ---- Feuille d'aide (bouton « ? ») ----
   Calque plein cadre, panneau centré. Fermeture par tap sur le fond, par le
   bouton, ou en changeant d'onglet. Jamais imposée : c'est un second rideau,
   pas un tutoriel. */
.help-sheet {
  position: absolute;
  inset: 0;
  z-index: 30;              /* au-dessus de la bannière (20) et des calques carte */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
.help-panel {
  width: 100%;
  max-height: 86%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5, 20px) var(--space-4) var(--space-4);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
}
.sheet-help { margin-left: auto; color: var(--color-text-muted); }
.sheet-help:hover { color: var(--color-text); }

.help-body h2 {
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-3);
}
.help-body ul { list-style: none; display: flex; flex-direction: column; gap: var(--space-3); }
.help-body li {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--color-text);
  padding-left: var(--space-4);
  position: relative;
}
.help-body li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--brand-red);
}
.help-close {
  align-self: stretch;
  min-height: 44px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--brand-gradient-flat);
  color: #fff;
  font-weight: 800;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ---- Bannière ---- */
#banner {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  right: var(--space-2);
  z-index: 20;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  font-size: var(--text-sm);
}
#banner[hidden] { display: none; }
#banner.error { border-color: var(--color-danger); color: var(--color-danger); }

/* ---- Deck épuisé : écran calme, pas d'alerte, pas de bouton de rechargement ---- */
.end-screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-12) var(--space-6);
  background: var(--color-surface);
}
.end-screen h2 { font-size: var(--text-2xl); font-weight: 800; letter-spacing: -0.02em; }
.end-screen .sub { font-size: var(--text-sm); color: var(--color-text-muted); margin-top: var(--space-2); }

/* ---- Accessibilité transverse ---- */
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ===== Couche carte : Immersion, tampons, actions, volet ⓘ, volet filtres ===== */

/* ---- Tampons de swipe ---- */
/* Tampons LIKE/NOPE : plus épais et plus larges qu'avant — chez Tinder c'est
   un élément graphique franc, pas une étiquette discrète. Le geste de swipe
   qui les révèle est justement ce que la refonte 2026 a promu au rang de
   langage visuel de la marque (« le swipe révèle ce qui est dessous »). */
.stamp {
  position: absolute;
  top: 48px;
  padding: 8px 18px;
  border: 4px solid;
  border-radius: var(--radius-md);
  font-weight: 900;
  font-size: var(--text-3xl);
  letter-spacing: 3px;
  text-transform: uppercase;
  opacity: 0;
  transform: rotate(-12deg);
  pointer-events: none;
  z-index: 6;
  text-shadow: 0 2px 10px rgba(0,0,0,0.35);
}
.stamp-like { left: 20px; color: var(--color-success); border-color: var(--color-success); }
.stamp-nope { right: 20px; color: var(--color-danger); border-color: var(--color-danger); transform: rotate(12deg); }

/* ---- Barre d'actions ---- */
/* Rangée d'action posée SOUS la carte, dans la gouttière de 104px que
   `.card-a` laisse en bas — sur le fond de l'appli, plus par-dessus la
   photo. C'est le gabarit de Tinder, et ça libère le bas de l'image. */
/* DEUX boutons seulement, POSÉS SUR LA PHOTO, en cercles de verre sombre —
   relevé sur les captures de Thomas. Ce ne sont pas des disques blancs
   alignés sous une carte : ce sont deux pastilles translucides à glyphe
   blanc (✕) et rouge (♥), au-dessus du voile bas de la photo. Le retour
   arrière n'est PAS dans cette rangée : c'est une petite flèche circulaire
   en haut à droite de la photo (voir .act-rewind). */
.actions {
  position: absolute;
  bottom: 26px;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 24px;
  z-index: 7;
}
/* Disque BLANC plein, glyphe coloré — la signature la plus reconnaissable de
   Tinder. L'ancienne version (disque sombre translucide, glyphe coloré, qui
   s'inversait au survol) se fondait dans la photo et ne se lisait pas d'un
   coup d'œil sur un fond clair. Le blanc plein tient sur n'importe quelle
   photo, ce qui est exactement le problème que Tinder résout ainsi. */
/* Hiérarchie de tailles : chez Tinder les deux actions principales (Nope,
   Like) sont nettement plus GROSSES que les secondaires — c'est ce contraste,
   autant que la couleur, qui rend la rangée lisible d'un coup d'œil et le
   pouce sûr de lui. Une première version donnait 58px aux trois boutons : la
   rangée était plate, on devait lire les glyphes pour viser. */
.act {
  width: 64px;
  height: 64px;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-full);
  border: none;
  /* Verre sombre translucide, pas un disque blanc plein : sur les captures
     la photo transparaît derrière les deux boutons. */
  background: rgba(20,20,22,0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-size: 24px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s ease;
  box-shadow: none;
}
.act:hover { transform: scale(1.12); }
.act:active { transform: scale(0.94); }
.act svg { width: 30px; height: 30px; display: block; }
.act-rewind svg { width: 22px; height: 22px; }
.act[disabled] { opacity: 0.4; cursor: default; }
.act[disabled]:hover { transform: none; box-shadow: 0 4px 14px rgba(0,0,0,0.35); }
/* Couleurs d'action de la charte. Le vert et le bleu ne sont plus des
   couleurs d'état système empruntées : depuis la refonte 2026 ils font
   partie de la palette de marque au même titre que le rose. */
/* Glyphes relevés sur les captures : la croix est BLANCHE, le cœur est
   ROUGE. Pas de corail ni de menthe — ces couleurs venaient de la palette
   élargie 2026, mais l'appli, elle, s'en tient au blanc et au rouge. */
.act-nope { color: #fff; }
.act-like { color: #ff4458; }
/* Action secondaire : plus petite, mais JAMAIS sous 44px — c'est le plancher
   de cible tactile, et le retour arrière est justement ce qu'on cherche
   après un swipe raté, souvent en marchant. */
/* Retour arrière : DANS la rangée d'action (retour de Thomas), et plus petit
   que Passer/Aimer — chez Tinder les cinq boutons n'ont pas la même taille,
   c'est ce contraste qui hiérarchise la rangée. */
.act-rewind {
  width: 48px;
  height: 48px;
  min-width: 44px;
  min-height: 44px;
  color: var(--brand-gold);
}
.act-rewind svg { width: 22px; height: 22px; }
.act-rewind:hover { box-shadow: 0 0 0 2px var(--brand-gold), 0 6px 18px rgba(255,179,59,0.45); }

/* ---- Écran « C'est un Match » ----
   Refait sur la capture de Thomas (Tinder 17.29.1) : fond BORDEAUX SOMBRE,
   « C'est un » et « Match » en serif italique ROUGE VIF qui encadrent deux
   photos INCLINÉES et superposées, puis la ligne « T'as matché avec … » et
   une grosse pastille BLANCHE.

   Mon premier jet était un dégradé rose avec une pastille ronde centrée et
   un titre en capitales : ça n'avait aucun rapport. */
.match-overlay {
  position: absolute;
  inset: 0;
  /* Au-dessus des onglets (10) et de la banniere (20) : l'ecran de match est
     plein ecran, il ne doit rien laisser depasser. */
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-4);
  background: #5c1414;
  color: #fff;
  text-align: center;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.match-overlay.open { opacity: 1; }
.match-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}
.match-close {
  position: absolute;
  top: var(--space-4);
  left: var(--space-4);
  z-index: 3;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
  border: 1.5px solid rgba(255,255,255,0.85);
  background: transparent;
  color: #fff;
  font-size: 18px;
  line-height: 1;
}
/* Les deux mots du titre. Serif italique, rouge vif sur le bordeaux — c'est
   le contraste qui fait tout l'écran. Ils MORDENT sur les photos (marges
   négatives) au lieu de se poser sagement au-dessus. */
.match-word {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 700;
  font-size: clamp(3rem, 17vw, 4.5rem);
  line-height: 0.95;
  color: #e8321e;
  letter-spacing: -0.01em;
}
.match-word-top { align-self: center; margin-bottom: -18px; z-index: 2; }
.match-word-bottom { align-self: center; margin-top: -26px; z-index: 2; }

/* Les deux clichés inclinés. `center top` et NON `center` : les photos
   d'acteurs sont des portraits, un cadrage centré coupait systématiquement
   la tête — bug signalé par Thomas. */
.match-stack {
  position: relative;
  width: 100%;
  height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.match-shot {
  position: absolute;
  width: 168px;
  height: 224px;
  border-radius: 10px;
  background: var(--brand-gradient) center top / cover no-repeat;
  box-shadow: 0 14px 34px rgba(0,0,0,0.45);
}
.match-shot-back {
  transform: rotate(-9deg) translate(-52px, -14px);
}
.match-shot-front {
  transform: rotate(7deg) translate(46px, 16px);
}
/* Animation d'entrée : les deux clichés arrivent en tombant et en pivotant,
   le titre se pose ensuite. C'est ce qui donne vie à l'écran — sans elle,
   « It's a Match » n'est qu'une affiche fixe. */
.match-overlay.open .match-shot-back {
  animation: match-drop-left 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}
.match-overlay.open .match-shot-front {
  animation: match-drop-right 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) 0.08s both;
}
@keyframes match-drop-left {
  from { transform: rotate(-26deg) translate(-150px, -90px); opacity: 0; }
  to   { transform: rotate(-9deg) translate(-52px, -14px); opacity: 1; }
}
@keyframes match-drop-right {
  from { transform: rotate(26deg) translate(150px, 100px); opacity: 0; }
  to   { transform: rotate(7deg) translate(46px, 16px); opacity: 1; }
}
.match-overlay.open .match-word {
  animation: match-word-in 0.42s cubic-bezier(0.2, 0.9, 0.3, 1.15) both;
}
.match-overlay.open .match-word-bottom { animation-delay: 0.1s; }
@keyframes match-word-in {
  from { opacity: 0; transform: scale(0.82); }
  to   { opacity: 1; transform: scale(1); }
}
/* `prefers-reduced-motion` : on coupe les animations, jamais l'écran. */
@media (prefers-reduced-motion: reduce) {
  .match-overlay.open .match-shot-back,
  .match-overlay.open .match-shot-front,
  .match-overlay.open .match-word { animation: none; }
}

.match-name {
  margin-top: var(--space-4);
  font-size: var(--text-base);
  font-weight: 700;
}
.match-line {
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--text-base);
  line-height: 1.3;
  max-width: 26ch;
  color: rgba(255,255,255,0.92);
}
.match-cta {
  margin-top: var(--space-6);
  min-height: 54px;
  padding: 0 var(--space-8);
  border: none;
  border-radius: var(--radius-full);
  background: #fff;
  color: #111;
  font-weight: 800;
  font-size: var(--text-base);
}
.match-dismiss {
  margin-top: var(--space-3);
  min-height: 44px;
  padding: 0 var(--space-4);
  border: none;
  background: transparent;
  color: rgba(255,255,255,0.82);
  font-size: var(--text-sm);
}

/* ---- Chips et badges ---- */
/* Pastilles posées sur la photo : blanc forcé pour la même raison que
   `.card-a .grad` — elles ne suivent pas le thème de l'appli, elles suivent
   la photo. Fond remonté à 0.22 : à 0.12 elles disparaissaient sur une photo
   claire. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  background: rgba(0,0,0,0.42);
  backdrop-filter: blur(4px);
  font-size: var(--text-xs);
  color: #fff;
}
.badge-source {
  border: 1px solid rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.18);
}

/* ---- Carte Immersion (variante A, gagnante) ---- */
/* Carte FLOTTANTE, pas plein cadre. `inset` laisse une gouttière de 12px sur
   les côtés et surtout 104px en bas : c'est dans cette bande, sur le fond
   clair, que la rangée d'action vient se poser — le gabarit exact de Tinder.
   Avant, la carte occupait tout l'écran et les boutons flottaient PAR-DESSUS
   la photo.
   `overflow: hidden` reste INTERDIT ici (test dédié) : il rognerait la carte
   pendant l'animation de sortie du swipe. L'arrondi est donc porté par les
   couches internes (.photo, .grad, .sheet), pas par un découpage du parent. */
/* PLEIN CADRE — relevé sur les captures de Thomas (Tinder 17.29.1, iPhone,
   2026-08-06) : la photo va bord à bord, sans gouttière, sans arrondi, sans
   ombre. J'avais introduit une carte flottante sur fond clair : c'était le
   Tinder d'AVANT, et ça éloignait l'appli de la référence sur son écran le
   plus important.
   `overflow: hidden` reste INTERDIT ici (test dédié) : il rognerait la carte
   pendant l'animation de sortie du swipe. */
.card-a {
  position: absolute;
  inset: 0;
  background: #000;
  touch-action: none;
  user-select: none;
}
.card-a .photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center top;
}
.card-a .segs { position: absolute; top: 10px; left: 12px; right: 12px; display: flex; gap: 4px; z-index: 5; }
.card-a .seg { flex: 1; height: 3px; border-radius: 2px; background: rgba(255,255,255,0.3); transition: background 0.15s; }
.card-a .seg.on { background: #fff; }
.card-a .tap { position: absolute; top: 0; bottom: 35%; width: 33%; z-index: 4; }
.card-a .tap.left { left: 0; }
.card-a .tap.right { right: 0; }
.card-a .grad {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* La rangée d'action est REVENUE sur la photo (alignement sur les captures
     de Thomas), donc le voile doit de nouveau lui réserver sa place en bas —
     sinon les deux pastilles chevauchent le nom, ce qui s'est produit. Ordre
     de bas en haut sur la référence : onglets, boutons, localisation, nom. */
  padding: 18px 16px 104px;
  background: linear-gradient(transparent, rgba(0,0,0,0.55) 40%, rgba(0,0,0,0.92));
  /* AUCUN arrondi en bas. La carte est plein cadre depuis la refonte : le
     voile n'a plus de coin de carte a epouser, et ses angles arrondis se
     voyaient alors comme deux entailles claires en bas de la photo. Verdict
     de Thomas : « vire moi les arrondis en bas des photos ca me donne la
     gerbe ». Ne pas les remettre en croyant « raccorder » le voile au
     conteneur : c'est le conteneur qui n'a plus d'arrondi la. */
  z-index: 5;
  /* BLANC FORCÉ, jamais var(--color-text). Tout ce qui est posé sur la photo
     vit sur un voile sombre, quel que soit le thème de l'appli. Sans cette
     ligne, le passage au thème clair a rendu le nom NOIR SUR NOIR — invisible
     à l'écran alors que tous les tests passaient. */
  color: #fff;
}
/* Nom en SANS gras, pas en serif : le serif de la refonte 2026 est réservé
   aux titres éditoriaux (marketing), l'interface produit de Tinder reste en
   sans. Un premier essai en serif donnait une fiche de catalogue de musée. */
.card-a .name { font-size: var(--text-3xl); font-weight: 800; letter-spacing: -0.02em; text-shadow: 0 1px 4px rgba(0,0,0,0.6); }
/* Relevé sur les captures officielles de Tinder 17.29.1 (App Store FR,
   2026-08-05) : l'âge est à la MÊME TAILLE que le nom, seulement plus léger
   et en blanc plein — « Alex 21 », « Ethan 24 ». Nous l'affichions plus
   PETIT et grisé, ce qui le faisait lire comme une note de bas de page. */
.card-a .name .age { font-weight: 400; font-size: inherit; color: #fff; }
.card-a .meta-line { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; }

/* ---- Volet ⓘ ---- */
/* Repli du volet à -100 % (motif déjà éprouvé en prod par
   `.filter-sheet` ci-dessous) : à l'ancienne valeur `-78%` avec
   `height: 76%`, le bord HAUT du volet replié atterrissait à 2 % de la
   hauteur de la carte SOUS son bas — c'est-à-dire ~14 px à l'intérieur de la
   bande de la tabbar (~45 px). Aucun ancêtre (`.card-a`, `.deck-area`,
   `.screen`) ne porte d'overflow, le premier clip est `.shell {
   overflow: hidden }` au bas de la barre, et aucun ancêtre ne crée de
   contexte d'empilement : le volet (positionné, z-index:8) peignait donc
   par-dessus la tabbar (statique) et avalait les taps sur Matchs/Potes.
   Diagnostic complet : .planning/debug/
   tabbar-masked-by-collapsed-info-sheet.md. Invariant à préserver, verrouillé
   par un test : |bottom| − height ≥ la bande de la tabbar exprimée en % de
   la hauteur de la carte. `overflow: hidden` sur `.deck-area`/`.card-a` est
   EXCLU comme fix : ça rognerait la carte pendant l'animation de fly-out du
   swipe (transform translate/rotate sur .card-a, deck.js:645). */
.card-a .info-btn {
  position: absolute;
  right: 14px;
  /* Aligné sur la ligne du NOM, pas sur la rangée d'action : sur les
     captures de Thomas la flèche de déploiement est à hauteur du nom, tout
     à droite — pas au niveau des boutons. */
  bottom: 108px;
  min-width: 44px;
  min-height: 44px;
  padding: var(--space-2);
  border-radius: var(--radius-full);
  border: none;
  background: rgba(255,255,255,0.18);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 18px;
  z-index: 6;
  transition: all 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-a .info-btn:hover { background: rgba(255,255,255,0.35); }
.card-a .sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -100%;
  height: 76%;
  background: var(--color-surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  z-index: 8;
  transition: bottom 0.28s ease;
  overflow-y: auto;
  padding: 16px;
  box-shadow: var(--shadow-lg);
}
.card-a.open .sheet { bottom: 0; }

/* ---- Fiche info (partagée par le volet ⓘ) ---- */
.info-h {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--color-text-muted);
  margin: 18px 0 8px;
}
.info-h:first-child { margin-top: 4px; }
.id-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; font-size: var(--text-sm); }
.id-grid .k { color: var(--color-text-muted); font-size: var(--text-xs); }
.studio-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-xs);
  margin-bottom: 8px;
}

/* ---- Barres par studio (maquette validée .planning/sketches/001) : le
   Plan 09 les substituera à .studio-list quand le deck portera un compte
   par studio. Largeur du remplissage à 0 par défaut : la maquette la posait
   en attribut style="width:X%" inline, interdit par le CSP style-src 'self'
   (04-01-SUMMARY.md § CSP) — le Plan 09 la posera en CSSOM après rendu. ---- */
.studio-row { display: grid; grid-template-columns: 110px 1fr 34px; align-items: center; gap: 8px; margin-bottom: 6px; font-size: var(--text-xs); }
.studio-row .bar { height: 6px; border-radius: 3px; background: var(--color-surface-2); overflow: hidden; }
.studio-row .bar > i { display: block; height: 100%; width: 0; background: var(--color-accent); border-radius: 3px; }
.studio-row .n { text-align: right; color: var(--color-text-muted); }

/* ---- Badge « déjà sur Thomflix » (Phase 7, DEDUP-01, D-04) : sobre, dans
   le volet ⓘ uniquement, aucune couleur d'accent (mémoire
   design-sober-no-flashy) — se lit comme le reste du volet. ---- */
.dispo-line { font-size: var(--text-sm); color: var(--color-text-muted); margin-bottom: 8px; }
/* Ligne de fraîcheur de l'index Stash (Phase 7, Plan 07-03, atténuation
   D-09) : hérite de .dispo-line, plus petite, aucune couleur d'accent. */
.dispo-fresh { font-size: var(--text-xs); }

.links { display: flex; flex-wrap: wrap; gap: 8px; }
.links a {
  text-decoration: none;
  font-size: var(--text-xs);
  padding: 6px 12px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2);
  transition: all 0.15s;
}
.links a:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* ---- Volet filtres : surface distincte, ne touche jamais à .card-a ---- */
.filter-sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -100%;
  height: 60%;
  background: var(--color-surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  z-index: 9;
  transition: bottom 0.28s ease;
  overflow-y: auto;
  padding: var(--space-4);
  box-shadow: var(--shadow-lg);
}
.filter-sheet.open { bottom: 0; }

/* ---- En-tête de volet partagé (volet ⓘ + volet filtres) ----
   Rangée EN FLUX (aucun flottement) qui réserve sa propre hauteur : c'est
   l'ancien positionnement flottant à droite qui laissait le cercle de
   fermeture plonger de 17px dans la ligne des chips SOURCE. Voir
   .planning/debug/filter-close-invisible.md. */
.sheet-header {
  display: flex;
  justify-content: flex-end;
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--color-surface);
  margin-bottom: var(--space-2);
}
.card-a .sheet .close,
.filter-sheet .close {
  min-width: 44px;
  min-height: 44px;
  border: none;
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-sm);
}
.filter-group { margin-bottom: var(--space-4); display: flex; flex-wrap: wrap; gap: var(--space-2); }
.filter-group .info-h { flex-basis: 100%; }
.filter-chip {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
.filter-chip.on { border-color: var(--color-accent); color: var(--color-accent); }
/* Chip désactivée (ex. « OnlyFans (bientôt) » quand la source n'est pas
   servie) : texte atténué + opacité réduite, aucune couleur d'accent
   (mémoire projet : sobriété, monochrome, jamais de couleur criarde). La
   hauteur de cible tactile (44px) et le rayon viennent déjà de .filter-chip.
   Le sélecteur `:disabled` s'applique parce que filters.js pose la
   PROPRIÉTÉ `disabled` du bouton (attribut réfléchi en DOM réel), pas
   seulement une classe. */
.filter-chip:disabled {
  color: var(--color-text-muted);
  background: transparent;
  border-color: var(--color-border);
  opacity: 0.6;
  cursor: default;
}

/* ===== Écran Matchs (Phase 5) =====
   Additions strictement additives : aucune redéfinition de jeton, aucun
   jeton nouveau — tout est repris de :root ci-dessus (05-UI-SPEC.md). */

/* CR-01 : .matches-screen est le contexte de positionnement NON DÉFILANT de
   la fiche plein écran (.match-detail, position: absolute) — un ancêtre
   positionné qui défile ferait défiler la fiche AVEC le contenu (fiche
   ouverte hors écran dès que la liste est scrollée). Le défilement vit dans
   .matches-scroll, wrapper interne qui porte bande + liste. */
.matches-screen {
  position: absolute;
  inset: 0;
  overflow: hidden;
  padding: 0;
}
.matches-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  padding: var(--space-4);
}

/* ---- Bande "Consensus du groupe" (D-01) ----
   WR-04 : le titre est un heading en flux normal AU-DESSUS de la rangée ;
   les tuiles défilent dans .strip-scroll, rangée flex nowrap interne.
   (L'idiome flex-basis:100% de .filter-group .info-h ne vaut que dans un
   conteneur qui WRAPPE — dans un défileur horizontal nowrap, le titre se
   tasserait en colonne à gauche des photos.) */
.consensus-strip {
  display: block;
  padding: var(--space-4) 0;
}
.strip-scroll {
  display: flex;
  overflow-x: auto;
  gap: var(--space-2);
}
.strip-title {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-2);
  padding-left: var(--space-1);
}
/* TUILE PORTRAIT et non plus pastille ronde : sur les captures de Thomas, la
   rangée « Nouveaux Matchs » de l'écran Messages est faite de vignettes
   rectangulaires arrondies au format portrait, pas de ronds. Les ronds, chez
   Tinder, sont réservés aux lignes de conversation en dessous. */
.strip-item { flex: none; width: 92px; }
.strip-photo {
  width: 92px;
  height: 122px;
  border-radius: 12px;
  background-size: cover;
  background-position: center top;
}
.strip-name {
  font-size: var(--text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- Liste verticale (D-01 : tous les likes, consensus inclus) ----
   Modèle Tinder (onglet Messages, Plan 05-27) : une ligne par match,
   pastille ronde à gauche, nom + aperçu d'état à droite — remplace la
   grille de tuiles (Gap 5 de la 05-UAT, constat E : « pique la navigation
   de Tinder »). */
/* ---- Écran Messages (gabarit Tinder, captures de Thomas) ----
   Grand titre noir en tête, intitulés de section discrets, rangée
   horizontale de tuiles, puis liste de conversations. */
.screen-title {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.03em;
  padding: var(--space-4) var(--space-1) var(--space-2);
}
.section-label {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: -0.01em;
  padding: var(--space-4) var(--space-1) var(--space-2);
}

/* Ligne de CONVERSATION — avatar rond, nom gras, sous-titre gris. Le
   pastiche est voulu : on joue à discuter avec l'acteur alors qu'on va
   seulement télécharger ses vidéos. */
.match-row .row-photo {
  width: 62px;
  height: 62px;
  border-radius: var(--radius-full);
  flex: none;
  background-size: cover;
  background-position: center top;
}
.match-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-1);
  cursor: pointer;
}
.match-row .row-text { min-width: 0; flex: 1; }
.match-row .row-name {
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.match-row .row-sub {
  margin-top: 2px;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- Grille des matchs (gabarit Tinder / Grindr) ----
   Deux colonnes de vignettes carrées, nom en surimpression basse. Remplace la
   liste de lignes : l'appli sert à choisir un mec pour ce qu'il MONTRE, la
   liste n'en montrait aucun. */
.matches-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
  padding: var(--space-2) 0;
}
.match-tile {
  position: relative;
  aspect-ratio: 3 / 4;      /* portrait, comme les cartes de swipe */
  border-radius: var(--radius-md);
  overflow: hidden;         /* ici c'est voulu : la tuile ne s'anime jamais */
  background: var(--color-surface-2);
  cursor: pointer;
}
.tile-photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center top;
}
/* Voile bas UNIQUEMENT sous le nom : un voile plein cadre ternirait la
   photo, qui est la seule raison d'être de cette tuile. */
.match-tile::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 46%;
  background: linear-gradient(transparent, rgba(0,0,0,0.82));
  pointer-events: none;
}
/* Bloc nom + sous-titre, ancré en bas de la tuile. Une seule ancre pour
   les deux lignes : cf. le commentaire de tileNode (matches.js) sur les
   deux `bottom` absolus qui se recouvraient. */
.tile-info {
  position: absolute;
  left: var(--space-2);
  right: var(--space-2);
  bottom: var(--space-2);
  z-index: 2;
}
.tile-name {
  color: #fff;             /* sur photo : jamais var(--color-text) */
  font-size: var(--text-sm);
  font-weight: 800;
  letter-spacing: -0.01em;
  text-shadow: 0 1px 4px rgba(0,0,0,0.6);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Badge de consensus : la seule info qui mérite de percer la photo. */
.tile-sub {
  color: rgba(255,255,255,0.88);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: -0.005em;
  text-shadow: 0 1px 4px rgba(0,0,0,0.6);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile-badge {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 2;
  min-width: 24px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  background: var(--brand-gradient-flat);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 800;
  text-align: center;
}
.match-tile:hover { transform: scale(1.02); transition: transform 0.15s ease; }

.matches-list {
  display: flex;
  flex-direction: column;
}
/* La ligne ENTIÈRE est la cible tactile — hauteur minimale 64px, largement
   au-dessus des 44px de l'UI-SPEC. Séparateur discret entre lignes, aucune
   carte/ombre (discipline sobre, jamais de relief supplémentaire). */
.match-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 64px;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  cursor: pointer;
}
/* Pastille ronde — 56px, jamais agrandie même pour une vignette basse
   résolution (D-15 : il n'y a plus rien à masquer, la couche floutée
   .tile-blur disparaît avec la grille). */
.row-photo {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background-size: cover;
  background-position: center top;
  background-color: var(--color-surface-2);
}
.row-text {
  flex: 1;
  min-width: 0;
}
.row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Aperçu d'état (contrat G, Plan 05-26) — même rythme qu'un aperçu de
   dernier message Tinder : gris atténué, une ligne avec ellipse. */
.row-sub {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Seul élément de la ligne à porter l'accent (§ Color de l'UI-SPEC) — le
   badge ne montre jamais de prénom, uniquement un nombre (D-02). Reprend
   exactement les couleurs de l'ancien .match-badge. */
.row-badge {
  flex: none;
  font-size: var(--text-xs);
  padding: 2px 6px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-accent);
  color: var(--color-accent);
}

/* ---- Fiche plein écran (Plan 05-27, modèle Tinder — D-04 renoncé au
   profit du modèle Messages) ----
   Glisse depuis la DROITE (left: 100% -> 0), jamais un volet qui remonte du
   bas — c'est le clipping de `.matches-screen` (overflow: hidden, déjà en
   place) qui garantit qu'elle ne déborde jamais avant l'ouverture.

   Plan 05-29 (Gap 6 point J — bandeau de retour trop bas, contenu qui passe
   au-dessus au défilement) : la fiche est une COLONNE FLEX NON DÉFILANTE —
   `.sheet-header` figé (position: static, jamais sticky) + `.detail-scroll`
   qui porte seul le défilement. Le bandeau SORT du flux défilant : il
   devient géométriquement impossible qu'un contenu passe au-dessus ou
   derrière lui. Aucun `env(safe-area-inset-*)` ici — `.shell` l'applique
   déjà une fois pour toute l'appli (une seule occurrence dans ce fichier,
   verrouillée par un test). */
.match-detail {
  position: absolute;
  inset: 0;
  left: 100%;
  background: var(--color-bg);
  z-index: 8;
  transition: left 0.28s ease;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.match-detail.open { left: 0; }
/* Bandeau EN FLUX, jamais `position: sticky` — c'est ce qui rend le
   « contenu qui passe au-dessus au défilement » structurellement
   impossible (Plan 05-29) : le bandeau n'appartient plus au conteneur qui
   défile. Opaque et pleine largeur (`--color-bg`/`--color-border` déjà
   définis dans :root, aucun nouveau jeton). Bouton de RETOUR (pas une croix
   de fermeture) aligné à gauche — `.sheet-header` (partagée avec le volet ⓘ
   du swipe et le volet filtres) aligne par défaut à droite (flex-end) :
   cette surcharge locale referme l'écart pour la seule fiche Matchs. */
.match-detail .sheet-header {
  justify-content: flex-start;
  position: static;
  flex: none;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  padding: 0 var(--space-2);
}
.match-detail .close {
  min-width: 44px;
  min-height: 44px;
  background: transparent;
  border: none;
  color: var(--color-text);
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xl);
}
/* Seule zone défilante de la fiche (Plan 05-29) — `min-height: 0` autorise
   un enfant flex à défiler au lieu de pousser son parent hors écran (même
   piège que `.screen`, ligne ≈ 126). Porte désormais le padding de contenu
   retiré de `.match-detail` ci-dessus. */
.detail-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-4);
}
.sheet-photo {
  height: 200px;
  border-radius: var(--radius-md);
  background-size: cover;
  background-position: center top;
  background-color: var(--color-surface-2);
}

/* ---- Menu discret + confirmation en deux temps (Plan 05-35, Gap 6 point G
   — « changer d'avis » après avoir vu les vignettes de torrent) ----
   Bouton `⋯` SOBRE dans le bandeau de la fiche : aucun accent, AUCUN jeton
   de couleur d'alerte (gate verrouillée) — la protection contre un tap
   accidentel vient de la confirmation en DEUX temps, pas d'une couleur
   criarde (mémoire projet : sobriété, monochrome). `.detail-menu-wrap`
   (position: relative) sert d'ancrage LOCAL au panneau sans jamais toucher
   `position: static` de `.match-detail .sheet-header` (Plan 05-29, gate
   verrouillée, aucune régression du bandeau figé). */
.detail-menu-wrap {
  position: relative;
  margin-left: auto;
}
.detail-menu {
  min-width: 44px;
  min-height: 44px;
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  font-size: var(--text-xl);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* z-index STRICTEMENT inférieur à .photo-viewer (9) et .model-profile (10) —
   de toute façon confiné par construction à la pile locale de .match-detail
   (z-index: 8), qui plafonne tout ce qu'il contient (gate verrouillée par
   comparaison numérique explicite, pas seulement par cette imbrication). */
.detail-actions {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 2;
  min-width: 220px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-2);
  flex-direction: column;
  gap: var(--space-1);
}
.detail-actions.open { display: flex; }
.detail-actions p {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  padding: var(--space-2);
  margin: 0;
}
.detail-actions button {
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: var(--color-text);
  font-size: var(--text-sm);
  padding: var(--space-2);
  min-height: 44px;
  border-radius: var(--radius-md);
}
.detail-actions button:hover { background: var(--color-surface-2); }
.detail-actions button:disabled {
  color: var(--color-text-muted);
  opacity: 0.6;
  cursor: default;
}

/* ---- Lignes torrent ---- */
.torrent-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-6);
}
.torrent-row {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.torrent-title {
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.torrent-meta { font-size: var(--text-xs); color: var(--color-text-muted); }
/* Avertissement sobre à ZÉRO seeder (D-14, Plan 07-04) — écran demandeur
   (matches.js, `.torrent-dead`) et écran admin (admin.js,
   `.admin-row-dead`). AUCUNE couleur d'alerte, aucun fond, aucune bordure :
   le message se lit, il ne crie pas — jamais `--color-danger` (réservée au
   geste « nope », mémoire projet `design-sober-no-flashy`). Ne conditionne
   JAMAIS l'état d'un bouton (D-13 : signalé, jamais bloqué). */
.torrent-dead, .admin-row-dead { font-size: var(--text-xs); color: var(--color-text-muted); }
/* Bouton primaire neutre — mêmes déclarations que .btn-primary, JAMAIS
   l'accent (§ Color de l'UI-SPEC : action neutre, pas une CTA accentuée). */
.torrent-cta {
  background: var(--color-primary);
  color: var(--color-bg);
  border: none;
  border-radius: var(--radius-md);
  min-height: 44px;
  font-weight: 800;
}
/* "Annuler" en fantôme, jamais en rouge (D-11 : action réversible et
   symétrique du rewind swipe, § Color de l'UI-SPEC). */
.btn-ghost {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  border-radius: var(--radius-md);
  min-height: 44px;
}
.btn-ghost:hover { color: var(--color-text); }
.torrent-cta:disabled,
.btn-ghost:disabled {
  color: var(--color-text-muted);
  background: transparent;
  border-color: var(--color-border);
  opacity: 0.6;
  cursor: default;
}
.request-state { font-size: var(--text-sm); color: var(--color-text-muted); }
/* Grisé, jamais l'accent — c'est ce qui le distingue du badge consensus. */
.request-count { font-size: var(--text-xs); color: var(--color-text-muted); }
.torrent-more {
  width: 100%;
  min-height: 44px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  border-radius: var(--radius-md);
}
.torrent-more:hover { color: var(--color-text); }

/* ---- État vide (même rythme calme que .end-screen) ---- */
.matches-empty {
  text-align: center;
  padding: var(--space-8) var(--space-6);
}
.matches-empty h2 { font-size: var(--text-xl); font-weight: 800; }
.matches-empty p { font-size: var(--text-sm); color: var(--color-text-muted); margin-top: var(--space-2); }

/* ===== Libellé non corroboré (Plan 05-19, Gap 4 de la 05-UAT) =====
   Additions strictement additives (aucune ligne retirée, aucun nouveau
   jeton de couleur, aucun usage de --color-success/--color-danger). Le
   deck passe de 40 à 226 cartes OnlyFans servies, dont ~186 sans nom
   corroboré (Plan 05-16) : le libellé de repli (titre de release gaytor)
   devient le cas MAJORITAIRE, pas l'exception — il doit être beau par
   défaut, pas signalé comme une dégradation (discipline UX Tinder : rien
   qui attire l'oeil sur la carte de swipe). Distinction posée par la seule
   typographie : poids normal (jamais le gras du nom), une taille en
   dessous, couleur atténuée (--color-text-muted, déjà défini dans :root
   ci-dessus), une seule ligne avec ellipse. */
.name-release {
  font-weight: 400;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* `.card-a .name` (deux classes, specificité 0,0,2,0) l'emporterait sinon
   sur la règle générique ci-dessus (une seule classe) — ce sélecteur composé
   referme l'écart de spécificité CSS. */
.card-a .name.name-release {
  font-weight: 400;
  font-size: var(--text-2xl);
  color: var(--color-text-muted);
}
/* .row-name/.strip-name portent déjà --text-xs (strip) ou héritent de la
   typographie de liste (row) — aucun ajustement de taille supplémentaire
   (règle du plan : ajustement de taille seulement si la légende est encore
   plus grande que le nom de carte, ce qui n'est pas le cas ici). */
.row-name.name-release,
.strip-name.name-release {
  color: var(--color-text-muted);
}
/* Sur une TUILE, le libellé est posé sur une photo : le gris de
   --color-text-muted y devient illisible (constaté à l'écran). On garde la
   distinction « libellé de release, pas un nom de personne » par la GRAISSE
   et une légère transparence, jamais par une couleur de thème — la règle
   d'affichage T-05-120 est respectée, la lisibilité aussi. */
.tile-name.name-release {
  color: rgba(255,255,255,0.88);
  font-weight: 500;
}
/* Extrait TEL QUEL du <h2> nu du volet détail (Plan 05-06, aucune classe
   jusqu'ici) : aucune déclaration ci-dessous ne change l'apparence actuelle
   d'un nom corroboré — cette classe n'existe que comme point d'accroche pour
   .match-title.name-release. */
.match-title {
  margin: 0;
}
.match-title.name-release {
  font-weight: 400;
  font-size: var(--text-lg);
  color: var(--color-text-muted);
}

/* ===== Vignette de torrent + visionneuse plein écran (Plan 05-23, Gap 5) =====
   Additions strictement additives (aucune ligne retirée, aucun nouveau jeton
   de couleur). Cœur de la demande de Thomas : « pour se faire une idée » —
   vignette généreuse et lisible, visionneuse sobre mais sans retenue. */

/* Même exception de vignette que .strip-photo (72px) — assez grande pour
   juger le torrent d'un coup d'œil dans la liste, sans dominer la ligne. */
.torrent-thumb {
  flex: none;
  width: 72px;
  height: 72px;
  border-radius: var(--radius-md);
  background-size: cover;
  background-position: center top;
}
.torrent-open {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
/* Le curseur pointeur ne se pose que sur une zone RÉELLEMENT activable
   ([role="button"], posé uniquement quand torrent.photos est non vide) —
   jamais sur la classe seule, qui existe aussi sans photo. */
.torrent-open[role="button"] {
  cursor: pointer;
}
.torrent-open .torrent-title {
  flex: 1;
  min-width: 0;
}

/* CR-01 : la visionneuse est montée sur .matches-screen, le MÊME conteneur
   non défilant que .match-detail, et strictement AU-DESSUS de lui
   (z-index: 9 > 8) — se referme sans jamais démonter la fiche en dessous. */
.photo-viewer {
  position: absolute;
  inset: 0;
  z-index: 9;
  background: var(--color-bg);
  overflow: hidden;
}
.viewer-photo {
  position: absolute;
  inset: 0;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
/* Moitiés de tap invisibles gauche/droite — jamais de bord visuel, une
   navigation qui doit se découvrir au toucher, pas s'imposer à l'oeil. */
.viewer-tap {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
}
.viewer-tap.left { left: 0; }
.viewer-tap.right { right: 0; }
.viewer-counter {
  position: absolute;
  bottom: var(--space-4);
  left: 0;
  right: 0;
  text-align: center;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  pointer-events: none;
}
/* Fermeture évidente (contrainte iPhone : jamais piéger l'utilisateur) —
   coin haut-droit, cible tactile 44px, au-dessus des zones de tap
   (z-index local) pour rester cliquable en toutes circonstances. */
.viewer-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 1;
  min-height: 44px;
  min-width: 44px;
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text);
  border-radius: var(--radius-full);
}

/* ===== Profil du modèle en lecture seule (Plan 05-33, Gap 6 point F) =====
   Additions strictement additives (aucune ligne retirée, aucun nouveau
   jeton de couleur, `:root` intouché). Un tap sur la photo ou le nom en
   tête de la fiche Matchs ouvre ce profil — mêmes briques que le volet ⓘ du
   swipe (deck.js : normalizePhotos/displayName/infoHTML), mais AUCUN
   bouton ni geste de swipe (modelProfileNode, T-05-203). Même géométrie et
   même transition que .match-detail, à un z-index STRICTEMENT supérieur
   (10 > 9 > 8, superposition fiche/visionneuse/profil) — se referme sur la
   fiche du match, jamais sur la liste ni l'écran de swipe. */
.model-profile {
  position: absolute;
  inset: 0;
  left: 100%;
  background: var(--color-bg);
  z-index: 10;
  transition: left 0.28s ease;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.model-profile.open { left: 0; }
/* Même surcharge que `.match-detail .sheet-header` (Plan 05-27/05-29) :
   bouton retour aligné à gauche, bandeau EN FLUX (jamais sticky), opaque et
   pleine largeur — reconduit ici pour la même raison, jamais un sticky qui
   redeviendrait tributaire du bloc conteneur. */
.model-profile .sheet-header {
  justify-content: flex-start;
  position: static;
  flex: none;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  padding: 0 var(--space-2);
}
/* Bouton retour du profil (correctif 2026-08-02, retour Thomas : « le
   bouton fléche retour sur le profil est trop petit ») — AUCUNE règle
   `.model-profile .close` n'existait avant ce correctif : seuls
   `.card-a .sheet .close`/`.filter-sheet .close` et `.match-detail .close`
   étaient définis, aucun des deux ne matchait ce sélecteur ; le bouton
   retombait donc à la taille par défaut du navigateur. Reprend
   `.match-detail .close` À L'IDENTIQUE (même bandeau `.sheet-header`, même
   bouton chevron de retour, même référence déjà validée) : cible tactile
   44×44 minimum (iPhone), même taille de glyphe (`--text-xl`). */
.model-profile .close {
  min-width: 44px;
  min-height: 44px;
  background: transparent;
  border: none;
  color: var(--color-text);
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xl);
}
/* Photo du profil (correctif 2026-08-02, retour Thomas : « pourquoi sur les
   profils on ne peut plus voir les photos en profil en entier ? c'est
   absurde »). Rejet explicite du choix initial : `background-size: cover`
   dans une boîte de hauteur FIXE et courte (320px) rogne systématiquement
   la photo pour remplir la boîte — quel que soit son cadrage d'origine.
   `contain` ne rogne JAMAIS le sujet : au pire il ajoute des bandes vides
   (`background-color`, même repli que `.viewer-photo`/`applyPhoto`) sur les
   côtés courts. `aspect-ratio: 3 / 4` (portrait, format dominant des photos
   de modèles) remplace la hauteur fixe : la boîte reste nettement plus
   généreuse ET proportionnelle à la largeur réelle de l'écran, sans jamais
   introduire d'unité `vh` (mémoire projet : 100vh est peu fiable en PWA iOS,
   cf. commentaire de `:root` plus haut — ce piège ne concerne normalement
   que la hauteur de l'app entière, mais autant ne jamais l'introduire ici
   non plus). `background-position: center` (plus `center top`, qui n'a de
   sens que pour un cadrage `cover` qui rogne le bas) : `position: relative`
   inchangée, sert toujours d'ancrage au carrousel imbriqué (.segs/.tap)
   ci-dessous. `cursor: pointer` UNIQUEMENT quand `modelProfileNode`
   (deck.js) a effectivement posé `role="button"` (photo exploitable +
   `onOpenPhotos` fourni) — jamais un curseur qui promet un tap mort. */
.profile-photo {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  background-color: var(--color-surface-2);
}
.profile-photo[role="button"] {
  cursor: pointer;
}
/* Reprend LES VALEURS de `.card-a .segs`/`.card-a .tap` à l'identique —
   sans jamais toucher ces sélecteurs eux-mêmes (non-régression de l'écran
   de swipe) — imbriqués ici dans `.profile-photo` (positionné) plutôt que
   dans `.card-a` (plein écran). */
.model-profile .segs {
  position: absolute;
  top: 10px;
  left: 12px;
  right: 12px;
  display: flex;
  gap: 4px;
}
.model-profile .seg {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: rgba(255,255,255,0.3);
  transition: background 0.15s;
}
.model-profile .seg.on { background: #fff; }
.model-profile .tap {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 33%;
}
.model-profile .tap.left { left: 0; }
.model-profile .tap.right { right: 0; }
/* Visionneuse plein écran ouverte DEPUIS le profil (tap sur .profile-photo,
   correctif 2026-08-02) : réutilise `.photo-viewer` TEL QUEL (Plan 05-23,
   z-index 9, gate verrouillée par les tests de superposition — INCHANGÉE
   ci-dessus), montée par `matches.js` (`openProfilePhotoViewer`) comme
   ENFANT DIRECT de `.model-profile` — JAMAIS comme frère sur
   `.matches-screen` (montage de la visionneuse des torrents), qui la
   laisserait SOUS le profil (9 < 10). Empilement traité EXPLICITEMENT, rien
   laissé au hasard : `.model-profile` (position: absolute + z-index: 10)
   crée son propre contexte d'empilement local dans lequel `.photo-viewer`
   peindrait déjà naturellement au-dessus de son bandeau/zone défilante (non
   positionnés) du seul fait de l'imbrication — cette classe dédiée
   (posée par `openProfilePhotoViewer`, jamais par `openPhotos()`) fixe en
   plus un z-index local explicite, pour ne dépendre d'aucune évolution
   future de ce bandeau. */
.photo-viewer.photo-viewer-in-profile {
  z-index: 11;
}
/* Rythme vertical uniquement — aucune redéfinition de taille/poids propre,
   `.name-release` (Plan 05-19) porte déjà la seule distinction visuelle. */
.profile-name {
  margin: var(--space-3) 0 0;
}
.profile-info {
  margin-top: var(--space-2);
}

/* ===== Écran Potes (Plan 08-01, SOC-01) =====
   Additions strictement additives : aucun jeton nouveau, aucune ligne
   existante retirée/modifiée — copie exacte de .matches-screen/
   .matches-scroll (même discipline CR-01 : la fiche plein écran du
   Plan 08-02 vivra sur .potes-screen, NON défilant ; le contenu réel vit
   dans .potes-scroll). .id-grid (déjà posée ci-dessus) est réutilisée
   telle quelle pour la grille de stats — aucune duplication. */
.potes-screen {
  position: absolute;
  inset: 0;
  overflow: hidden;
  padding: 0;
}
.potes-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  padding: var(--space-4);
}
/* Onglet Potes, gabarit de l'onglet « Likes » de Tinder : grand titre
   d'écran, puis des sections intitulées portant chacune une grille de
   vignettes à deux colonnes. .likes-grid reprend .matches-grid a
   l'identique (même 2 colonnes, même gouttière) SANS la dupliquer en
   valeurs : les deux grilles sont le même objet, elles doivent bouger
   ensemble. */
.likes-section { padding-bottom: var(--space-2); }
/* RANGÉE qui défile, pas une grille (demande de Thomas). Même patron que
   `.strip-scroll` (bande « Vos goûts se croisent » de l'onglet Matchs) :
   flex + overflow-x, les vignettes en `flex: none`.

   Les marges négatives font saigner la rangée jusqu'aux bords de l'écran en
   annulant le padding de `.potes-scroll`, et le padding interne le rétablit
   à l'intérieur : c'est ce qui fait qu'une vignette est coupée au bord et
   qu'on VOIT qu'il y a une suite. Sans ça, la rangée s'arrête proprement à
   16 px du bord et ne dit pas qu'elle défile. */
.likes-rail {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding: var(--space-2) var(--space-4);
  margin: 0 calc(var(--space-4) * -1);
  scroll-snap-type: x proximity;
  scrollbar-width: none;              /* Firefox */
  -webkit-overflow-scrolling: touch;  /* inertie iOS */
}
.likes-rail::-webkit-scrollbar { display: none; }
.likes-rail .match-tile {
  flex: none;
  width: 144px;
  scroll-snap-align: start;
}

/* ===== Écran Potes — état groupe (Plan 08-02, D-02/D-03/D-05/D-06) =====
   Additions strictement additives : aucun jeton nouveau, aucune ligne
   ci-dessus retirée/modifiée. `.pote-row` reprend le LAYOUT de
   `.match-row` (ci-dessus) SANS sa règle de curseur — une ligne de
   classement est INERTE (§ Interaction Notes, 08-UI-SPEC.md), elle n'ouvre
   rien, contrairement à `.match-row` qu'utilise `rowNode()` pour la liste
   de consensus (réutilisée littéralement, aucune règle CSS neuve pour
   elle). `.title-card` reprend les déclarations de `.admin-row`
   (ci-dessous). Aucune couleur d'accent, aucune médaille : le rang reste
   un numéral gris identique du premier au dernier (§ Color). */
.pote-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 64px;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.pote-rank {
  flex: none;
  width: 20px;
  text-align: right;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}
.pote-avatar {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background: var(--color-surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: var(--text-xl);
  color: var(--color-text-muted);
}
.pote-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pote-meta {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Gap ENTRE cartes empilées (Spacing Scale, 08-UI-SPEC.md) : --space-2, via
   margin-bottom — jamais un conteneur flex additionnel (le parent reste un
   simple bloc, comme `.admin-row` sur `.admin-section-list`). Gap INTERNE
   (eyebrow -> nom -> chiffre) : --space-1, plus resserré. */
.title-card {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin-bottom: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.title-card-label {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.title-card-name {
  font-size: var(--text-xl);
  font-weight: 800;
  color: var(--color-text);
  /* UI long-text/E3 (08-UI-SPEC.md ligne 24) : tronqué sur une ligne avec
     ellipse, même règle que .row-name — un identifiant Jellyfin long ne
     doit jamais faire déborder la carte. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.title-card-stat {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  /* Punchline potache (checkpoint Plan 08-02) : plus longue qu'un simple
     chiffre, elle peut légitimement passer sur 2 lignes — PAS d'ellipse
     ici, contrairement à .title-card-name (un nom) et .pote-meta (une
     ligne de compteurs) qui restent volontairement sur une seule ligne. */
}

/* ===== Écran admin de validation (Plan 06-03, D-01/D-12/D-13) =====
   Additions strictement additives (aucune ligne retirée/modifiée). Ouvert
   depuis l'icône discrète de la topbar, sur TOUS les onglets (admin.js) —
   monté directement dans `.shell` (frère de `.topbar`/`.tabbar`), jamais
   nesté dans un `.screen` particulier, pour rester ouvrable quel que soit
   l'onglet actif. Placé au-dessus de la barre du haut (`.topbar` n'a aucun
   z-index propre) mais SOUS la bannière d'erreurs globale (`#banner`,
   z-index 20, montée en dehors de `.shell` dans index.html) : un incident
   reste lisible pendant une validation. z-index 10, même valeur que
   `.tabbar` ci-dessus — sans collision : `.admin-screen` est monté APRÈS
   `.tabbar` dans le DOM (admin.js l'ajoute à `.shell` à l'ouverture), donc
   l'égalité de z-index se départage par l'ordre du DOM, qui le place déjà
   au-dessus. Même géométrie/transition que `.match-detail`/`.model-profile`
   (glisse depuis la droite). */
.admin-screen {
  position: absolute;
  inset: 0;
  left: 100%;
  background: var(--color-bg);
  z-index: 10;
  transition: left 0.28s ease;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.admin-screen.open { left: 0; }
/* Bandeau EN FLUX (jamais sticky), reprend `.match-detail .sheet-header` À
   L'IDENTIQUE (Plan 05-29 : structurellement impossible qu'un contenu passe
   au-dessus au défilement). */
.admin-screen .sheet-header {
  justify-content: flex-start;
  position: static;
  flex: none;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  padding: 0 var(--space-2);
}
/* Bouton retour — reprend `.match-detail .close` À L'IDENTIQUE (cible
   tactile 44×44 minimum). */
.admin-screen .close {
  min-width: 44px;
  min-height: 44px;
  background: transparent;
  border: none;
  color: var(--color-text);
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xl);
}
.admin-title {
  font-size: var(--text-sm);
  font-weight: 800;
  margin-left: var(--space-2);
}
/* Ligne d'état du disque (D-12/D-13) : sobre, PAS d'accent. `.low` (seule
   classe de couleur ajoutée par ce plan, gate verrouillée par grep) change
   UNIQUEMENT la couleur du texte — jamais un bouton désactivé, jamais un
   blocage : information seule (D-13, décision informée de Thomas, jamais un
   garde « par prudence »). */
.admin-disk {
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
}
.admin-disk.low { color: var(--color-danger); }
.admin-empty {
  padding: var(--space-6);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}
/* Une ligne de la file (D-02) : photo + nom/poids/consensus sur une ligne,
   titre du torrent tronqué sur une seconde — même rythme que `.torrent-row`
   (matches.js), jamais de couleur d'accent. */
.admin-row {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin: 0 var(--space-4) var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.admin-row:first-child { margin-top: var(--space-3); }
.admin-row-photo {
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background-size: cover;
  background-position: center top;
  background-color: var(--color-surface-2);
}
.admin-row-text { flex: 1; min-width: 0; }
.admin-row-meta { font-size: var(--text-sm); }
/* Grisé, jamais l'accent — même discipline que `.request-count` de
   matches.js (D-13/§ Color : aucune couleur d'accent hors de la ligne
   disque dans cet écran). */
.admin-row-badge {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.admin-row-title {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* « Voir les photos » — volontairement DISCRET et hors de la rangée
   Valider/Refuser : c'est une aide à la décision, pas une décision. Un
   bouton plein à côté des deux CTA inviterait au clic de travers. */
.admin-row-photos {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-1);
  padding: 4px 10px 4px 6px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  min-height: 32px;
}
.admin-row-photos svg { width: 16px; height: 16px; flex: none; }
.admin-row-photos:hover { color: var(--color-text); border-color: var(--color-text-muted); }

.admin-row-actions {
  flex: none;
  display: flex;
  gap: var(--space-2);
  width: 100%;
}
.admin-row-actions .torrent-cta,
.admin-row-actions .btn-ghost {
  flex: 1;
}

/* ===== Plan 06-04 : trois sections, marqueur auto, mention d'espace
   insuffisant, pastille de comptage — additions strictement additives
   (aucune ligne ci-dessus retirée/modifiée). Palette sobre : AUCUNE
   couleur d'accent hors des deux seuls signaux d'alerte du fichier (la
   ligne disque sous seuil, `.admin-disk.low` déjà posée ci-dessus, et la
   mention d'espace insuffisant `.admin-row-space-warning` ci-dessous). */
.admin-section-title {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  padding: var(--space-3) var(--space-4) var(--space-1);
  margin: 0;
}
/* Marqueur discret « auto » (D-07) — section « en cours » uniquement,
   même gris que `.admin-row-badge`/`.admin-row-title`, jamais l'accent. */
.admin-row-auto {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  padding: 0 var(--space-2);
  margin-left: var(--space-2);
}
/* Libellé d'état terminal (section « traitées ») — sobre, pas un bouton. */
.admin-row-state {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
/* Mention d'espace insuffisant (D-13) : seul second signal d'alerte de cet
   écran avec `.admin-disk.low` — un span de texte, RIEN d'autre. Ne
   conditionne jamais l'affichage/l'état des boutons (posés sans condition
   liée au disque dans admin.js). */
.admin-row-space-warning {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-danger);
}
/* Pastille de comptage sur l'icône admin de la topbar (D-09) — sobre,
   AUCUNE couleur d'accent (surface neutre + bordure), jamais un badge
   rouge/bleu qui donnerait à ce chiffre l'air d'une alerte. */
#admin-btn { position: relative; }
#admin-btn .badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-full);
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  font-size: 10px;
  line-height: 14px;
  text-align: center;
}
/* ❤️ désactivé à l'épuisement du quota de likes (D-15/D-18, Plan 06-05) —
   sobre, jamais une couleur d'alerte : un simple retrait visuel (opacité +
   perte du hover), le message qui explique pourquoi vit dans la bannière
   (LIKE_QUOTA_MESSAGE, deck.js), pas dans une couleur criarde ici. `.act-nope`
   n'a JAMAIS cette règle — le passage n'est jamais désactivé (D-18). */
.act-like:disabled {
  opacity: 0.35;
  cursor: default;
}
.act-like:disabled:hover {
  background: rgba(14,16,20,0.75);
  color: var(--color-success);
  transform: none;
}

/* ===== Quick task 260806-dqv : écran des faux profils de potes.
   Réutilise au maximum `.admin-*` (écran plein, en-tête, lignes, boutons)
   et `.field`/`.btn-primary` (formulaire) — seuls ajouts réels : la
   vignette de ligne (déjà couverte par `.admin-row-photo`, aucune classe
   de plus nécessaire) et les champs du formulaire (`.pote-*`). Sobre et
   monochrome, dans la continuité de l'existant — aucune couleur d'accent. */
.pote-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.pote-field textarea {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  color: var(--color-text);
  font-size: 16px; /* sous 16px, iOS Safari zoome au focus (même contrainte que .field input) */
  min-height: 72px;
  resize: vertical;
}
.pote-field input[type="file"] {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
}
/* Quick task 260806-iks (D-01) : bouton « Générer » à droite du libellé
   (Bio/Punchline) — ligne horizontale, alignement vertical centré, sobre et
   monochrome (mêmes variables que .btn-ghost). */
.pote-field-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.pote-generate {
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
}
