/* ============================================================
   KINET — v2.0
   Refonte visuelle complète ("table rase") : palette violet/or (plus
   d'ambre unique surchargé pour tout — voir plus bas), Bricolage
   Grotesque/Instrument Sans/Space Mono, grille d'affiches pour les
   listes de films/séries (bascule grille/liste, js/ui.js), le
   "cadran" comme signature de la note (remplace l'ancien rectangle
   plat .counter). Sprockets/pellicule retirés : motif directement lié
   à l'ancien thème cinéma que cette version abandonne.
   ============================================================ */

/* La police est chargée depuis <head> (index.html, <link rel="stylesheet">)
   plutôt qu'un @import ici (retour utilisateur : "Chargement plus rapide au
   démarrage") — un @import en tête de feuille de style force le navigateur
   à d'abord télécharger ET PARSER style.css avant même de découvrir qu'il
   lui faut aussi cette police, alors qu'un <link> dans <head> part en
   parallèle dès le tout début du chargement de la page. */

:root{
  /* ==========================================================
     CODE DE DESIGN (docs/code-design.md, audit du 14/10/2026).
     Seules ces variables font foi. Les anciennes (--violet, --gold,
     --teal, --bronze, --plum...) plus bas ne sont plus que des alias
     vers elles, gardés pour ne rien casser.
     ========================================================== */
  --font-display:'Bricolage Grotesque', "Helvetica Neue", Arial, sans-serif;
  --font-text:'Instrument Sans', "Helvetica Neue", Arial, sans-serif;
  --fs-xs:11px; --fs-sm:13px; --fs-md:15px; --fs-lg:20px; --fs-xl:28px;
  --fs-2xl:clamp(32px, 5vw, 48px);
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px; --sp-7:48px; --sp-8:64px;
  --r-sm:8px; --r-md:14px; --r-full:999px;
  --dur-press:100ms; --dur-fast:150ms; --dur-base:220ms; --dur-slow:320ms;
  --ease-out:cubic-bezier(.2,.8,.2,1); --ease-in-out:cubic-bezier(.65,0,.35,1);
  --text-1:#f4f3ee; --text-2:#9a9993; --text-3:#6b6a66;
  --line-strong:rgba(244,243,238,0.18);
  --accent:#4fcfff; --accent-ink:#06151c; --accent-wash:rgba(79,207,255,0.12);
  /* Note : couleur de DONNÉE, uniquement sur une note. */
  --note-low:#ff6b57; --note-mid:#e8a33d; --note-mid-dim:#a66f1f; --note-mid-wash:rgba(232,163,61,0.1); --note-high:#6ad18b;
  /* Rareté des cartes (Cartes uniquement), même statut que la note. */
  --rarity-legendary:#e8a33d; --rarity-legendary-dim:#a66f1f; --rarity-legendary-wash:rgba(232,163,61,0.16);
  --rarity-rare:#6fa39f; --rarity-rare-wash:rgba(111,163,159,0.14);
  --shadow-float:0 16px 40px rgba(0,0,0,0.45);
  /* ==========================================================
     KINET — v3.0 "Scope"
     Retour utilisateur (long échange) : la DA v2.x "lisait IA" — violet
     comme couleur primaire, aurore en dégradés animés plein écran, verre
     dépoli partout. Palette table rase, validée sur draft séparée
     ("Kinet Scope") avant d'atterrir ici : noir quasi neutre (pas de
     teinte violette dans les gris), UN SEUL accent interactif (bleu
     glace — remplace le violet partout, même rôle exact), UN SEUL accent
     de suivi personnel (ambre — remplace l'or, même rôle). Les couleurs
     sémantiques du cadran (rouge/vert) et les accents fonctionnels
     étroits (teal, bronze) restent : ce n'était pas le problème signalé.
     ========================================================== */
  /* --ink : texte posé SUR une surface colorée (bouton glace/ambre) —
     toujours sombre dans les deux thèmes. --ink-surface : le vrai fond
     sombre, s'éclaircit en thème clair (voir [data-theme="light"]). */
  --ink:#0a0a0a;
  --ink-surface:#0a0a0a;
  --surface:#161616;
  --surface-2:#1f1f1f;
  --surface-3:#262626;
  --line:#2a2a2a;
  --line-soft:#1c1c1c;
  --text:var(--text-1);
  --text-muted:var(--text-2);
  /* Glace : tout ce qui est interactif (boutons, liens, focus, actif) —
     seul rôle, comme l'ambre juste en dessous : une couleur, un métier. */
  --violet:var(--accent);
  --violet-dim:#1f6f8f;
  --violet-wash:var(--accent-wash);
  /* Ambre : réservé au suivi personnel (ce que vous regardez, votre
     progression) — jamais décoratif. Le cadran (.counter) garde son
     propre barème rouge/jaune/vert juste en dessous, indépendant. */
  --gold:var(--accent);
  --gold-dim:var(--violet-dim);
  --gold-wash:var(--accent-wash);
  /* Teal conservé pour son rôle informationnel étroit (badge
     manuel/revisionnage, aide, succès) — jamais remis en cause, ce
     n'était pas un ton violet. */
  --teal:var(--text-2);
  --teal-wash:rgba(var(--text-rgb),0.06);
  --coral:var(--note-low);
  --coral-dim:#8a3f34;
  /* Barème du cadran (.counter, noteColorClass() dans js/ui.js) : rouge
     <2.5, or 2.5-3.99 (réutilise --gold ci-dessus), vert >=4. */
  --green:var(--note-high);
  --green-dim:#3f7a52;
  --green-wash:rgba(106,209,139,0.1);
  /* Bronze conservé (accent rare, succès/mise en avant) — ton chaud déjà
     voisin de l'ambre, pas un dérivé du violet, donc inchangé. Plum
     repointé sur des gris neutres : c'était "un bloc violet profond et
     saturé" par design (v2.7) — le concept de bloc plein reste utile
     (.top-films-showcase, .ach-summary) mais plus en violet. */
  --bronze:var(--text-2);
  --bronze-dim:var(--line-strong);
  --bronze-wash:rgba(var(--text-rgb),0.06);
  --plum:var(--surface);
  --plum-2:var(--surface-2);
  /* Hauteur de #mobileTabbar (v2.1.x) — référencée par elle-même et par
     tout ce qui doit se décaler pour ne pas passer dessous une fois
     qu'elle occupe le bas de l'écran (mobile only) : .fab,
     .update-banner, le padding bas des pages. Un seul chiffre à changer
     si la barre grandit un jour. */
  --mobile-tabbar-h:60px;
  /* Triplet RGB (pas une couleur complète) pour les panneaux "vitrés" avec
     backdrop-filter:blur (menu déroulant, .modal, barre mobile) — chacun
     compose son propre rgba(var(--glass-rgb), alpha) avec SON alpha déjà
     réglé (0.72/0.74/0.86 selon le composant), seule la teinte de base
     change de thème. Thème clair en option (retour utilisateur) : sans ce
     découplage, ces 3 panneaux resteraient un verre sombre même en thème
     clair — leur texte (var(--text), lui bien réassigné) y deviendrait
     illisible (texte sombre sur verre sombre). */
  --glass-rgb:10,10,10;
  /* Triplet RGB de --text (même logique que --glass-rgb ci-dessus) — pour
     un liseré "toujours la couleur du texte, très estompée" sur les
     panneaux vitrés (menu déroulant, .modal) : s'inverse correctement tout
     seul avec le thème plutôt que de rester un blanc fixe qui disparaîtrait
     en thème clair. */
  --text-rgb:244,243,238;
}
/* Thème clair en option (retour utilisateur) — sombre par défaut, activé
   par [data-theme="light"] sur <html> (voir setTheme(), js/ui.js). Pas de
   variante "suit le système" : demandé comme un simple bouton, pas une
   préférence automatique — l'attribut n'existe que si quelqu'un l'a
   explicitement choisi, jamais posé par défaut.
   Seuls les tokens qui échoueraient tels quels sont redéfinis ici :
   - --ink reste identique dans les deux thèmes (texte posé sur une
     surface glace/ambre, qui elle-même ne change pas de teinte de base
     entre thèmes — voir le commentaire sur --ink dans :root).
   - --plum/--plum-2 restent identiques aussi (gris neutres désormais,
     plus un bloc violet) : un bloc sombre plein reste lisible et voulu
     quel que soit le thème du reste de la page, comme une carte à
     contraste inversé assumée plutôt qu'une surface neutre.
   - Les accents (glace/ambre/teal/corail/vert/bronze) sont RE-TEINTÉS
     (pas juste réutilisés) : leurs valeurs sombres sont choisies pour
     ressortir sur un fond quasi noir — utilisées telles quelles comme
     TEXTE sur un fond clair, elles échoueraient le contraste. Assombris
     ici pour rester lisibles en texte ET rester de vrais boutons pleins
     avec du texte blanc/--ink par-dessus — même logique de teinte,
     ajustée en clarté (le glace #4fcfff devient un bleu nuit profond). */
:root[data-theme="light"]{
  --ink-surface:#f4f2ec;
  --surface:#faf9f5;
  --surface-2:#f0efe9;
  --surface-3:#e5e3db;
  --line:#dddad1;
  --line-soft:#e9e7e0;
  --text-1:#1c1c1a;
  --text-2:#5e5d58;
  --text-3:#8a8983;
  --line-strong:rgba(28,28,26,0.22);
  --accent:#0e6f8f;
  --accent-ink:#ffffff;
  --violet-dim:#0a4f66;
  --accent-wash:rgba(14,111,143,0.1);
  --note-mid:#9c6a1f;
  --rarity-legendary:#9c6a1f;
  --note-mid-dim:#6b4913;
  --rarity-legendary-dim:#6b4913;
  --note-mid-wash:rgba(156,106,31,0.12);
  --rarity-legendary-wash:rgba(156,106,31,0.14);
  --rarity-rare:#3f7d78;
  --rarity-rare-wash:rgba(63,125,120,0.14);
  --note-low:#c73f2c;
  --coral-dim:#8a3f34;
  --note-high:#2f9155;
  --green-dim:#3f7a52;
  --green-wash:rgba(47,145,85,0.1);
  --glass-rgb:250,249,245;
  --text-rgb:28,28,26;
}
/* Fond de page en thème clair : même recette que le fond sombre plus bas
   (mêmes taches de couleur, même grille de points) — pas une composition
   différente, juste ses deux valeurs littérales (non tokenisées, seule
   variable ici est l'union des rgba() eux-mêmes) adaptées : le point de
   grille passe de clair-sur-sombre à sombre-sur-clair (sinon invisible sur
   ce fond), --ink final remplacé par --ink-surface (déjà réassignée
   au-dessus). Les taches violet/or/prune/bronze gardent LEURS mêmes rgba —
   elles rendent en lavis pastel sur un fond clair plutôt qu'en lueur
   saturée sur fond sombre, cohérent avec la même identité, pas une
   composition à redessiner. */
:root[data-theme="light"] body{
  background:var(--ink-surface);
  /* Le raccourci background: ci-dessus réinitialise TOUS ses sous-attributs
     (position/attachment/repeat) à leur valeur initiale s'ils ne sont pas
     redonnés ici — sans cette ligne, le fond perdrait son attachement fixe
     (body{}, plus bas dans ce fichier) une fois le thème clair activé,
     redevenant un fond qui défile avec le contenu. */
  background-attachment:fixed;
}
*{box-sizing:border-box; margin:0; padding:0;}
/* Code de design : les contrôles de formulaire héritent de la police de la
   page (sinon le navigateur les met en Arial 13.33px, audit D1), et le
   texte sans taille explicite prend la taille de lecture. */
button, input, select, textarea{font-family:inherit;}
body{font-size:var(--fs-md);}
/* Fond de page : le violet du header seul ne suffisait pas (retour
   utilisateur v2.0 — "le fond reste plat/vide dès qu'on scrolle") — deux
   grandes taches très douces, fixées à l'écran plutôt qu'au document, pour
   garder une vraie atmosphère quelle que soit la longueur de la page
   plutôt qu'un dégradé qui ne couvrirait que le haut. Discret exprès (le
   cadran reste la seule vraie signature) : jamais assez marqué pour gêner
   la lecture d'une liste par-dessus. */
/* Grille de points (v2.0.8) : jusqu'ici réservée à l'écran de connexion
   (.auth-pattern) — étendue à TOUTE l'app en toile de fond commune, très
   discrète (0.04 d'opacité, sous les deux taches de lumière), pour que
   l'identité posée sur l'écran de connexion se retrouve partout plutôt
   que de s'arrêter à la porte. .auth-pattern reste par-dessus pour son
   halo plus marqué propre à cet écran (voir plus bas) — pas de doublon
   visuel gênant, elle vient juste renforcer ce que le corps de page fait
   déjà partout. */
/* Aurore retirée (v3.0, retour utilisateur — "ça fait IA" : dégradés
   colorés animés en fond plein écran, justement le genre de signature
   qu'on cherche à ne PAS avoir). Fond redescendu à un noir quasi neutre
   uni + le même quadrillage de points très discret qu'avant (texture,
   pas lumière) — plus aucune animation de fond, prefers-reduced-motion
   n'a donc plus rien à couper ici. */
/* Réduire les animations (retour utilisateur, Ton profil → Paramètres →
   Accessibilité) : un réglage DANS Kinet, pour qui préfère l'activer ici
   plutôt que dans les réglages de son système — voir setReduceMotion(),
   js/ui.js. Coupe tout à la racine (durées quasi nulles, !important)
   plutôt que de dupliquer chacune des règles @media
   (prefers-reduced-motion:reduce) déjà écrites dans ce fichier pour le
   réglage système : les deux mènent au même résultat par des chemins
   différents, sans avoir à maintenir la logique à deux endroits. */
html.reduce-motion *, html.reduce-motion *::before, html.reduce-motion *::after{
  animation-duration:0.001ms !important;
  animation-iteration-count:1 !important;
  transition-duration:0.001ms !important;
  scroll-behavior:auto !important;
}

/* Verrou de défilement du fond pendant une modale/le tuto (retour
   utilisateur : "pendant le tuto je peux quand même scroll") — posé/retiré
   par lockBodyScroll()/unlockBodyScroll() (js/ui.js). Sur html ET body :
   certains navigateurs mobiles ignorent overflow:hidden posé sur un seul
   des deux. */
html.scroll-locked, html.scroll-locked body{overflow:hidden;}

/* Base "se sent installée, pas juste ouverte dans un onglet" (audit design,
   skill mobile-native) — chacune de ces lignes corrige un signal précis
   qui trahit un site web plutôt qu'une app :
   - tap-highlight : le flash gris/bleu qu'iOS/Android posent sur tout élément
     cliquable au toucher, jamais vu dans une vraie app, jamais désactivé
     ici avant — chaque élément tactile a déjà son propre retour (:active,
     voir .btn plus bas), inutile de garder celui du navigateur par-dessus.
   - overscroll-behavior:none sur html/body : empêche le rebond natif du
     navigateur ET le pull-to-refresh natif d'Android Chrome de se
     déclencher EN PLUS du geste tirer-pour-rafraîchir déjà posé à la main
     (js/ui.js) — sans ça, les deux peuvent se chevaucher. */
html{
  -webkit-tap-highlight-color:transparent;
  -webkit-text-size-adjust:100%;
  overscroll-behavior:none;
}
body{overscroll-behavior:none;}

body{
  background:var(--ink);
  background-attachment:fixed;
  color:var(--text);
  font-family:var(--font-text);
  /* 100vh = la plus GRANDE hauteur de viewport mobile (barre d'adresse
     masquée) — au premier affichage, cette barre est visible, donc 100vh
     dépasse la zone réellement visible (audit design, skill mobile-native).
     100dvh suit la zone visible en direct ; 100vh reste en repli pour les
     navigateurs qui ne connaissent pas encore dvh. */
  min-height:100vh;
  min-height:100dvh;
  padding:0 0 80px 0;
  position:relative;
}
/* Grain argentique (v2.7) — texture fine sur tout le fond, comme une
   pellicule plutôt qu'un filtre : se sent plus qu'il ne se voit (2.5%
   d'opacité). Fixed + pointer-events:none, jamais devant le contenu
   interactif ni les affiches (qui ont déjà leur propre grain). */
body::after{
  content:'';
  position:fixed; inset:0;
  pointer-events:none;
  z-index:2000;
  opacity:0.025;
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Entête collante (v3.0, retour utilisateur : "c'était pas du tout comme
   ça" sur la draft Scope — l'entête défilait avec le contenu avant,
   comme un simple bloc de page plutôt qu'un vrai chrome d'app). Fondu en
   dégradé en bas plutôt qu'un trait dur (skill Apple, "scroll edge
   effects, not hard dividers") : le contenu disparaît sous la barre au
   lieu d'être coupé net. env(safe-area-inset-top) : la barre doit rester
   sous l'encoche, pas à ras du bord. */
header{
  position:sticky; top:env(safe-area-inset-top, 0px); z-index:50;
  padding:20px 32px 22px;
  margin-bottom:32px;
  overflow:visible;
  background:linear-gradient(to bottom, rgba(var(--glass-rgb),0.97) 70%, rgba(var(--glass-rgb),0) 100%);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
}
/* Tache "atmosphère" (v3.0) retirée en v3.2 — retour utilisateur : posée
   sur .title-row (une boîte qui ne fait que la largeur de "Kinet v2.0",
   flex shrink-to-fit), le dégradé radial de 560x240px n'avait pas la
   place de s'estomper : seul son centre quasi opaque restait visible,
   un bloc plein avec un bord net plutôt qu'une lueur douce. Elle n'avait
   de sens que sur la largeur du header entier (voir l'historique git) —
   pas réessayée ici, purement décorative, pas une perte fonctionnelle. */
.title-row{
  display:flex;
  align-items:flex-end;
  gap:10px;
}
.brand-link{
  display:flex;
  align-items:center;
  gap:11px;
  cursor:pointer;
  transition:opacity .15s;
}
.brand-link:hover{opacity:0.82;}
.brand-link:focus-visible{outline:2px solid var(--violet-dim); outline-offset:4px; border-radius:var(--r-sm);}
/* Ligne du haut (v2.1.x, retour utilisateur — "l'organisation entre
   icône, Kinet et série/film") : marque (.title-row) et rangée
   utilitaire (#userBar) COTE À COTE — la marque dit "qui/où", la rangée
   utilitaire + le profil disent "où aller" ; #primaryTabs (juste après
   dans le DOM) garde sa propre ligne, au plus près du contenu qu'il
   commande. Voir le commentaire complet dans index.html. */
.header-top{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
  flex-wrap:wrap;
}
/* #userBar n'a plus besoin d'écarter ses propres enfants (nav + profil) :
   .header-top ci-dessus s'en charge déjà à un niveau au-dessus, il ne
   reste qu'à les tenir groupés et serrés. */
.user-bar{
  display:flex;
  align-items:center;
  gap:16px;
  flex-wrap:wrap;
  position:relative;
}
/* Nav de l'entête : les pages assez fréquentes pour mériter un accès
   direct depuis n'importe où (À voir, Séries, Prochainement, Amis — qui
   contient Groupes, voir index.html). Groupée dans une seule pilule
   (v3.0, draft Scope) plutôt que des boutons flottant librement : même
   logique que la nav du haut de la draft (fond surface, bord hairline,
   radius 999px, 4px de marge intérieure) — chaque bouton garde SA propre
   pilule (999px) à l'intérieur, imbriquée plutôt qu'alignée à plat. */
.header-nav{
  display:flex;
  gap:2px;
  flex-wrap:wrap;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-full);
  padding:4px;
}
/* v3.0 (draft Scope, "respecte cette page à la lettre") — .topnav a dans
   la draft est du texte nu, sans icône ni pastille : pas de fond/bord au
   repos, juste la couleur qui change, un radius:999px qui ne sert qu'à
   l'état actif (plein) et au survol. Les .nav-pip (pastilles rondes
   colorées derrière chaque icône, v2.1.x) sont masquées ici — gardées
   dans le DOM pour ne rien casser côté JS/tests, juste plus affichées,
   exactement le registre visuel de la draft. */
.header-nav-btn{
  position:relative; /* point du badge .has-unread ci-dessous */
  display:flex; align-items:center;
  background:none;
  border:none;
  color:var(--text-muted);
  padding:8px 16px;
  border-radius:var(--r-full);
  font-family:var(--font-text);
  font-weight:600;
  font-size:var(--fs-sm);
  letter-spacing:0.02em;
  white-space:nowrap;
  cursor:pointer;
  transition:color .15s ease, background-color .15s ease;
}
.header-nav-btn .nav-pip{display:none;}
.header-nav-btn:hover{color:var(--text);}
/* Pilule active (v3.0, draft Scope, retour utilisateur : "une couleur
   bleue pour le bouton sur lequel on est") — remplissage glace plein,
   texte sombre dessus, exactement .topnav a.active dans la draft.
   updateHeaderNav(), js/router.js, pose .active sur le SEUL bouton qui
   correspond à la route en cours. */
.header-nav-btn.active{background:var(--violet); color:#06151c;}
/* Boutons d'action (v3.2) — Rechercher/Avis/Installer/Nouveautés : pas
   une page, pas de .active possible, donc pas le même langage que les
   vrais liens (texte nu qui peut s'allumer). Icône+pastille reprises ici
   (inverse de la règle juste au-dessus, spécificité plus élevée car
   deux classes) — un bouton d'action, pas un onglet éteint. */
.header-nav-btn.header-nav-action{padding:7px;}
.header-nav-btn.header-nav-action .nav-pip{display:flex;}
.header-nav-btn.header-nav-action span:not(.nav-pip){
  position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap;
}
/* Pastille (v2.1.x) : le rond teinté derrière chaque icône — la
   destination se reconnaît aussi à sa couleur, pas seulement à sa forme/
   son texte. Couleurs reprises telles quelles de la palette Halation
   (violet/or/bronze/teal), jamais une nouvelle couleur ajoutée pour
   l'occasion — voir index.html pour l'attribution de chacune. */
.nav-pip{
  width:26px; height:26px; flex:none;
  border-radius:50%;
  display:flex; align-items:center; justify-content:center;
}
.nav-pip svg{width:14px; height:14px;}
.nav-pip-violet{background:var(--violet-wash); color:var(--violet);}
.nav-pip-gold{background:var(--gold-wash); color:var(--gold);}
.nav-pip-bronze{background:var(--bronze-wash); color:var(--bronze);}
.nav-pip-teal{background:var(--teal-wash); color:var(--teal);}
/* Badge "non lu" (js/activityState.js) : un seul point, sur 👥, qui couvre
   Amis + Groupes (pas d'icône Groupes séparée dans l'entête) — calé sur
   le coin de la pastille (26px, 6px de marge gauche) plutôt que le bord
   du bouton entier, sinon un libellé long ("Nouveautés") l'isole loin de
   l'icône qu'il signale. */
.header-nav-btn.has-unread::after{
  content:'';
  position:absolute;
  top:2px; left:24px;
  width:7px; height:7px;
  border-radius:50%;
  background:var(--coral);
  border:1.5px solid var(--ink);
}
/* Avatar + pseudo comme UN item flex (v2.1, retour utilisateur) — voir le
   commentaire sur .user-identity dans index.html pour le pourquoi
   (empêche le pseudo de se détacher de la photo sous flex-wrap mobile). */
.user-identity{
  display:flex;
  align-items:center;
  gap:10px;
  flex:none;
}
/* max-width + ellipsis (même traitement que .film-title) : #userDisplayName
   n'a jamais eu de garde-fou de largeur — un pseudo un peu long (aucune
   limite de longueur côté saisie/base, voir js/profile.js) pousserait
   .user-identity à s'étirer bien au-delà de sa place dans .header-top,
   jamais repéré faute d'avoir jamais été testé avec un pseudo pareil. */
#userDisplayName{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:160px;}
/* Photo de profil : bien visible (elle sert aussi de bouton d'accès au
   profil, voir js/profile.js) — plus de bouton "Profil" ni "Déconnexion"
   séparés dans le header (déconnexion déplacée dans la modale profil, voir
   .profile-logout-row plus bas), donc on peut lui laisser plus de place. */
.user-avatar-btn{
  width:44px; height:44px;
  border-radius:50%;
  padding:0; margin:0;
  border:1px solid var(--line);
  background:var(--surface-2);
  cursor:pointer;
  flex:none;
  overflow:hidden;
  display:flex; align-items:center; justify-content:center;
  transition:border-color .15s, transform .15s;
}
.user-avatar-btn:hover{border-color:var(--violet-dim); transform:scale(1.05);}
.user-avatar-btn:focus-visible{outline:2px solid var(--violet-dim); outline-offset:2px;}
.user-avatar{
  width:100%; height:100%;
  border-radius:50%;
  object-fit:cover;
}
.user-avatar-fallback{display:flex; opacity:0.6;}
.user-avatar-fallback svg{width:18px; height:18px;}
/* Barre de navigation mobile (v2.1.x) — voir le commentaire complet sur
   #mobileTabbar dans index.html. Cachée par défaut (desktop) : la media
   query 600px plus bas la fait vivre pour de vrai (position fixe, 5
   colonnes égales) et masque .user-bar en retour — jamais les deux à la
   fois, quelle que soit la largeur. */
.mobile-tabbar{display:none;}
.mobile-tab{
  flex:1;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:3px;
  background:none;
  border:none;
  color:var(--text-muted);
  padding:8px 4px 6px;
  cursor:pointer;
  font-family:var(--font-text);
  font-size:var(--fs-xs);
  transition:color .15s;
}
.mobile-tab svg{width:22px; height:22px;}
.mobile-tab.active{color:var(--violet);}
.mobile-tab:active{transform:scale(0.94);}
.mobile-tab-avatar, .mobile-tab-avatar-fallback{
  width:22px; height:22px;
  border-radius:50%;
  object-fit:cover;
}
.mobile-tab-avatar-fallback{display:flex; align-items:center; justify-content:center; background:var(--surface-2);}
.mobile-tab-avatar-fallback svg{width:14px; height:14px;}
/* Mêmes badges "non lu" que la rangée d'icônes desktop (.header-nav-btn.
   has-unread plus haut) — #friendsBtn/#changelogBtn sont cachés sous le
   seuil mobile, sans ça leur signal disparaîtrait avec eux (voir
   refreshActivityBadge(), js/activityState.js et initChangelog(),
   js/changelog.js, qui posent .has-unread ici aussi). #mobileTabProfile
   n'a pas d'état .active suivi par route (c'est une modale, pas une
   page) — porte quand même ce badge pour Nouveautés, désormais accessible
   depuis "Mon activité" plutôt qu'un onglet à elle seule. */
.mobile-tab{position:relative;}
.mobile-tab.has-unread::after{
  content:'';
  position:absolute;
  top:4px; right:22%;
  width:8px; height:8px;
  border-radius:50%;
  background:var(--coral);
  border:1.5px solid var(--ink);
}
/* Discret volontairement (v2.0) : le numéro de version reste affiché
   (l'utilisateur y tient) mais tassé contre le titre plutôt que mis en
   avant — ce n'est pas une donnée que quelqu'un vient chercher. */
.version-tag{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--text-muted);
  opacity:0.55;
  padding-bottom:8px;
}
.version-tag b{color:var(--text-muted); font-weight:600;}
/* Sélecteur principal Films/Séries (v2.0.6) — voir index.html pour le
   pourquoi. Pleine largeur du header, sous le titre : assez proéminent
   pour se voir comme LA navigation de l'app, pas une option secondaire. */
/* v3.2 (retour utilisateur : "les 2 boutons qui traînent tout seuls,
   Film et Série") — chacun avait sa propre carte (fond/bord/radius
   individuels), deux boîtes flottantes déconnectées du reste depuis que
   .header-nav est devenue une seule pilule groupée. Même langage ici :
   UN seul conteneur pilule (fond surface, bord hairline, radius 999px,
   4px de marge intérieure), chaque onglet sans fond/bord à lui, juste
   la couleur qui change — .topnav de la draft Scope, un segmented
   control classique plutôt que deux cartes séparées. */
/* v3.2 (retour utilisateur : "les boutons séries et films sont de trop,
   je veux une solution sans bouton") — la pilule groupée (v3.2 encore
   plus tôt) réglait "deux cartes séparées" mais restait un bouton. Texte
   nu désormais : aucun fond, aucun bord, aucun padding-boîte — juste
   deux mots côte à côte, séparés par un point médian typographique (même
   registre que les séparateurs déjà en usage ailleurs, pas un nouvel
   élément). Seule la couleur + un soulignement fin distinguent l'actif,
   rien à "cliquer comme un bouton" visuellement. */
.primary-tabs{
  display:flex;
  align-items:center;
  gap:10px;
  margin-top:20px;
}
.primary-tab-sep{color:var(--text-muted); font-size:var(--fs-sm);}
.primary-tab{
  display:flex;
  align-items:center;
  gap:6px;
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-md);
  color:var(--text-muted);
  background:none;
  border:none;
  padding:2px 0 4px;
  cursor:pointer;
  border-bottom:2px solid transparent;
  transition:color .15s ease, border-color .15s ease;
}
.primary-tab svg{width:16px; height:16px; flex:none;}
.primary-tab:hover{color:var(--text);}
.primary-tab[aria-selected="true"]{
  color:var(--violet);
  border-bottom-color:var(--violet);
}
h1{
  font-family:var(--font-display);
  font-weight:800;
  font-size:var(--fs-2xl);
  letter-spacing:-0.02em;
  line-height:1;
}
.container{
  max-width:1080px;
  margin:0 auto;
  padding:0 32px;
}
/* Pages Groupes (pas des modals) : liste / détail groupe / détail
   proposition sont de vrais écrans routés par URL (#/groupes/...) plutôt
   qu'empilés en popups — un groupe contient membres + propositions + votes
   + discussion, trop pour rester en modal. Voir js/router.js. */
.page{
  padding-top:8px;
  padding-bottom:60px;
  animation:pageIn .18s cubic-bezier(.2,.8,.3,1);
}
@keyframes pageIn{from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:translateY(0);}}
@media (prefers-reduced-motion:reduce){.page{animation:none;}}
/* Révélation au scroll (v2.1.x, "Halation" plus loin) — voir
   observeReveal(), js/ui.js. Animation autonome une fois lancée par le
   scroll (pas un retour direct de clic) : garde l'exception
   prefers-reduced-motion, comme .page ci-dessus. */
.reveal{
  opacity:0;
  transform:translateY(14px);
  transition:opacity .5s ease-out, transform .5s cubic-bezier(.16,1,.3,1);
}
.reveal.in-view{opacity:1; transform:none;}
/* Cascade au sein d'une grille/section (ex. .ach-tier-grid, .stat-tiles) —
   les enfants directs traversent le seuil de l'observateur à peu près au
   même instant, c'est ce décalage de transition qui crée l'effet
   d'apparition en escalier plutôt qu'un fondu synchronisé. */
.reveal-stagger > .reveal:nth-child(1){transition-delay:0s;}
.reveal-stagger > .reveal:nth-child(2){transition-delay:.06s;}
.reveal-stagger > .reveal:nth-child(3){transition-delay:.12s;}
.reveal-stagger > .reveal:nth-child(4){transition-delay:.18s;}
.reveal-stagger > .reveal:nth-child(5){transition-delay:.24s;}
.reveal-stagger > .reveal:nth-child(n+6){transition-delay:.3s;}
@media (prefers-reduced-motion: reduce){
  .reveal{opacity:1; transform:none; transition:none;}
}
/* Même repasse que .modal-head/.mm-section (retour utilisateur : "on
   manque d'espace") — .page-header est la coquille de titre partagée par
   toutes les pages à bascule (à voir, séries, amis, groupes...), même
   levier qu'un seul point de changement pour toutes. */
.page-header{
  display:flex;
  align-items:center;
  gap:16px;
  margin-bottom:32px;
}
.page-header h2{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-xl);
  flex:1;
  min-width:0;
}

.toolbar{
  display:flex;
  justify-content:space-between;
  align-items:center;
  flex-wrap:wrap;
  gap:12px;
  margin-bottom:32px;
}
.toolbar-search{
  display:flex;
  gap:10px;
  align-items:center;
  flex:1;
  min-width:240px;
  /* Ancre #toolbarFilters (position:absolute plus bas), voir le commentaire
     juste en dessous. */
  position:relative;
}
.toolbar-search select{flex:none;}
/* Repasse (retour utilisateur : "3 boutons côte à côte pour 3 sortes de
   tri, c'est beaucoup trop") — Tri/Genre/Année vivaient en 3 <select>
   toujours visibles côte à côte sur desktop (repliés derrière
   #filtersToggleBtn seulement sous le seuil mobile, voir le bloc @media
   plus bas). Repliés maintenant PARTOUT derrière un seul bouton : panneau
   ancré façon #moreMenu (même recette que .dropdown-menu juste plus haut —
   verre, "Halation", menuIn) sur desktop/tablette, feuille plein écran
   inchangée sous 600px (le bloc @media plus bas la redéfinit toujours,
   après celui-ci donc prioritaire à cette largeur). Le comportement JS
   (openToolbarFilters()/closeToolbarFilters(), js/app.js) ne changeait déjà
   rien selon la largeur — seule cette présentation CSS bougeait. */
#filtersToggleBtn{display:inline-flex; position:relative;}
/* Point d'état : un tri/genre/année non-par-défaut est actif alors que le
   panneau est fermé — même principe que .header-nav-btn.has-unread
   (js/activityState.js), sans lequel changer un filtre puis refermer le
   panneau ne laisserait plus aucune trace visible qu'un filtre est appliqué. */
#filtersToggleBtn.has-active-filter::after{
  content:'';
  position:absolute;
  top:6px; right:6px;
  width:7px; height:7px;
  border-radius:50%;
  background:var(--violet);
  border:1.5px solid var(--surface);
}
.toolbar-filters{
  display:none;
  position:absolute;
  top:calc(100% + 8px);
  right:0;
  z-index:60;
}
.toolbar-filters.open{display:block; animation:menuIn .12s ease-out;}
.toolbar-filters-sheet{
  display:flex;
  flex-direction:column;
  gap:12px;
  width:240px;
  background:rgba(var(--glass-rgb),0.72);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border:1px solid rgba(var(--text-rgb),0.09);
  border-radius:var(--r-md);
  padding:14px;
  box-shadow:0 16px 36px rgba(0,0,0,0.45);
}
.toolbar-filters-sheet select{width:100%;}
.toolbar-filters-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-sm);
}
/* Bouton "Sélectionner" (actions groupées), même traitement pressé que les
   autres bascules .btn.active ci-dessus. */
#bulkSelectBtn.active{border-color:var(--violet); color:var(--violet); background:var(--violet-wash);}
.toolbar-actions{
  display:flex;
  align-items:center;
  gap:10px;
  flex-wrap:wrap;
}

/* --- Bascule grille / liste (js/ui.js) --- */
/* Une seule préférence partagée (localStorage) affichée sur chaque page à
   affiches (catalogue, watchlist, top, séries) — voir js/ui.js pour le
   pourquoi d'un contrôle répété plutôt qu'un seul global. */
.view-toggle{
  display:inline-flex;
  gap:2px;
  padding:3px;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--surface);
  flex:none;
}
.view-toggle-btn{
  width:32px; height:28px;
  border-radius:var(--r-sm);
  border:none;
  background:none;
  color:var(--text-muted);
  cursor:pointer;
  font-size:var(--fs-sm);
  display:flex; align-items:center; justify-content:center;
  transition:background .15s, color .15s;
}
.view-toggle-btn:hover{color:var(--text);}
.view-toggle-btn.is-active{background:var(--violet-wash); color:var(--violet);}

/* Menu "⋯" : Export/Import, actions de gestion des données, moins
   fréquentes que le reste — sorties du flux principal de la barre. */
.menu-wrap{position:relative;}
.btn.icon-only{padding:10px 13px; font-size:var(--fs-md); line-height:1;}
/* Icône SVG (v2.1, #moreMenuBtn) plutôt qu'un glyphe texte comme "⋯" —
   sans taille explicite un <svg> sans width/height s'affiche à sa taille
   intrinsèque par défaut du navigateur (bien trop grande dans un bouton). */
.btn.icon-only svg{width:16px; height:16px; display:block;}
/* v2.9, "Halation" : flotte au-dessus du catalogue, candidat naturel au
   verre (contrairement à l'entête, qui ne défile pas au-dessus de rien —
   pas de flou sans mouvement derrière à révéler). */
.dropdown-menu{
  display:none;
  flex-direction:column;
  gap:2px;
  position:absolute;
  top:calc(100% + 8px);
  right:0;
  background:rgba(var(--glass-rgb),0.72);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border:1px solid rgba(var(--text-rgb),0.09);
  border-radius:var(--r-sm);
  padding:6px;
  min-width:190px;
  box-shadow:0 16px 36px rgba(0,0,0,0.45);
  z-index:60;
  animation:menuIn .12s ease-out;
}
.menu-wrap.open .dropdown-menu{display:flex;}
.dropdown-menu button{
  display:flex;
  align-items:center;
  gap:8px;
  background:none;
  border:none;
  color:var(--text);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  text-align:left;
  padding:9px 10px;
  border-radius:var(--r-sm);
  cursor:pointer;
  white-space:nowrap;
}
.dropdown-menu button:hover{background:var(--surface-2); color:var(--violet);}
@keyframes menuIn{from{opacity:0; transform:translateY(-4px);} to{opacity:1; transform:translateY(0);}}

/* Bouton flottant "+ Ajouter", visible seulement en dessous de 600px (voir
   media query) — remplace le bouton de la barre pour ne pas la surcharger. */
.fab{
  display:none;
  align-items:center;
  justify-content:center;
  position:fixed;
  bottom:22px;
  right:22px;
  width:56px;
  height:56px;
  border-radius:50%;
  background:var(--violet);
  color:var(--ink);
  border:none;
  font-size:var(--fs-xl);
  font-weight:600;
  line-height:1;
  cursor:pointer;
  box-shadow:0 10px 28px rgba(79,207,255,0.35);
  z-index:40;
  transition:transform .15s ease;
}
.fab:hover{transform:scale(1.06); box-shadow:0 10px 28px rgba(79,207,255,0.35), 0 0 32px 6px rgba(232,163,61,0.3);}
.fab:active{transform:scale(0.92);}

/* Retour en haut (retour utilisateur : le catalogue peut dépasser 300
   films) — voir js/ui.js. Même coin que .fab (bas-droite), sans conflit :
   .fab est display:none par défaut, ne réapparaît qu'en dessous de 600px
   (media query plus bas), où ce bouton-ci passe alors à gauche pour ne
   pas les superposer. */
.scroll-top-btn{
  align-items:center;
  justify-content:center;
  position:fixed;
  bottom:22px;
  right:22px;
  width:44px;
  height:44px;
  border-radius:50%;
  background:var(--surface);
  color:var(--text);
  border:1px solid var(--line);
  font-size:var(--fs-lg);
  line-height:1;
  cursor:pointer;
  box-shadow:0 8px 20px rgba(0,0,0,0.3);
  z-index:35;
  transition:transform .15s ease, opacity .15s ease;
}
.scroll-top-btn:hover{transform:translateY(-2px); border-color:var(--violet-dim); color:var(--violet);}
.scroll-top-btn:active{transform:translateY(0) scale(0.94);}

/* Geste de rafraîchissement façon app native (retour utilisateur) — voir
   js/ui.js. `top` (pas transform) pour la position : suivi du doigt en JS
   pendant le tirer, en px — laisse `transform` entièrement libre pour la
   rotation de l'icône pendant le chargement, sans que les deux ne se
   marchent dessus dans la même propriété. Hors écran par défaut (top
   négatif) ; jamais affiché sur desktop (aucun listener touch n'existe
   sans écran tactile, voir 'ontouchstart' in window côté JS). */
.pull-refresh-indicator{
  position:fixed;
  /* top reste l'ancrage FIXE (caché au-dessus du viewport) — tout le
     mouvement du geste passe par transform:translateY(), posé image par
     image par le ressort physique (animateSpring(), js/spring.js), jamais
     par top (déclenche layout+paint à chaque frame tactile, bien plus
     coûteux que le compositeur seul — audit design, emil-design-eng). */
  top:-50px; left:50%;
  margin-left:-20px;
  width:40px; height:40px;
  border-radius:50%;
  background:var(--surface);
  border:1px solid var(--line);
  color:var(--violet);
  display:flex; align-items:center; justify-content:center;
  font-size:var(--fs-lg);
  z-index:45; /* sous .overlay (50), au-dessus du reste — visible même par-dessus la barre de nav mobile pendant le tirer */
  box-shadow:0 8px 20px rgba(0,0,0,0.3);
  transition:background .15s ease, color .15s ease;
  transform:translateY(0);
  will-change:transform;
}
.pull-refresh-indicator.ready{color:var(--gold); border-color:var(--gold);}
.pull-refresh-indicator.loading{animation:pullRefreshSpin .7s linear infinite;}
@keyframes pullRefreshSpin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion:reduce){
  .pull-refresh-indicator.loading{animation:none;}
}
/* type="date" ajouté ici (audit design, skill mobile-native) — même raison
   que font-size ci-dessous : sans lui dans cette liste, le seul champ date
   de l'app (viewingDateInput, revisionnage) héritait de la taille par
   défaut du navigateur, sous le seuil zoom iOS comme les autres avant
   correction. */
input[type="text"], input[type="email"], input[type="number"], input[type="date"], select, textarea{
  background:var(--surface);
  border:1px solid var(--line);
  color:var(--text);
  font-family:var(--font-text);
  /* 16px minimum (audit design, skill mobile-native) — en dessous, iOS
     Safari zoome automatiquement la page au focus d'un champ et NE
     DÉZOOME PAS tout seul au blur : la vue reste recadrée tant que la
     personne ne pince pas pour revenir en arrière. Touchait LITTÉRALEMENT
     chaque champ de texte de toute l'app (recherche, email de connexion,
     titre de film, commentaire...) — repli à 14px gardé pour la souris/le
     clavier physique (voir @media (pointer:coarse) juste en dessous). */
  font-size:var(--fs-md);
  padding:10px 12px;
  border-radius:var(--r-sm);
  outline:none;
}
/* Desktop (souris/clavier précis, jamais de zoom tactile à éviter) :
   repli sur la taille d'origine, plus cohérente avec le reste de
   l'interface à cette échelle — seul le tactile exige 16px. */
@media (pointer:fine){
  input[type="text"], input[type="email"], input[type="number"], input[type="date"], select, textarea{
    font-size:var(--fs-sm);
  }
}
input[type="text"]{flex:1; min-width:180px;}
/* input[type="email"] manquait ici (v2.1, retour utilisateur) : sans lui
   dans cette liste, #authEmail (le seul champ email de l'app, écran de
   connexion) restait un input natif du navigateur — non stylé, minuscule à
   côté du reste de la carte. */
.field input[type="email"]{width:100%;}
/* v2.9, "Halation" : un halo discret au focus plutôt qu'un simple
   changement de bordure — répond au geste (on vient de cliquer dedans),
   cohérent avec les boutons plus bas. */
input[type="text"]:focus, input[type="email"]:focus, input[type="number"]:focus, select:focus, textarea:focus{
  border-color:var(--violet-dim);
  box-shadow:0 0 0 3px rgba(79,207,255,0.16);
}
textarea{width:100%; resize:vertical; line-height:1.5; font-family:var(--font-text);}
select{cursor:pointer;}
/* Upload d'avatar (js/profile.js) — ::file-selector-button plutôt que
   .btn.secondary directement (pas de classe possible dessus), même langage
   visuel : transparent, filet, violet au survol. */
input[type="file"]{font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-muted);}
input[type="file"]::file-selector-button{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.06em;
  text-transform:uppercase;
  background:transparent;
  color:var(--text-muted);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:8px 12px;
  margin-right:10px;
  cursor:pointer;
  transition:border-color .15s, color .15s;
}
input[type="file"]::file-selector-button:hover{color:var(--text); border-color:var(--violet-dim);}
.avatar-upload-status{font-size:var(--fs-sm); color:var(--text-muted); margin-top:6px; min-height:1em;}
.avatar-upload-status.error{color:var(--coral);}
.btn{
  font-family:var(--font-text);
  font-size:var(--fs-sm);
  letter-spacing:0.01em;
  background:var(--violet);
  color:var(--ink);
  border:none;
  padding:11px 18px;
  border-radius:var(--r-sm);
  cursor:pointer;
  font-weight:600;
  white-space:nowrap;
  transition:background .15s, transform .1s, box-shadow .2s;
  /* Audit design, skill mobile-native : touch-action supprime le délai de
     300ms que certains navigateurs tactiles gardent avant de déclencher le
     clic (ils attendent de voir si un 2e tap arrive, pour le zoom
     double-tap — inutile ici, aucun bouton n'a besoin de ce geste).
     user-select évite qu'un appui un peu long sélectionne le texte du
     bouton au lieu de l'activer — jamais posé sur du texte de CONTENU
     (résumé, commentaire...), seulement sur un contrôle. */
  touch-action:manipulation;
  user-select:none;
  -webkit-user-select:none;
}
/* v2.9, "Halation" : un vrai halo au survol, pas juste un changement de
   teinte — la lueur qu'un point actif dégage, voir la section Le style de
   la page de référence pour le principe complet. */
.btn:hover{background:#7ad9ff; box-shadow:0 0 0 1px rgba(79,207,255,0.35), 0 0 24px 4px rgba(79,207,255,0.35);}
.btn:active{transform:scale(0.96);}
@media (prefers-reduced-motion: reduce){.btn:active{transform:none;}}
.btn:disabled{opacity:0.35; cursor:default; transform:none;}
.btn.secondary{
  background:transparent;
  color:var(--text-muted);
  border:1px solid var(--line);
}
.btn.secondary:hover{color:var(--text); border-color:var(--violet-dim);}
.btn.danger{background:transparent; color:var(--coral); border:1px solid var(--coral-dim);}
.btn.danger:hover{background:rgba(255,107,87,0.1); border-color:var(--coral);}
/* .btn posé sur un <a> (ex. #filmDetailTrailerBtn) plutôt qu'un <button> :
   un vrai lien externe (YouTube), pas une action JS — sans ça le
   soulignement par défaut du navigateur s'ajouterait à la bordure. */
a.btn{display:inline-flex; align-items:center; text-decoration:none;}

/* Lien d'évitement (audit accessibilité) : invisible tant qu'il n'a pas le
   focus clavier (premier Tab de la page), permet de sauter l'entête entier
   pour aller droit au contenu — voir #skipToContentLink dans index.html et
   son activation dans js/router.js. z-index au-dessus de tout (2000 est le
   plus haut déjà utilisé ailleurs, pour les toasts) : doit rester visible
   même si une modale est ouverte au moment où on tabule. */
.skip-link{
  position:absolute;
  top:-48px;
  left:12px;
  z-index:2100;
  background:var(--violet);
  color:#fff;
  padding:10px 18px;
  border-radius:var(--r-sm);
  font-family:var(--font-text);
  font-weight:600;
  font-size:var(--fs-sm);
  text-decoration:none;
  transition:top .15s ease;
}
.skip-link:focus-visible{
  top:12px;
}

/* Focus clavier cohérent avec le thème, à la place de l'anneau bleu par défaut.
   [role="button"] inclus : les lignes de liste rendues focusables par
   makeRowClickable() (js/ui.js, audit d'accessibilité) sont des <div>, pas
   des <button> — sans cette règle elles deviendraient atteignables au Tab
   mais totalement invisibles une fois focalisées. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [role="button"]:focus-visible{
  outline:2px solid var(--violet-dim);
  outline-offset:2px;
}

/* Scrollbar discrète, assortie au thème (Chrome/Edge + Firefox). */
*{scrollbar-width:thin; scrollbar-color:var(--line) transparent;}
::-webkit-scrollbar{width:10px; height:10px;}
::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:var(--line); border-radius:var(--r-sm); border:2px solid var(--ink);}
::-webkit-scrollbar-thumb:hover{background:var(--violet-dim);}

.count-line{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--text-muted);
  margin-bottom:18px;
}

/* Objectif annuel chiffré + barre de progression (retour utilisateur, étude
   comparative "Reading Challenge" de Goodreads) — voir renderAnnualGoalWidget(),
   js/recap.js. Posé tout en haut de l'accueil (#appContainer), avant même la
   barre d'outils : c'est le seul endroit visible à CHAQUE ouverture de l'app,
   le même raisonnement que le badge de défi sur le profil Goodreads. Vide
   (display:none) tant que films/profil ne sont pas encore chargés, jamais de
   cadre vide à l'écran pendant ce court instant. */
/* v3.2 (retour utilisateur : "l'idée est super la façon dont elle est
   implémentée est super moche... même implémentation que [le bandeau
   d'install]" — un bloc en carte, fond+bordure+coin arrondi, exactement
   le même langage que le bandeau retiré juste avant lui dans ce fichier).
   Posé directement sur le fond de la page désormais : pas de boîte, juste
   un trait fin en haut pour le séparer de ce qui précède — même registre
   que .objectif dans la draft Scope (bordure-haut seule, aucun fond). */
.annual-goal-widget{
  display:flex;
  flex-direction:column;
  gap:8px;
  padding-top:16px;
  margin-bottom:22px;
  border-top:1px solid var(--line);
}
.annual-goal-head{
  display:flex;
  align-items:center;
  gap:10px;
}
.annual-goal-label{
  font-weight:600;
  font-size:var(--fs-sm);
}
.annual-goal-count{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--text-muted);
  margin-left:auto;
}
.annual-goal-widget.is-complete .annual-goal-count{color:var(--violet);}
.annual-goal-edit{
  background:none;
  border:none;
  color:var(--text-muted);
  cursor:pointer;
  font-size:var(--fs-sm);
  line-height:1;
  padding:2px;
  flex:none;
}
.annual-goal-edit:hover{color:var(--text);}
.annual-goal-bar{
  height:8px;
  border-radius:var(--r-full);
  background:var(--line);
  overflow:hidden;
}
.annual-goal-fill{
  width:100%;
  height:100%;
  border-radius:var(--r-full);
  background:var(--violet);
  /* scaleX() plutôt que width (audit design, emil-design-eng) : width
     déclenche layout+paint à chaque frame, scaleX() ne touche que le
     compositeur (GPU). --pct posé en ligne par renderAnnualGoalWidget()
     (0 à 1) ; transform-origin:left pour que la barre grandisse depuis la
     gauche, jamais depuis le centre. ease-in-out (pas ease) : un élément
     qui se déplace/se transforme À L'ÉCRAN, pas un simple changement de
     couleur au survol. */
  transform-origin:left;
  transform:scaleX(var(--pct, 0));
  transition:transform 0.5s ease-in-out;
}
.annual-goal-widget.is-complete .annual-goal-fill{background:var(--gold, #e7b24c);}
@media (prefers-reduced-motion:reduce){
  .annual-goal-fill{transition:none;}
}
/* Moment rare et mérité (audit design, emil-design-eng : "rare/first-time
   → peut se permettre du délice") — seule animation d'entrée de tout ce
   widget, réservée à l'instant précis où l'objectif de l'année est
   atteint. Jamais scale(0) (rien n'apparaît de rien dans le monde réel) :
   part d'une échelle déjà visible. */
.annual-goal-done-msg{
  font-size:var(--fs-sm);
  color:var(--violet);
  font-weight:600;
  animation:annualGoalDone 0.4s cubic-bezier(.2,.8,.3,1) both;
}
@keyframes annualGoalDone{
  from{opacity:0; transform:scale(0.92) translateY(2px);}
  to{opacity:1; transform:scale(1) translateY(0);}
}
@media (prefers-reduced-motion:reduce){
  .annual-goal-done-msg{animation:none;}
}
.annual-goal-form{
  display:flex;
  gap:8px;
  align-items:center;
  flex-wrap:wrap;
}
.annual-goal-form input[type="number"]{
  width:90px;
}
.annual-goal-form-cancel{
  background:none;
  border:none;
  color:var(--text-muted);
  cursor:pointer;
  font-size:var(--fs-sm);
  text-decoration:underline;
}
.annual-goal-form-cancel:hover{color:var(--text);}
@media (max-width:600px){
  .annual-goal-widget{padding:12px 14px;}
  .annual-goal-head{flex-wrap:wrap;}
}

.friend-avatar{
  width:36px; height:36px;
  border-radius:50%;
  object-fit:cover;
  border:1px solid var(--line);
  flex:none;
}
.friend-avatar-placeholder{
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--surface-2);
  font-size:var(--fs-md);
  opacity:0.6;
}
/* Catalogue d'un ami (ou détail de stats) : même carte que les films
   perso, mais cliquable vers le détail de la critique (v2.1.x, voir
   openFilmReviewDetail(), js/stats.js) dès qu'elle porte un id de film —
   c'est-à-dire toujours, contrairement à l'ancien comportement qui exigeait
   une fiche TMDB (un film ajouté à la main n'en a pas, mais a bien un
   crit/review à montrer). */
.friend-film-row{cursor:default;}
.friend-film-row:hover{border-color:var(--line); transform:none; box-shadow:none;}
.friend-film-row[data-film-id]{cursor:pointer;}
.friend-film-row[data-film-id]:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}
/* Ligne d'une personne (recherche/suggestions/demandes/amis) → aperçu du
   profil public au clic (v2.2, retour utilisateur), même langage visuel que
   .friend-film-row ci-dessus — voir wireFriendRowClicks(), js/friends.js. */
.wl-row[data-user-id]{cursor:pointer; transition:border-color .15s, transform .15s, box-shadow .15s;}
.wl-row[data-user-id]:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}

.pagination{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:16px;
  margin-top:24px;
}

/* ============================================================
   Lignes de film (catalogue) — mode LISTE par défaut, mode GRILLE
   (cartes à affiche) activable via la bascule ci-dessus. Les deux modes
   réutilisent EXACTEMENT le même DOM généré par render() (js/app.js) —
   .holes/poster/.film-main/.star-btn/.counter, toujours dans cet ordre —
   seule la disposition CSS (grid-template-areas) change entre les deux,
   aucune fonction de rendu n'a besoin de le savoir.
   ============================================================ */
#filmList{display:flex; flex-direction:column;}
.film-row{
  display:grid;
  grid-template-columns:auto 1fr auto auto;
  grid-template-areas:"poster main star counter";
  align-items:center;
  column-gap:16px;
  background:linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:14px 16px;
  margin-bottom:8px;
  cursor:pointer;
  perspective:500px; /* v2.8 : vraie profondeur pour la bascule de .counter, voir counterFlip plus haut */
  transition:border-color .15s, transform .12s, box-shadow .12s;
  position:relative;
}
.film-row:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}
.film-row:active{transform:translateY(0); box-shadow:none;}
/* Case à cocher des actions groupées (retour utilisateur) — position:absolute
   plutôt qu'un 5e item de la grille nommée ci-dessus (.film-row a des
   grid-template-areas fixes à 4 zones, poster/main/star/counter, pareil en
   mode grille juste plus bas) : l'ajouter comme item de grille demanderait
   de réécrire ces zones rien que pour ce mode optionnel. .film-row a déjà
   position:relative, sert d'ancre sans toucher au flux normal — visible
   dans les deux modes (grille/liste) sans CSS spécifique à chacun. */
.bulk-select-checkbox{
  position:absolute;
  top:10px; left:10px;
  z-index:2;
  width:18px; height:18px;
  cursor:pointer;
  accent-color:var(--violet);
}
/* Taille volontairement généreuse : l'affiche est le seul élément vraiment
   "cinéma" d'une ligne de liste, le reste (titre, badges, note) est du
   texte. Classe partagée par toutes les listes de films de l'app
   (catalogue, watchlist, tops, recommandations, catalogue d'un ami) : un
   seul changement les fait toutes bénéficier de la même présence. */
.film-poster{
  grid-area:poster;
  width:52px; height:78px;
  object-fit:cover;
  border-radius:var(--r-sm);
  border:1px solid var(--line);
  flex:none;
}
/* SVG (v2.0.14) plutôt qu'un emoji 🎬/📺 — même raisonnement que l'écran
   de connexion (v2.0.9-v2.0.11) : un emoji couleur dépend de la police du
   visiteur, hors de contrôle CSS. C'est même le cas le PLUS visible de
   tout le site (un poster manquant, ça arrive dès le premier film ajouté,
   sur toutes les listes), pas juste l'écran de connexion qu'on ne voit
   qu'une fois — voir js/app.js, js/watchlist.js, js/series.js,
   js/top.js, js/upcoming.js, js/friends.js, js/groups.js, js/journal.js,
   js/proposals.js, js/publicProfile.js (tous génèrent ce même
   `<div class="film-poster film-poster-placeholder"><svg>...</svg></div>`
   plutôt qu'un emoji brut). Taille en % (pas en px) : un seul réglage
   fonctionne à toutes les tailles de posters (grille large, liste
   compacte, mobile) sans dupliquer une règle par variante. */
.film-poster-placeholder svg{width:42%; height:42%;}
.film-poster-placeholder{
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--ink-surface);
  opacity:0.5;
  /* Filet de sécurité générique : le format rectangle (pas carré) laisse
     plus de marge ici, mais un débordement n'a de toute façon jamais rien
     à apporter visuellement. */
  overflow:hidden;
}
.review-badge{
  display:inline-block;
  margin-left:6px;
  font-size:var(--fs-sm);
  vertical-align:middle;
  opacity:0.75;
}
.film-main{grid-area:main; min-width:0;}
.film-title{
  font-family:var(--font-display);
  font-weight:600;
  font-size:var(--fs-md);
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.film-sub{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:3px;
  letter-spacing:0.03em;
}
.star-btn{
  grid-area:star;
  flex:none;
  background:none;
  border:none;
  font-size:var(--fs-lg);
  cursor:pointer;
  color:var(--line);
  line-height:1;
  padding:4px;
  transition:transform .15s ease, color .15s;
}
.star-btn:hover{transform:scale(1.15);}
.star-btn.active{color:var(--gold);}
/* Micro-interactions (js/ui.js, pulseElement()) : retour visuel direct au
   clic étoile/sauvegarde — pas d'exception prefers-reduced-motion ici,
   contrairement à overlayOut/modalOut plus loin : ce n'est pas une
   animation autonome, elle EST le geste (même règle que l'arc d'Odyssée). */
.star-btn.pulse{animation:starPulse .3s ease-out;}
@keyframes starPulse{
  0%{transform:scale(1);}
  40%{transform:scale(1.35);}
  100%{transform:scale(1);}
}

/* --- Le cadran : signature visuelle de Kinet --- */
/* Remplace l'ancien rectangle plat .counter — un badge circulaire, toujours
   au même endroit qu'une note apparaît (catalogue, amis, groupes, top,
   profil public...). Couleur par BARÈME plutôt que par origine de la note
   (manuel ou grille, une distinction que plus rien n'affiche depuis le
   retrait de .manual-badge) : rouge en dessous de 2.5, or entre 2.5
   et 4, vert à partir de 4 — voir noteColorClass() dans js/ui.js, appelée
   à chaque site qui construit un .counter. Pas un anneau de progression
   proportionnel (ça demanderait de faire passer la valeur en style inline
   depuis chaque site d'appel JS) — juste une forme délibérée, cerclée,
   qui ne se confond jamais avec un bouton ou une donnée quelconque. */
.counter{
  grid-area:counter;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-md);
  font-weight:700;
  color:var(--text-muted);
  background:var(--surface-2);
  border:2px solid var(--line);
  border-radius:50%;
  width:46px; height:46px;
  display:flex; align-items:center; justify-content:center;
  flex:none;
  font-variant-numeric:tabular-nums;
}
.counter.rate-low{color:var(--coral); border-color:var(--coral-dim); background:rgba(255,107,87,0.08);}
.counter.rate-mid{color:var(--note-mid); border-color:var(--note-mid-dim); background:var(--note-mid-wash);}
.counter.rate-high{color:var(--green); border-color:var(--green-dim); background:var(--green-wash);}
/* v2.8 : bascule franchement plus marquée (retour utilisateur — "j'ai pas
   vu" sur la v2.7, un simple quart de tour de 0.4s passait inaperçu) : un
   tour complet (360°) avec un coup de zoom au passage, en perspective
   réelle (voir .film-row/.wl-row/.friend-film-row plus bas) pour un vrai
   effet 3D plutôt qu'un aplatissement horizontal. Le glow existant
   (savePulse) reste en plus. Pas d'exception prefers-reduced-motion ici
   (comme starPulse plus haut) : retour direct d'un clic de sauvegarde,
   jamais une animation autonome — même règle déjà appliquée partout
   ailleurs dans ce fichier. */
.counter.pulse{animation:savePulse .5s ease-out, counterFlip .6s cubic-bezier(.34,1.56,.64,1);}
@keyframes savePulse{
  0%{box-shadow:0 0 0 0 rgba(232,163,61,0.55);}
  100%{box-shadow:0 0 0 8px rgba(232,163,61,0);}
}
@keyframes counterFlip{
  0%{transform:rotateY(0) scale(1);}
  35%{transform:rotateY(180deg) scale(1.2);}
  70%{transform:rotateY(360deg) scale(1.2);}
  100%{transform:rotateY(360deg) scale(1);}
}

/* .manual-badge ("manuel" à côté du titre) retiré en v3.2, retour
   utilisateur : "pareil pour le tag manuel qui est de trop" — même sort
   que "Note manuelle · ancien référentiel" juste avant. f.manualNote
   sert encore au calcul de la note (getDisplayNote(), js/app.js), plus à
   aucun affichage dédié. */

/* État de chargement (index.html, #filmList) : 5 lignes statiques le temps
   du 1er chargement réel, remplacées dès que render() tourne (voir le
   commentaire dans index.html) — animation autonome/en boucle, garde donc
   l'exception prefers-reduced-motion (contrairement à starPulse/savePulse
   ci-dessus, pilotées en direct par un clic). */
.film-row.skeleton{cursor:default;}
.film-row.skeleton:hover{border-color:var(--line); transform:none; box-shadow:none;}
.skeleton-block{
  background:linear-gradient(90deg, var(--surface-2) 25%, var(--line) 50%, var(--surface-2) 75%);
  background-size:200% 100%;
  border-radius:var(--r-sm);
  animation:skeletonShimmer 1.4s ease-in-out infinite;
}
.skeleton-poster{width:38px; height:56px; flex:none; grid-area:poster;}
.skeleton-line{height:14px;}
.skeleton-counter{width:46px; height:46px; border-radius:50%; flex:none; grid-area:counter;}
.film-row.skeleton .film-main{grid-area:main;}
@keyframes skeletonShimmer{
  0%{background-position:200% 0;}
  100%{background-position:-200% 0;}
}
@media (prefers-reduced-motion:reduce){
  .skeleton-block{animation:none;}
}

/* Top films : le classement est un vrai ordre (contrairement aux 01/02...
   des critères, qui ne sont que des repères de lecture), donc le rang
   affiché porte une information réelle — podium (top 3) légèrement mis en
   avant, sans médaille ni décor superflu. */
.top-tabs{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:20px;}
.top-tabs .btn.active{border-color:var(--violet); color:var(--violet);}

.rewatch-badge{
  display:inline-block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--teal);
  border:1px solid var(--teal);
  border-radius:var(--r-sm);
  padding:2px 5px;
  margin-left:8px;
  vertical-align:middle;
  font-weight:600;
}

/* Happenings : easter eggs par film (js/happenings.js), même emplacement
   que les badges manuel/💬/↻ ci-dessus. Icône thématique au film plutôt
   qu'un pictogramme générique "secret ici" — l'indice fait partie du jeu. */
.happening-badge{
  display:inline-block;
  background:none;
  border:none;
  font-size:var(--fs-sm);
  margin-left:6px;
  padding:0;
  cursor:pointer;
  vertical-align:middle;
  line-height:1;
  filter:grayscale(0.4);
  transition:filter .15s, transform .15s;
}
.happening-badge:hover{filter:none; transform:scale(1.15);}

/* .auth-card sert à deux endroits : l'écran de connexion (dans
   .auth-hero, en colonne à côté de la vitrine ci-dessous) ET l'écran de
   maintenance (#maintenanceContainer, seul, centré) — la mise en boîte
   (fond/bordure/coins) reste commune, le centrage/largeur est spécifique
   à chaque contexte pour ne pas forcer le même gabarit aux deux. */
.auth-card{
  background:linear-gradient(165deg, var(--surface) 0%, var(--surface-2) 200%);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:30px;
  /* Centre son propre contenu verticalement (v2.1) — utile seulement sur
     l'écran de connexion, où .auth-hero l'étire maintenant à la hauteur
     de la vitrine à gauche (voir .auth-hero) ; sur l'écran de maintenance
     (#maintenanceContainer, pas dans cette grille) la boîte reste à sa
     hauteur naturelle, donc sans effet visible là-bas. */
  display:flex;
  flex-direction:column;
  justify-content:center;
  /* v2.7 : même entrée en fondu que .auth-pitch, légèrement décalée —
     opacity seule, voir la remarque sur l'alignement mesuré au pixel
     près juste au-dessus de .auth-pitch. */
  animation:authFadeIn .6s .15s ease-out both;
}
#maintenanceContainer .auth-card{max-width:420px; margin:60px auto;}
@media (prefers-reduced-motion: reduce){
  .auth-card{animation:none;}
}
.auth-card h2{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-lg);
  margin-bottom:12px;
}
.auth-intro{
  font-size:var(--fs-sm);
  color:var(--text-muted);
  line-height:1.65;
  margin-bottom:18px;
}
.auth-card .field{margin-bottom:18px;}
.auth-card .btn{width:100%;}
/* Connexion Google (v2.1) : bouton clair volontairement neutre plutôt que
   .btn violet plein — reste identifiable comme "action tierce" (Google),
   pas une action de marque Kinet comme "Envoyer le lien magique"
   juste en dessous. */
.google-btn{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:10px;
  background:#fff;
  color:#3c4043;
  border:1px solid var(--line);
}
.google-btn:hover{background:#f2f2f2;}
.google-icon{width:18px; height:18px; flex:none;}
.auth-divider{
  display:flex;
  align-items:center;
  gap:12px;
  margin:14px 0;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}
.auth-divider::before, .auth-divider::after{
  content:'';
  flex:1;
  height:1px;
  background:var(--line);
}
.auth-status{
  margin-top:14px;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--teal);
  line-height:1.6;
}

/* --- Vitrine de connexion (v2.0.3) --- */
/* .ico (emoji dans un <span> à police système forcée) a existé ici de
   v2.0.3 à v2.0.9 pour caser un emoji à côté d'un texte en Space Mono
   (qui n'a pas de glyphes émoji couleur) sans rendu cassé — retiré en
   v2.0.10, plus aucun emoji sur cet écran, tout est passé en SVG (voir
   index.html et les commentaires .auth-* plus bas). Classe supprimée
   plutôt que laissée mort : plus aucune occurrence dans le HTML/JS. */
/* #authContainer ne force plus de hauteur calculée (calc(100dvh - ...) —
   fragile : l'entête n'a pas de hauteur fixe, sur certains écrans ça
   laissait un vide en trop ou pas assez). À la place : du padding généreux
   et un contenu volontairement plus riche (texture + démo jouable
   visible à toutes les tailles, pas juste sur desktop) pour que l'écran
   ne dépende d'aucun calcul de viewport pour ne pas paraître vide. */
#authContainer{
  position:relative;
  padding:48px 0 72px;
}
/* Ancienne texture de points de l'écran de connexion : retirée (code de
   design, fond uni, audit D7). L'élément reste dans le HTML, masqué. */
#authContainer::before{
  content:'';
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  background:none;
}
/* Grille 2 colonnes (v2.0.10) : texte + démo à gauche, carte de connexion
   à droite. Colonne 2 fixe (380px, la largeur de la carte) plutôt que
   fractionnaire : la carte de connexion n'a aucune raison de s'étirer sur
   un grand écran. Repasse en une seule colonne sous 760px (voir media
   query 760px). */
.auth-hero{
  display:grid;
  grid-template-columns:minmax(0,1fr) 380px;
  /* stretch (v2.1, retour utilisateur : les deux blocs doivent être au
     même niveau) plutôt que start — la carte de connexion s'étire
     maintenant à la hauteur de la vitrine à gauche (measure vérifiée :
     492px vs 371px sans ça), .auth-card centre alors son propre contenu
     dans cet espace en plus (voir plus bas) au lieu de rester collée en
     haut d'une boîte devenue plus grande qu'elle. */
  align-items:stretch;
  gap:48px;
  max-width:1180px;
  margin:0 auto;
  padding:0 16px;
}
.auth-pitch{min-width:0; max-width:640px; animation:authFadeIn .6s ease-out both;}
/* v2.7 : entrée en fondu (opacity seule, jamais transform/hauteur) —
   pas touché à l'alignement des deux blocs, mesuré au pixel près (voir
   .auth-hero plus haut) : une animation qui déplace/redimensionne
   pourrait réintroduire le décalage déjà corrigé une fois. Autonome une
   fois lancée (transition de page) → garde l'exception
   prefers-reduced-motion, comme .page plus bas. */
@keyframes authFadeIn{from{opacity:0;} to{opacity:1;}}
@media (prefers-reduced-motion: reduce){
  .auth-pitch{animation:none;}
}
.auth-headline{
  font-family:var(--font-display);
  font-weight:800;
  font-size:var(--fs-2xl);
  letter-spacing:-0.02em;
  line-height:1.15;
  text-wrap:balance;
  margin-bottom:14px;
}
.auth-pitch-sub{
  /* Voix éditoriale (v3.0, draft Scope) : serif italique réservée aux
     phrases d'accroche/légendes, jamais à l'UI fonctionnelle — même
     registre que .section-label dans la draft. Premier endroit où
     l'appli en a besoin ; à réutiliser pour d'autres légendes/sous-titres
     au fil des prochains écrans retouchés. */
  font-family:var(--font-text);
  font-style:normal;
  font-weight:500;
  color:var(--text-muted);
  font-size:var(--fs-md);
  line-height:1.6;
  max-width:44ch;
  margin-bottom:24px;
}
/* max-width seul (pas de width:100% explicite) : en grille (desktop),
   .auth-card est un item de grid-template-columns qui s'étire déjà à sa
   colonne de 380px par défaut (justify-self:stretch, implicite) ; en flex
   colonne (mobile, voir media query 760px), un item flex column s'étire
   déjà sur l'axe transverse par défaut (align-items:stretch, implicite)
   — cette règle ne fait donc que plafonner une largeur qui, sans elle,
   grandirait trop sur un mobile large/une tablette. */
.auth-hero .auth-card{max-width:420px;}
.auth-card-foot{
  margin-top:16px;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  text-align:center;
}

/* États vides : une petite marque en couronne (dégradé violet/or de la marque)
   au-dessus du texte, pour qu'une liste vide ait une présence sans être criarde. */
/* Repère discret d'un état vide : anneau neutre, immobile (code de design :
   pas de couleur décorative, pas d'animation en boucle). */
.empty-state::before{
  content:""; display:block; width:44px; height:44px; margin:0 auto 14px; border-radius:50%;
  border:2px solid var(--line-strong);
}
.empty-state{
  text-align:center;
  padding:60px 20px;
  color:var(--text-muted);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  /* grid-column:1/-1 (v2.1, retour utilisateur "pourquoi ne pas le
     centrer") : en mode GRILLE, #filmList/#wlList/etc. sont eux-mêmes
     display:grid (voir plus bas) — sans ça, ce message (seul enfant
     quand la liste est vide) devenait un item de grille comme un autre,
     coincé dans UNE SEULE colonne de ~158px sur le côté gauche au lieu
     de profiter de toute la largeur ; le texte se centrait bien DANS
     cette colonne étroite, mais la colonne elle-même n'était pas centrée
     sur la page. Span sur toutes les colonnes disponibles pour que le
     text-align:center centre vraiment sur la largeur réelle. Ignoré sans
     effet ni risque en mode LISTE (pas de grille à cet endroit-là). */
  grid-column:1 / -1;
}

/* Modal */
/* v2.9, "Halation" : la modale devient une vitre dépolie plutôt qu'un
   panneau opaque — l'aurore du fond (voir body plus haut) se devine à
   travers, floutée. Le voile derrière (.overlay) s'éclaircit un peu
   (0.78→0.55) et se floute lui aussi : c'est le flou qui sépare le
   contenu du dessous, pas juste l'obscurité. */
.overlay{
  position:fixed; inset:0;
  background:rgba(6,5,10,0.55);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  display:none;
  align-items:flex-start;
  justify-content:center;
  padding:40px 16px;
  overflow-y:auto;
  z-index:50;
}
.overlay.open{display:flex; animation:overlayIn .15s ease-out;}
/* Fermeture (js/ui.js, closeOverlay()) : symétrique de .open, garde
   display:flex le temps de l'animation de sortie plutôt que de disparaître
   d'un coup. */
/* ease-out, pas ease-in, même à la fermeture (audit design, emil-design-eng :
   "never use ease-in for UI animations" — ease-in démarre lentement,
   exactement au moment où la réactivité compte le plus, celui où
   l'utilisateur vient de cliquer pour fermer). Fermeture aussi plus rapide
   que l'ouverture (.18s à l'entrée, modalIn) : se fermer doit toujours se
   sentir plus instantané que s'ouvrir. */
.overlay.closing{display:flex; animation:overlayOut .12s ease-out forwards;}
.overlay.closing .modal{animation:modalOut .12s ease-out forwards;}
/* padding 28px -> 34px (retour utilisateur : "on manque d'espace, ça
   manque d'être épuré") — .modal est LA coquille partagée par toutes les
   fenêtres de l'app (profil, stats, succès, journal, film, admin...), un
   seul chiffre ici change la respiration de chacune d'un coup. */
.modal{
  background:rgba(var(--glass-rgb),0.74);
  backdrop-filter:blur(26px) saturate(160%);
  -webkit-backdrop-filter:blur(26px) saturate(160%);
  border:1px solid rgba(var(--text-rgb),0.09);
  border-radius:var(--r-md);
  width:100%;
  max-width:640px;
  padding:34px;
  animation:modalIn .18s cubic-bezier(.2,.8,.3,1);
}
@keyframes overlayIn{from{opacity:0;} to{opacity:1;}}
@keyframes modalIn{from{opacity:0; transform:translateY(8px) scale(.98);} to{opacity:1; transform:translateY(0) scale(1);}}
@keyframes overlayOut{from{opacity:1;} to{opacity:0;}}
@keyframes modalOut{from{opacity:1; transform:translateY(0) scale(1);} to{opacity:0; transform:translateY(8px) scale(.98);}}
@media (prefers-reduced-motion:reduce){
  .overlay.open, .modal, .overlay.closing, .overlay.closing .modal{animation:none;}
}

/* --- Tuto d'accueil interactif (retour utilisateur, voir js/onboarding.js) ---
   Pas un .overlay/.modal classique (ceux-ci centrent toujours un rectangle
   fixe) : ce tuto doit dimmer TOUTE la page sauf une icône RÉELLE déjà
   existante, jamais dupliquée pour de vrai — juste "montrée" via un clone
   purement décoratif (même technique que le ghost du glisser-déposer de
   l'entête, js/ui.js), posé par-dessus un voile plein écran. #onboardingLayer
   lui-même ne capte aucun clic (pointer-events:none) — ce sont ses enfants
   qui décident individuellement (le voile capte, le clone jamais, la carte
   oui) : l'ORDRE dans le DOM (voile, puis clone, puis carte) suffit à les
   empiler visuellement dans le bon sens, sans le moindre z-index à gérer
   entre eux ni de bloqueur de clic séparé à poser/retirer. */
.onboarding-layer{
  position:fixed; inset:0;
  /* Au-dessus de .overlay (50) et .update-banner (400) : le tuto doit
     rester lisible même si une notification de mise à jour traîne. Sous
     .skip-link (2100, doit rester joignable par-dessus TOUTE modale). */
  z-index:900;
  display:none;
  pointer-events:none;
  /* Bug retour utilisateur ("thème clair... on ne voit pas les icônes") :
     le voile est délibérément TOUJOURS sombre (un vrai moment "spotlight"),
     mais le clone décoratif de l'icône visée héritait des tokens du thème
     ACTUEL — en thème clair, ses couleurs pensées pour un fond clair
     devenaient illisibles posées sur ce voile sombre. Les variables sont
     réassignées ici, sur le conteneur : tout ce qui vit dedans (clone
     compris, via ses classes d'origine comme .nav-pip-violet) hérite de
     CES valeurs sombres plutôt que de celles du thème choisi, quel qu'il
     soit — les tokens CSS suivent l'arbre du DOM, pas la feuille de style
     d'origine de chaque classe. */
  --violet:#4fcfff; --violet-dim:#1f6f8f; --violet-wash:rgba(79,207,255,0.12);
  --gold:#e8a33d; --gold-dim:#a66f1f; --gold-wash:rgba(232,163,61,0.1);
  --teal:#6fa39f; --teal-wash:rgba(111,163,159,0.14);
  --bronze:#d9aa67; --bronze-dim:#8f6a3a; --bronze-wash:rgba(217,170,103,0.12);
  --coral:#ff6b57; --coral-dim:#8a3f34;
  --ink:#0a0a0a; --surface:#161616; --surface-2:#1f1f1f; --surface-3:#262626;
  --line:#2a2a2a; --text:#f4f3ee; --text-muted:#8d8c86;
  --glass-rgb:10,10,10; --text-rgb:244,243,238;
  /* Bug retour utilisateur (2e passe, "Rappel terminé... illisible") : les
     variables ci-dessus ne suffisaient PAS partout. Redéfinir --text ne
     change RIEN pour un élément qui n'écrit jamais color:var(--text) lui-
     même — .onboarding-title, par ex., n'a pas de color: du tout, donc
     hérite du color RÉEL (déjà résolu, pas la variable) de son ancêtre le
     plus proche qui en déclare un : ici, body{color:var(--text)}, avec le
     --text du THÈME ACTUEL (non réassigné, lui) — texte sombre du thème
     clair sur une carte forcée sombre par ce bloc, quasi invisible. Poser
     color:var(--text) ICI, sur le conteneur, donne à TOUT ce qui vit
     dedans et n'a pas sa propre couleur (comme .onboarding-title) un point
     de départ déjà résolu avec les bonnes variables — les éléments qui
     déclarent leur propre color:var(--text-muted)/var(--violet) (le corps,
     l'eyebrow...) continuent de le faire normalement, rien ne change pour
     eux. */
  color:var(--text);
}
.onboarding-layer.open{display:block;}
/* .closing reste display:block (comme .overlay.closing) — sans ce doublon,
   retirer .open pour lancer la sortie repasserait le voile en display:none
   AVANT que overlayOut/modalOut n'aient eu la moindre chance de jouer. */
.onboarding-layer.closing{display:block;}
.onboarding-backdrop{
  position:fixed; inset:0;
  background:rgba(6,5,10,0.55);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  pointer-events:auto;
  animation:overlayIn .15s ease-out;
}
/* Même correctif ease-out qu'.overlay.closing plus haut (audit design,
   emil-design-eng). */
.onboarding-layer.closing .onboarding-backdrop{animation:overlayOut .12s ease-out forwards;}

/* Clone décoratif de l'icône/bouton visé — jamais le vrai élément, jamais
   interactif (pointer-events:none posé ici, aria-hidden + id retiré posés
   en JS). Garde ses classes d'origine (.header-nav-btn, .mobile-tab...) le
   temps du clone : leur propre CSS (fond, rayon, icône) s'applique donc
   sans rien redéfinir ici, un halo violet ("Halation", cohérent avec
   .btn:hover) suffit à signaler "regarde ici". position:fixed + left/top
   posés en JS sur le rect EXACT de la vraie cible au moment de la prise. */
.onboarding-clone{
  position:fixed;
  pointer-events:none;
  /* Halo plus marqué (retour utilisateur : "visibles mais pas assez à mon
     goût") — anneau et lueur renforcés (0.55→0.8, 0.35→0.55). */
  box-shadow:0 0 0 4px rgba(79,207,255,0.8), 0 0 30px 8px rgba(79,207,255,0.55);
  /* Un bouton NON actif (onglet non sélectionné, avatar de repli...) est
     délibérément discret dans son état normal (var(--text-muted), parfois
     opacity:0.6) — correct hors spotlight, mais ici c'est justement LUI le
     centre d'attention : cette règle (jamais !important, donc battue par
     toute couleur PROPRE à un descendant comme .nav-pip-violet, qui reste
     coloré comme avant) relève le plancher pour tout le reste — icônes/
     texte en gris discret sans couleur de badge à eux. */
  color:var(--text);
}
.onboarding-clone, .onboarding-clone *{opacity:1 !important;}

.onboarding-card{
  position:fixed;
  pointer-events:auto;
  width:270px;
  max-width:calc(100vw - 32px);
  background:rgba(var(--glass-rgb),0.92);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border:1px solid rgba(var(--text-rgb),0.09);
  border-radius:var(--r-md);
  padding:18px;
  box-shadow:0 20px 44px rgba(0,0,0,0.4);
  animation:modalIn .18s cubic-bezier(.2,.8,.3,1);
}
.onboarding-layer.closing .onboarding-card{animation:modalOut .12s ease-out forwards;}
/* Bienvenue/clôture (pas de cible précise) : carte centrée plutôt que
   positionnée près d'un élément — voir showOnboardingCenter(), js/onboarding.js. */
.onboarding-card.onboarding-card-center{
  left:50%; top:50%; transform:translate(-50%,-50%);
  width:300px;
  text-align:center;
}
@media (prefers-reduced-motion:reduce){
  .onboarding-backdrop, .onboarding-card,
  .onboarding-layer.closing .onboarding-backdrop, .onboarding-layer.closing .onboarding-card{animation:none;}
}

.onboarding-eyebrow{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs); letter-spacing:0.06em; text-transform:uppercase;
  color:var(--violet);
  display:flex; align-items:center; justify-content:space-between;
  margin-bottom:8px;
}
.onboarding-title{
  font-family:var(--font-display);
  font-weight:700; font-size:var(--fs-md);
  margin-bottom:8px;
  text-wrap:balance;
}
.onboarding-body{
  font-size:var(--fs-sm); line-height:1.6;
  color:var(--text-muted);
  margin-bottom:16px;
}
.onboarding-foot{display:flex; align-items:center; justify-content:space-between; gap:10px;}
.onboarding-card-center .onboarding-foot{justify-content:center;}
.onboarding-link-btn{
  background:none; border:none; padding:0;
  font-family:var(--font-text);
  font-size:var(--fs-sm); font-weight:600;
  color:var(--text-muted);
  cursor:pointer;
}
.onboarding-link-btn:hover{color:var(--text);}
.onboarding-icon-badge{
  width:44px; height:44px; border-radius:50%;
  background:var(--violet-wash); color:var(--violet);
  display:flex; align-items:center; justify-content:center;
  margin:0 auto 14px;
}
.onboarding-icon-badge svg{width:22px; height:22px;}
/* Petite pointe triangulaire (carré tourné à 45°) reliant la carte à la
   cible qu'elle décrit — même fond/liseré que .onboarding-card pour se
   fondre dedans plutôt que de dessiner un triangle plein. */
.onboarding-arrow{
  position:absolute; width:13px; height:13px;
  background:rgba(var(--glass-rgb),0.92);
  border:1px solid rgba(var(--text-rgb),0.09);
  border-right:none; border-bottom:none;
}
.onboarding-arrow.place-bottom{top:-7px; left:50%; margin-left:-6.5px; transform:rotate(45deg);}
.onboarding-arrow.place-top{bottom:-7px; left:50%; margin-left:-6.5px; transform:rotate(225deg);}

@media (max-width:600px){
  .onboarding-card{width:250px; padding:16px;}
  .onboarding-card.onboarding-card-center{width:270px;}
}

.modal-head{
  display:flex;
  justify-content:space-between;
  align-items:flex-start;
  margin-bottom:26px;
  gap:12px;
}
.modal-head h2{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-xl);
}
.close-x{
  background:none; border:none; color:var(--text-muted);
  font-size:var(--fs-lg); cursor:pointer; line-height:1; padding:4px;
}
.close-x:hover{color:var(--text);}
.field{margin-bottom:16px;}
.field label{
  display:block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.08em;
  text-transform:uppercase;
  color:var(--text-muted);
  margin-bottom:6px;
}
.field input[type="text"]{width:100%;}

.tmdb-results{
  margin-top:8px;
  display:flex;
  flex-direction:column;
  gap:4px;
  max-height:220px;
  overflow-y:auto;
}
.tmdb-result{
  display:flex;
  align-items:center;
  gap:10px;
  padding:6px;
  border-radius:var(--r-sm);
  cursor:pointer;
  transition:background .1s;
}
.tmdb-result:hover{background:var(--surface-2);}
.tmdb-result img{
  width:32px; height:48px;
  object-fit:cover;
  border-radius:var(--r-sm);
  flex:none;
}
.tmdb-poster-placeholder{
  width:32px; height:48px;
  flex:none;
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--ink-surface);
  border-radius:var(--r-sm);
  opacity:0.5;
}
.tmdb-poster-placeholder svg{width:42%; height:42%;}
.tmdb-result-title{font-size:var(--fs-sm);}
.tmdb-result-year{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:2px;
}
.tmdb-empty{
  font-size:var(--fs-sm);
  color:var(--text-muted);
  font-style:italic;
  padding:6px 2px;
}

/* --- Squelettes de chargement (retour utilisateur) --- Remplace le texte
   brut "Chargement…" par des silhouettes animées sur les listes à fort
   trafic (watchlist, séries, top, amis, groupes, recherche globale) — voir
   skeletonRows()/skeletonCards() dans js/ui.js. Un seul gabarit .skeleton-row
   (poster + 2 lignes de texte) sert pour toutes : ces listes partagent
   déjà .wl-row (poster + titre + sous-texte), donc une seule silhouette
   suffit à toutes les approximer. */
.skeleton-row{
  display:flex;
  align-items:center;
  gap:14px;
  padding:12px 14px;
  margin-bottom:8px;
  border:1px solid var(--line);
  border-radius:var(--r-md);
  background:linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 220%);
}
.skeleton-poster{width:52px; height:78px; border-radius:var(--r-sm); flex:none;}
.skeleton-lines{flex:1; display:flex; flex-direction:column; gap:8px; min-width:0;}
.skeleton-line{height:11px; border-radius:var(--r-sm);}
.skeleton-line.short{width:40%;}
.skeleton-line.medium{width:65%;}
.skeleton-poster, .skeleton-line{
  background:linear-gradient(100deg, var(--surface-2) 30%, var(--line-soft) 50%, var(--surface-2) 70%);
  background-size:200% 100%;
  animation:skeletonShimmer 1.4s ease-in-out infinite;
}
@keyframes skeletonShimmer{
  0%{background-position:200% 0;}
  100%{background-position:-200% 0;}
}
/* Squelette autonome une fois affiché (comme une transition de page ou
   l'ouverture d'une modale) : garde l'exception prefers-reduced-motion déjà
   établie pour ce type d'animation — mais SANS filer un rectangle figé aux
   couleurs du dégradé pour autant, la silhouette reste lisible sans juste
   perdre le mouvement. */
@media (prefers-reduced-motion:reduce){
  .skeleton-poster, .skeleton-line{animation:none; background:var(--surface-2);}
}
/* Top films (v2.3) — chaque film choisi réutilise le gabarit .tmdb-result
   (poster + titre + année). Réordonnable par glisser-déposer direct sur la
   poignée (retour utilisateur, même geste/même technique que l'entête —
   voir wireTopFilmsDrag(), js/profile.js) : plus de boutons monter/
   descendre, juste une poignée et un retrait. */
/* user-select:none (retour utilisateur : "ça fait comme si on sélectionnait
   du texte") — même paire que .header-nav.edit-mode .header-nav-btn
   (glisser-déposer de l'entête, plus haut dans ce fichier) : oubliée ici à
   tort, un pointerdown+move sur du texte sélectionne par défaut dans
   n'importe quel navigateur, touch-action:none (déjà là) empêche le SCROLL
   pendant le glisser mais pas la sélection de texte, un problème distinct. */
.top-films-picker{display:flex; flex-direction:column; gap:8px; touch-action:none; user-select:none;}
.top-film-chip{
  display:flex;
  align-items:center;
  gap:10px;
  /* Mis en avant en TAILLE, pas juste en emplacement (retour utilisateur :
     "je voudrais qu'ils prennent la ligne en entière") — affiche et texte
     agrandis plutôt qu'une simple ligne compacte comme le reste des
     réglages du profil. */
  padding:10px;
  border-radius:var(--r-sm);
  background:var(--surface-2);
  /* Audit design (emil-design-eng) : sans ça, .dragging (juste en dessous)
     posait/retirait son opacity 0.25 d'un coup sec au début/à la fin du
     glisser — ici un simple fondu, cohérent avec le reste de l'app
     (aucun transition:all, propriété précise). */
  transition:opacity .15s ease;
}
.top-film-chip img, .top-film-chip .tmdb-poster-placeholder{width:52px; height:78px; object-fit:cover; border-radius:var(--r-sm); flex:none;}
/* flex:1 — .tmdb-result-info (partagée avec les résultats de recherche
   TMDB, jamais stylée nulle part, un <div> sans layout propre) reste sinon
   à sa largeur de contenu par défaut (flex:0 1 auto) : le texte se
   retrouve collé à l'affiche plutôt que d'étirer la ligne, exactement ce
   qui empêchait la puce de "prendre la ligne en entier". Scopé à
   .top-film-chip seulement — les 6 autres usages (résultats de recherche
   watchlist/séries/propositions/profil/avatar) restent des lignes
   compactes, inchangées. */
.top-film-chip .tmdb-result-info{flex:1; min-width:0;}
.top-film-chip .tmdb-result-title{
  font-size:var(--fs-md); font-weight:700;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.top-film-chip .tmdb-result-year{font-size:var(--fs-sm);}
.top-film-grip{
  color:var(--text-muted);
  cursor:grab;
  flex:none;
  padding:4px;
  font-size:var(--fs-lg);
  line-height:1;
  touch-action:none;
}
.top-film-chip.dragging{opacity:0.25; outline:1.5px dashed var(--violet-dim); outline-offset:-1px;}
/* Clone décoratif pendant le glisser — même principe que
   .header-nav-drag-ghost (js/ui.js) : jamais le vrai élément, juste un
   aperçu qui suit le pointeur pendant que la vraie ligne, sous lui,
   continue de porter le réordonnancement. */
.top-film-drag-ghost{
  position:fixed;
  z-index:1000;
  pointer-events:none;
  box-shadow:0 14px 32px rgba(0,0,0,0.35);
  border-radius:var(--r-sm);
  cursor:grabbing;
}
.top-film-chip-remove{
  width:26px; height:26px;
  border:1px solid var(--line);
  background:var(--surface);
  color:var(--text-muted);
  border-radius:var(--r-sm);
  cursor:pointer;
  font-size:var(--fs-sm);
  display:flex; align-items:center; justify-content:center;
  flex:none;
  transition:border-color .15s, color .15s;
}
.top-film-chip-remove:hover{border-color:var(--coral-dim); color:var(--coral);}
/* Réorganiser l'entête EN PLACE (retour utilisateur : "si on clique dessus
   on revient sur la page d'accueil sauf que cette fois on peut modifier
   les icones") — plus de liste séparée à part (ancienne
   .header-order-dnd-*, retirée) : le glisser-déposer agit directement sur
   les VRAIS .header-nav-btn, activé/désactivé par enterHeaderEditMode()/
   exitHeaderEditMode() (js/ui.js) qui posent/retirent .edit-mode sur
   .header-nav. touch-action:none indispensable au tactile — sans lui, le
   navigateur intercepte le geste pour faire défiler la page avant même
   que pointermove ne soit reçu. */
.header-nav.edit-mode .header-nav-btn{touch-action:none; user-select:none;}
/* Tremblement ("jiggle") : sert D'INDICATEUR — sans lui, rien ne dit
   qu'une icône normalement fixe devient soudain saisissable. Même
   vocabulaire que l'édition d'écran d'accueil iOS/Android, déjà connu.
   installHeaderBtn exclu (PWA, affiché seulement quand pertinent — voir
   HEADER_NAV_DEFAULT_ORDER, js/ui.js) : ni réordonnable, ni concerné par
   ce mode, reste cliquable normalement. Décalage de phase sur les boutons
   pairs : le même mouvement rejoué en même temps sur toute la rangée
   paraîtrait mécanique plutôt que "vivant". */
.header-nav.edit-mode .header-nav-btn:not(#installHeaderBtn){
  animation:headerNavJiggle .22s ease-in-out infinite alternate;
  cursor:grab;
}
.header-nav.edit-mode .header-nav-btn:not(#installHeaderBtn):nth-child(even){animation-delay:-.11s;}
@keyframes headerNavJiggle{
  from{transform:rotate(-2deg);}
  to{transform:rotate(2deg);}
}
/* Pendant le glisser, le VRAI bouton devient un simple emplacement vide
   (le ghost ci-dessous porte tout le visuel) — juste assez visible
   (contour pointillé) pour montrer où l'icône atterrira si on relâche là. */
.header-nav.edit-mode .header-nav-btn.dragging{
  animation:none;
  cursor:grabbing;
  opacity:0.25;
  outline:1.5px dashed var(--violet-dim);
  outline-offset:-1px;
}
/* Ghost (retour utilisateur : "je dois pouvoir une fois prise la déplacer
   librement") — clone en position:fixed qui suit le pointeur au pixel
   près (translate posé en JS, voir wireHeaderNavDrag()) pendant que le
   vrai bouton, sous lui, continue de porter le réordonnancement sans
   bouger visuellement. left/top/width/height posés en JS (calqués sur le
   bouton d'origine au moment de la prise). */
.header-nav-drag-ghost{
  position:fixed;
  z-index:1000;
  pointer-events:none;
  box-shadow:0 14px 32px rgba(0,0,0,0.35);
  cursor:grabbing;
}
/* Pas d'animation autonome sous prefers-reduced-motion (même règle que
   .reveal) — le contour pointillé (déjà posé sur .dragging
   ci-dessus, et ici sur les icônes encore "au repos") prend seul le relais
   comme indicateur "sélectionnable" ; le ghost continue de suivre le
   pointeur (translate n'est pas une animation autonome, c'est le geste
   lui-même — même règle que starPulse/savePulse ailleurs dans ce fichier). */
@media (prefers-reduced-motion:reduce){
  .header-nav.edit-mode .header-nav-btn:not(#installHeaderBtn){
    animation:none;
    outline:2px dashed var(--violet-dim);
    outline-offset:3px;
    border-radius:var(--r-full);
  }
}
.tmdb-selected{
  display:flex;
  align-items:center;
  gap:10px;
  margin-top:10px;
  padding:8px;
  background:var(--ink-surface);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
}
.tmdb-selected img{width:32px; height:48px; object-fit:cover; border-radius:var(--r-sm); flex:none;}
.tmdb-selected-info{flex:1; min-width:0;}
#tmdbSelectedTitle{font-size:var(--fs-sm);}
/* #tmdbSelected (modale principale d'ajout/édition) : plus le même
   gabarit que .tmdb-selected (watchlist/séries/proposition, une vignette
   + ce texte côte à côte) depuis que l'affiche a sa propre colonne dédiée
   (.film-modal-poster-col, voir index.html et plus bas) : ce bloc-ci ne
   contient plus que le texte, pas la peine de reprendre un fond/bordure
   de carte en plus de celle déjà posée par .film-modal-poster-col. */
#tmdbSelected{margin-top:10px;}
#tmdbSelectedTitle{font-family:var(--font-display); font-weight:600; font-size:var(--fs-md);}
/* "Voir la fiche du film" / "Retirer la fiche TMDB" : deux .crit-help-toggle
   côte à côte (inline-block par défaut sur un <button>). Un peu d'air
   entre les deux pour qu'elles se lisent comme deux actions distinctes,
   pas un seul libellé qui déborde. */
#tmdbViewFicheBtn{margin-right:16px;}
.manual-toggle-label{
  display:flex;
  align-items:center;
  gap:8px;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  letter-spacing:0.03em;
  color:var(--text);
  cursor:pointer;
  margin-top:4px;
}
.manual-toggle-label input{accent-color:var(--teal); cursor:pointer;}
/* Lien du profil public (#publicProfileLinkHint) : même bloc que
   .manual-toggle-hint, plus une ligne lien + bouton Copier. */
.public-profile-link-row{
  display:flex;
  align-items:center;
  gap:10px;
  margin-top:8px;
  font-style:normal;
}
.public-profile-link-row span{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  color:var(--teal);
  overflow-wrap:anywhere;
  flex:1;
}

/* Modale "Nouveau/Modifier un film" (retour utilisateur : "dans la draft
   tu m'as fait une super fiche film avec sur la droite les critères et à
   gauche en grand l'affiche du film, au-dessus des critères la note et
   encore au-dessus le titre... là tu t'es contenté de grossir la note").
   La vraie mise en page cette fois : affiche à gauche, titre/note/
   critères à droite, dans une modale élargie pour lui laisser la place
   (.film-modal). Deuxième retour, la note noter-avec-des-critères étant
   LE cœur de l'app : "trop dans la longueur", "prend beaucoup plus de
   largeur", critères "sur la même ligne comme la draft". La modale
   passe à 1080px (plutôt que 640px de base) et les 7 critères se
   répartissent sur 2 colonnes (#criteriaWrap ci-dessous) au lieu d'une
   pile verticale unique : la même info, sur environ la moitié de la
   hauteur. */
.modal.film-modal{max-width:1080px;}
/* .film-modal-poster réécrit width/height/border-radius de .film-poster
   (52×78px, pensé pour une vignette de ligne de catalogue) : cette classe
   vient après elle dans le fichier, donc l'emporte à spécificité égale.
   .film-poster-placeholder (icône centrée, fond neutre, voir plus haut)
   continue de s'appliquer normalement, seule la taille change. */
.film-modal-poster{
  width:100%;
  aspect-ratio:2/3;
  height:auto;
  border-radius:var(--r-md);
  object-fit:cover;
}
/* Les 7 critères sur 2 colonnes plutôt qu'une pile verticale unique :
   c'est ce qui fait vraiment gagner la largeur supplémentaire de la
   modale en hauteur récupérée. Repli à 1 colonne avant même que
   .film-modal-layout ne s'empile (voir le second seuil plus bas) : entre
   les deux seuils, la colonne de contenu devient trop étroite pour 2
   critères lisibles côte à côte alors que l'affiche est encore à côté. */
#criteriaWrap{
  display:grid;
  grid-template-columns:1fr 1fr;
  column-gap:32px;
  margin-top:8px;
}
.criterion{
  border-top:1px solid var(--line);
  padding:14px 0;
}
@media (max-width:860px){
  #criteriaWrap{grid-template-columns:1fr;}
}
@media (max-width:860px){
  .film-modal-poster-col{width:140px; margin:0 auto;}
}

/* Cadran en vedette (retour utilisateur : "dans la draft tu m'as fait une
   super fiche film... je ne le vois nulle part") — réutilise le VRAI
   .counter de l'app (chaque affiche du catalogue en a un), juste agrandi,
   plutôt qu'un nouveau composant. Mis à jour par updateLiveScore()
   (js/app.js), déjà appelée à chaque glissement de curseur. */
.crit-dial-hero{
  display:flex; align-items:center; gap:18px;
  padding:20px 0;
  margin-bottom:8px;
  border-bottom:1px solid var(--line);
}
.crit-dial-hero-counter{
  width:80px; height:80px;
  font-size:var(--fs-xl);
  border-width:3px;
  flex:none;
}
.crit-dial-hero-label{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-md);
}
.crit-dial-hero-hint{
  font-size:var(--fs-sm);
  color:var(--text-muted);
  margin-top:2px;
}

/* .criterion (grille 2 colonnes, bordure/espacement) : voir la règle
   juste au-dessus de #criteriaWrap, avec le reste de la mise en page de
   la modale film. */
.crit-head{
  display:flex;
  justify-content:space-between;
  align-items:center;
  cursor:pointer;
}
.crit-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  letter-spacing:0.05em;
}
.crit-label .num{color:var(--violet-dim); margin-right:6px;}
/* Éditable (v2.1, retour utilisateur : le drag seul manque de précision,
   même avec la molette — voir js/ui.js) : un chiffre qu'on peut taper
   directement, en plus du curseur, plutôt qu'une simple valeur affichée.
   Repris en <input type="number"> mais habillé pour rester discret comme
   avant (pas de bordure/fond visibles hors focus) — voir aussi
   updateCritValInput()/updateCritSliderFromInput(), js/app.js. */
/* input[type="number"].crit-val (pas juste .crit-val) : sans l'attribut de
   type en plus de la classe, la règle générique input[type="number"]{...}
   plus haut (padding/bordure/fond permanents) l'emporterait à specificité
   égale — voir le commentaire au-dessus de .crit-val. */
input[type="number"].crit-val{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  font-weight:600;
  color:var(--gold);
  width:52px;
  text-align:right;
  background:none;
  /* v2.2, retour utilisateur ("les curseurs c'est galère") persistant même
     après le thumb agrandi/le champ éditable ci-dessus : la bordure ne
     devenait visible qu'au survol (border transparent par défaut), donc
     invisible au tactile qui n'a pas de survol — rien n'indiquait qu'on
     pouvait taper une valeur précise plutôt que de viser le curseur. Bordure
     toujours visible (discrète), plus marquée au survol/focus. */
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:3px 6px;
  -moz-appearance:textfield;
}
input[type="number"].crit-val::-webkit-inner-spin-button,
input[type="number"].crit-val::-webkit-outer-spin-button{-webkit-appearance:none; margin:0;}
input[type="number"].crit-val:hover{border-color:var(--violet-dim);}
input[type="number"].crit-val:focus{border-color:var(--violet-dim); background:var(--surface); outline:none;}
.crit-def{
  font-size:var(--fs-sm);
  color:var(--text-muted);
  font-style:italic;
  margin-top:4px;
  line-height:1.5;
}
.crit-slider-row{
  display:flex;
  align-items:center;
  gap:10px;
  margin-top:10px;
  /* Un peu d'air vertical en plus (v2.1) pour le thumb agrandi à 20px
     (voir input[type="range"]::-webkit-slider-thumb plus bas). */
  padding:6px 0;
}
input[type="range"]{
  flex:1;
  -webkit-appearance:none;
  appearance:none;
  height:4px;
  background:var(--line);
  border-radius:var(--r-sm);
  outline:none;
  /* Au doigt, un scroll qui passe sur une barre de notation la faisait
     sauter à la valeur touchée, changeant la note sans le vouloir.
     touch-action:pan-y dit au navigateur qu'un geste vertical sur cet
     élément est un scroll de page (jamais un drag de curseur) — seul un
     geste horizontal (ou un tap) est encore traité comme une interaction
     avec le slider. */
  touch-action:pan-y;
}
/* Sans ce doublon, le focus clavier resterait invisible sur les sliders :
   input[type="range"]{outline:none;} ci-dessus a la même spécificité que
   la règle générique input:focus-visible plus haut dans ce fichier et,
   venant après elle dans la feuille, l'emporterait silencieusement. */
input[type="range"]:focus-visible{
  outline:2px solid var(--violet-dim);
  outline-offset:2px;
}
/* 20px (au lieu de 14px) — v2.1, retour utilisateur : trop petit pour viser
   précisément au doigt sur mobile. Zone de clic réellement plus grande
   (pas juste visuellement) : c'est cette taille de thumb qui définit la
   zone interactive d'un slider natif, contrairement à un bouton normal où
   du padding suffirait. */
input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none;
  width:20px;height:20px;
  border-radius:50%;
  background:var(--gold);
  cursor:pointer;
  border:2px solid var(--ink);
}
input[type="range"]::-moz-range-thumb{
  width:20px;height:20px;
  border-radius:50%;
  background:var(--gold);
  cursor:pointer;
  border:2px solid var(--ink);
}
/* Curseur jamais réglé (série sans note, audit C4) : pouce neutre, pour ne
   pas faire croire à une note déjà posée. */
input[type="range"].is-unset::-webkit-slider-thumb{background:var(--surface-3); border-color:var(--text-muted);}
input[type="range"].is-unset::-moz-range-thumb{background:var(--surface-3); border-color:var(--text-muted);}
.avatar-source-tabs{
  display:flex;
  gap:6px;
  margin-bottom:10px;
}
.avatar-source-tab{
  flex:1;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.03em;
  padding:8px 6px;
  border:1px solid var(--line);
  background:transparent;
  color:var(--text-muted);
  border-radius:var(--r-sm);
  cursor:pointer;
}
.avatar-source-tab.active{
  color:var(--violet);
  border-color:var(--violet-dim);
  background:var(--violet-wash);
}
.crit-help-toggle{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  background:none;
  border:none;
  cursor:pointer;
  text-decoration:underline;
  text-underline-offset:2px;
  margin-top:8px;
  padding:0;
}
.crit-help-toggle:hover{color:var(--teal);}
.crit-help{
  display:none;
  font-size:var(--fs-sm);
  line-height:1.7;
  color:var(--text-muted);
  white-space:pre-line;
  margin-top:8px;
  padding:10px 12px;
  background:var(--ink-surface);
  border-left:2px solid var(--teal);
  border-radius:var(--r-sm);
}
.crit-help.open{display:block;}
.crit-anchors{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--teal);
  white-space:pre-line;
  line-height:1.8;
  padding-bottom:10px;
  margin-bottom:10px;
  border-bottom:1px solid var(--line);
}
.crit-questions{
  white-space:pre-line;
  line-height:1.7;
}

.modal-footer{
  display:flex;
  justify-content:space-between;
  align-items:center;
  margin-top:24px;
  padding-top:18px;
  border-top:1px solid var(--line);
}
/* Masqués par défaut (desktop) : Top films/Avis/Nouveautés sont déjà dans
   l'entête (#topBtn/#feedbackBtn/#changelogBtn) — les doublonner ici ne
   servait à personne (retour utilisateur, audit). Révélés sous le seuil
   mobile (voir @media (max-width:600px) plus bas), seul endroit où
   l'entête a disparu et où ces boutons sont le seul chemin qui reste.
   .mm-row.profile-mobile-only (spécificité à 2 classes, pas juste
   .profile-mobile-only) : sans ça, .mm-row{display:flex} plus bas dans ce
   fichier l'emporterait par ordre de déclaration à spécificité égale, et
   ces 3 rangées resteraient visibles sur desktop — bug constaté en
   vérifiant la refonte du profil en direct avant déploiement. */
.mm-row.profile-mobile-only{display:none;}
/* Installation en app (js/pwa.js) — .wl-note réutilisée telle quelle pour
   le texte descriptif/les instructions (pas de nouvelle classe pour un
   simple paragraphe secondaire). */
#installSectionContent .wl-note{margin-top:0; margin-bottom:10px; max-width:52ch; font-size:var(--fs-sm);}
#installSectionContent .wl-note:last-child{margin-bottom:0;}

/* ============================================================
   Ton profil / Paramètres — refonte (retour utilisateur : "illisible et
   beaucoup trop confus", "bien catégoriser", inspirée des drafts validés).
   Deux idées portent tout ce bloc : une carte d'identité en tête de "Ton
   profil" (avatar + pseudo, plus un formulaire), et des RANGÉES groupées
   en cartes (icône + libellé + interrupteur/flèche) partout ailleurs —
   remplace les boutons empilés (.profile-activity-links, retiré) et les
   cases à cocher nues (.manual-toggle-label reste utilisée ailleurs dans
   l'app, mais plus ici). ============================================ */

/* --- Carte d'identité --- */
.pf-header{display:flex; flex-direction:column; align-items:center; text-align:center; margin-bottom:18px;}
.pf-avatar-wrap{position:relative; width:76px; height:76px; margin-bottom:10px; flex:none;}
.pf-avatar-wrap img, .pf-avatar-wrap .user-avatar-fallback{width:76px; height:76px; border-radius:50%;}
.pf-avatar-wrap img{object-fit:cover; border:2px solid var(--line);}
.pf-avatar-wrap .user-avatar-fallback{
  border:2px solid var(--line); background:var(--surface-2);
  display:flex; align-items:center; justify-content:center; color:var(--text-muted);
}
.pf-avatar-wrap .user-avatar-fallback svg{width:32px; height:32px;}
.pf-avatar-edit{
  position:absolute; right:-2px; bottom:-2px; width:26px; height:26px; border-radius:50%;
  background:var(--violet); color:var(--ink); border:2px solid var(--surface);
  display:flex; align-items:center; justify-content:center; cursor:pointer; transition:background .15s;
}
.pf-avatar-edit:hover{background:#7ad9ff;}
.pf-avatar-edit svg{width:13px; height:13px;}
.pf-name-input{
  font-family:var(--font-display); font-weight:700; font-size:var(--fs-lg); text-align:center;
  background:none; border:none; border-bottom:1px solid transparent; color:var(--text);
  padding:2px 6px; width:auto; max-width:100%;
}
.pf-name-input:hover{border-bottom-color:var(--line);}
.pf-name-input:focus{border-bottom-color:var(--violet-dim); outline:none;}
.pf-name-hint{font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px;}

.pf-avatar-panel{
  background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r-md);
  padding:14px; margin-bottom:18px; display:none;
}
.pf-avatar-panel.open{display:block;}

/* --- Rangées groupées en cartes (Paramètres, Mon activité, Compte...) --- */
/* margin-top 18px -> 28px, padding des lignes 12px -> 15px (même retour
   utilisateur que .modal juste plus haut) : les sections d'un même écran
   (Ton profil, Paramètres...) se touchaient presque, chaque ligne
   cliquable était un peu comprimée. */
.mm-section{margin-top:28px;}
.mm-section-title{
  font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-xs); letter-spacing:0.06em; text-transform:uppercase;
  color:var(--text-muted); margin-bottom:10px;
}
.mm-card{background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r-md); overflow:hidden;}
.mm-row{
  display:flex; align-items:center; gap:12px; width:100%;
  padding:15px 16px; background:none; border:none; text-align:left; cursor:pointer;
  font-family:var(--font-text); color:var(--text); transition:background .12s;
}
.mm-row + .mm-row, .mm-row + .pf-public-panel{border-top:1px solid var(--line);}
.mm-row:hover{background:var(--surface-3);}
/* #moreMenuWrap (export/import, v3.2) : une ligne seule, hors .mm-card
   exprès (voir le commentaire dans index.html — overflow:hidden y aurait
   coupé .dropdown-menu à l'ouverture). Même fond/bord/coin que .mm-card
   posés directement ici, SANS overflow:hidden — le radius sur .mm-row
   lui-même (pas juste le conteneur) garde des coins propres aussi au
   survol, qui change le fond de .mm-row et non de .menu-wrap. */
.mm-section .menu-wrap{background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r-md);}
.mm-section .menu-wrap .mm-row{border-radius:var(--r-md);}
.mm-icon{width:30px; height:30px; border-radius:var(--r-sm); display:flex; align-items:center; justify-content:center; flex:none;}
.mm-icon svg{width:16px; height:16px;}
.mm-icon.ic-violet{background:var(--violet-wash); color:var(--violet);}
.mm-icon.ic-gold{background:var(--gold-wash); color:var(--gold);}
.mm-icon.ic-teal{background:var(--teal-wash); color:var(--teal);}
.mm-icon.ic-bronze{background:var(--bronze-wash); color:var(--bronze);}
.mm-row-body{flex:1; min-width:0;}
.mm-row-title{font-size:var(--fs-sm); font-weight:600;}
.mm-row-sub{font-size:var(--fs-xs); color:var(--text-muted); margin-top:1px;}
.mm-chevron{color:var(--text-muted); flex:none; display:flex;}
.mm-chevron svg{width:16px; height:16px;}

/* Interrupteur : un vrai <input type="checkbox"> caché visuellement (pas
   display:none — resterait accessible au clavier/lecteur d'écran) plutôt
   qu'un <div> custom — le <label class="mm-row"> qui l'entoure suffit à
   rendre TOUTE la rangée cliquable nativement, sans le moindre JS. */
.mm-switch-input{position:absolute; opacity:0; width:1px; height:1px; overflow:hidden;}
.mm-switch{
  width:38px; height:22px; border-radius:var(--r-full); background:var(--surface-3); border:1px solid var(--line);
  position:relative; flex:none; transition:background .15s, border-color .15s;
}
.mm-switch::after{
  content:''; position:absolute; top:1px; left:1px; width:18px; height:18px; border-radius:50%;
  background:var(--text-muted); transition:transform .15s, background .15s;
}
.mm-switch-input:checked + .mm-switch{background:var(--violet); border-color:var(--violet);}
.mm-switch-input:checked + .mm-switch::after{transform:translateX(16px); background:var(--ink);}
.mm-switch-input:focus-visible + .mm-switch{outline:2px solid var(--violet); outline-offset:2px;}

/* --- Profil public : panneau qui se déplie sous sa rangée --- */
.pf-public-panel{display:none; padding:14px; background:var(--surface); border-top:1px solid var(--line);}
.pf-public-panel.open{display:block;}
.pf-public-note{font-size:var(--fs-xs); color:var(--text-muted); line-height:1.5; margin-bottom:14px;}
/* Bouton unique "Copier le lien" (retour utilisateur : afficher l'URL en
   clair "de trop, ça gêne") — publicProfileUrl() (js/profile.js) la calcule
   à la demande, jamais besoin de la montrer pour la copier. */
.pf-copy-link-btn{
  display:flex; align-items:center; justify-content:center; gap:8px; width:100%;
  padding:10px 14px; background:var(--violet-wash); color:var(--violet); border:1px solid var(--violet-dim);
  border-radius:var(--r-sm); font-family:var(--font-text); font-size:var(--fs-sm); font-weight:700; cursor:pointer;
  transition:background .12s;
}
.pf-copy-link-btn:hover{background:var(--violet-dim); color:var(--text);}
.pf-copy-link-btn svg{width:15px; height:15px; flex:none;}
.pf-top-sub{font-size:var(--fs-xs); color:var(--text-muted); margin-bottom:10px;}
.pf-top-sub{font-size:var(--fs-xs); color:var(--text-muted); margin-bottom:10px;}

/* --- En-tête "Paramètres" avec flèche retour (au lieu du close-x) --- */
.modal-head-back{display:flex; align-items:center; gap:10px; margin-bottom:20px;}
.modal-head-back h2{flex:1;}
.modal-back-btn{
  width:30px; height:30px; border-radius:var(--r-sm); border:1px solid var(--line); background:var(--surface-2);
  color:var(--text-muted); display:flex; align-items:center; justify-content:center; flex:none; cursor:pointer;
  transition:color .15s, border-color .15s;
}
.modal-back-btn:hover{color:var(--text); border-color:var(--violet-dim);}
.modal-back-btn svg{width:16px; height:16px;}
.modal-footer .right{display:flex; gap:10px;}

/* Tutoriel d'import Letterboxd (index.html #importLetterboxdTutoOverlay) —
   numéros en pastille plutôt qu'une simple liste : cohérent avec le
   traitement déjà donné aux icônes d'entête (.nav-pip), un même motif
   "pastille = repère visuel" plutôt que d'en inventer un nouveau ici. */
.import-tuto-steps{list-style:none; margin:0 0 18px; padding:0; display:flex; flex-direction:column; gap:14px;}
.import-tuto-steps li{display:flex; gap:12px; align-items:flex-start;}
.import-tuto-num{
  flex:none; width:24px; height:24px; border-radius:50%;
  background:var(--violet-wash); color:var(--violet);
  font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-sm); font-weight:700;
  display:flex; align-items:center; justify-content:center;
}
.import-tuto-steps p{margin:0; font-size:var(--fs-sm); line-height:1.5; color:var(--text);}
.import-tuto-steps code{background:var(--surface-2); border:1px solid var(--line); border-radius:var(--r-sm); padding:1px 5px; font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-sm);}
.import-tuto-note{font-size:var(--fs-sm); line-height:1.5; color:var(--text-muted); margin:0 0 18px;}

/* Statistiques */
.stats-modal{max-width:720px;}
#openRecapBtn{width:100%; margin-bottom:16px;}

/* Bilan cinéphile partageable (v2.43) — voir js/recap.js. Le canvas garde
   son ratio portrait (1080x1350, format bien adapté au partage) en
   s'adaptant à la largeur du modal plutôt qu'un défilement horizontal. */
.recap-modal{max-width:420px;}
/* 3 boutons dans un footer de 420px de large (Export PDF, retour
   utilisateur, ajouté au Partager/Télécharger l'image déjà là) : le
   .modal-footer générique (justify-content:space-between, pas de wrap) ne
   suffit plus à les tenir sur une ligne — repli propre sur plusieurs
   lignes plutôt qu'un débordement horizontal. */
.recap-modal .modal-footer{flex-wrap:wrap; gap:10px; justify-content:flex-end;}
#recapBody{display:flex; justify-content:center;}
#recapCanvas{
  width:100%;
  max-width:380px;
  height:auto;
  border-radius:var(--r-md);
  border:1px solid var(--line);
}
#downloadRecapBtn{width:100%;}
/* Top films (v2.3) — vitrine façon Letterboxd, distincte de la liste
   .stats-section en dessous (triée par note) : ceux-ci sont un choix
   personnel, affichés dans l'ordre voulu par le propriétaire du profil.
   Voir renderPublicProfilePage(), js/publicProfile.js. */
.top-films-showcase{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(90px, 1fr));
  gap:14px;
}
/* min-width:0 — même bug/raisonnement que .film-row/.wl-row en grille
   plus bas dans ce fichier (S25, "affiches disproportionnées") : cet item
   est lui aussi un enfant direct d'une grille CSS avec un titre en
   white-space:nowrap juste en dessous, minmax(90px,1fr) étant même plus
   étroit — un titre long y déborderait sa juste part encore plus
   facilement sans ce 0. */
.top-films-showcase-item{display:flex; flex-direction:column; gap:6px; min-width:0;}
.top-films-showcase-item img,
.top-films-showcase-item .film-poster-placeholder{
  width:100%;
  aspect-ratio:2/3;
  height:auto;
  object-fit:cover;
  border-radius:var(--r-sm);
  /* v2.7 : liseré bronze au repos (choix rare/mérité, voir les tokens en
     tête de fichier) — distingue ces affiches du catalogue ordinaire même
     sans survol. */
  border:1px solid var(--bronze-dim);
  transition:border-color .15s, transform .15s;
}
.top-films-showcase-item[data-tmdb-id]{cursor:pointer;}
/* v2.9, "Halation" : halo à deux teintes au survol, plutôt qu'un simple
   changement de bordure — un top film mérite bien ça. */
.top-films-showcase-item[data-tmdb-id]:hover img,
.top-films-showcase-item[data-tmdb-id]:hover .film-poster-placeholder{
  border-color:var(--gold);
  transform:translateY(-2px);
  box-shadow:0 0 0 1px rgba(232,163,61,0.3), 0 0 26px 4px rgba(232,163,61,0.3), 0 0 50px 10px rgba(79,207,255,0.16);
}
.top-films-showcase-title{
  font-size:var(--fs-sm);
  font-weight:600;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
/* Mur de posters — profil public (retour utilisateur) : même esprit que
   .top-films-showcase juste au-dessus, généralisé à tout le catalogue. */
.poster-wall{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(110px, 1fr));
  gap:14px;
}
/* min-width:0 — même raison que .top-films-showcase-item juste au-dessus
   (.poster-wall-caption est aussi en white-space:nowrap). */
.poster-wall-item{position:relative; display:flex; flex-direction:column; gap:6px; min-width:0;}
.poster-wall-item img,
.poster-wall-item .film-poster-placeholder{
  width:100%;
  aspect-ratio:2/3;
  height:auto;
  object-fit:cover;
  border-radius:var(--r-sm);
  border:1px solid var(--line);
}
.poster-wall-note{
  position:absolute;
  top:6px; right:6px;
  min-width:26px; height:26px; padding:0 6px;
  border-radius:var(--r-full);
  display:flex; align-items:center; justify-content:center;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs); font-weight:700;
  color:var(--text);
  background:rgba(20,17,28,0.82);
  backdrop-filter:blur(2px);
}
/* .rate-low/-mid/-high (js/ui.js, noteColorClass()) sont sinon scopées à
   .counter — redéclarées ici plutôt que d'ajouter .counter à cet élément,
   dont le style de base (taille, bordure) ne convient pas à ce badge. */
.poster-wall-note.rate-low{color:var(--coral);}
.poster-wall-note.rate-mid{color:var(--note-mid);}
.poster-wall-note.rate-high{color:var(--green);}
.taste-note-pill{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  border-radius:var(--r-full);
  padding:2px 8px;
  background:var(--surface-2);
  border:1px solid var(--line);
  white-space:nowrap;
}
.taste-note-pill.rate-low{color:var(--coral); border-color:var(--coral-dim);}
.taste-note-pill.rate-mid{color:var(--note-mid); border-color:var(--note-mid-dim);}
.taste-note-pill.rate-high{color:var(--green); border-color:var(--green-dim);}
.poster-wall-fav{
  position:absolute;
  top:6px; left:6px;
  color:var(--gold);
  text-shadow:0 1px 3px rgba(0,0,0,0.6);
  font-size:var(--fs-sm);
}
.poster-wall-caption{
  font-size:var(--fs-sm);
  font-weight:600;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.stat-tiles{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(110px, 1fr));
  gap:14px;
  margin-bottom:28px;
}
.stat-tile{
  background:linear-gradient(160deg, var(--ink-surface) 0%, var(--surface) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:14px 12px;
  text-align:center;
}
.stat-value{
  font-family:var(--font-display);
  font-size:var(--fs-xl);
  font-weight:800;
  color:var(--gold);
  font-variant-numeric:tabular-nums;
}
.stat-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--text-muted);
  text-transform:uppercase;
  margin-top:4px;
}
.stats-section{margin-bottom:28px;}
/* Amis/Groupes (retour utilisateur, backlog : "même respiration que la
   modale film"). Ces lignes (avatar + nom + 1-2 boutons) sont bien plus
   sparses qu'une ligne catalogue (affiche + note + actions) : en pile sur
   1 colonne, jusqu'à 1080px de large (.container), elles laissaient
   beaucoup de vide au milieu plutôt que d'utiliser la largeur. Grille
   plutôt qu'une pile, .wl-row garde exactement le même style (juste plus
   de marge-bas à retirer, le gap de la grille la remplace). */
/* Page Amis : plus concernée depuis le fil d'affiches (draft Amis du
   15/10), ses listes sont en une colonne et les recommandations ont leur
   propre grille d'affiches (.fr-posters). */
#groupsList{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));
  gap:10px;
}
#groupsList > .wl-row{
  margin-bottom:0;
}
@media (max-width:600px){
  #groupsList{
    grid-template-columns:1fr;
  }
}
/* Bouton "Voir tous les films notés" (v2.5) — révèle le catalogue complet
   d'un ami, replié par défaut sous ses stats. Voir openFriendProfile(),
   js/friends.js. */
/* Bouton "Voir plus"/"Voir moins" générique sous une liste repliée — voir
   renderCollapsible() (js/ui.js) et le catalogue replié d'un profil d'ami
   (openFriendProfile(), js/friends.js), les deux mécanismes qui l'utilisent. */
.list-toggle-btn{width:100%; margin-bottom:16px;}
/* Digest de retour (js/activityState.js) : même famille que .chosen-banner
   (accent or, bandeau plein-largeur) mais vit hors des pages à bascule
   (voir index.html) donc porte son propre espacement vertical plutôt que
   de compter sur .page. */
.digest-banner{
  background:var(--surface);
  border:1px solid var(--gold-dim);
  border-radius:var(--r-sm);
  padding:16px 32px;
  margin:16px auto;
}
.digest-title{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-md);
  margin-bottom:12px;
}
.digest-banner .wl-list{margin-bottom:14px;}
/* Mode hors ligne (js/offline.js) : même gabarit que .digest-banner
   (bandeau plein-largeur hors des pages à bascule) mais accent --coral
   plutôt qu'or — c'est un état dégradé à signaler, pas une bonne nouvelle
   à consulter. */
.offline-banner{
  background:var(--surface);
  border:1px solid var(--coral-dim);
  border-radius:var(--r-sm);
  padding:14px 32px;
  margin:16px auto;
  font-size:var(--fs-sm);
}
/* .install-banner (js/pwa.js) retiré en v3.2, retour utilisateur : "le
   rectangle pour télécharger l'app... franchement pas beau" — même
   famille visuelle (bloc en dégradé, bordure, coins ronds) que l'ancien
   .annual-goal-widget (voir plus haut dans ce fichier), simplifié pour
   la même raison. */
/* Bandeau d'édition de l'entête (retour utilisateur, voir index.html et
   enterHeaderEditMode()/js/ui.js) — même gabarit/accent qu'avait
   .install-banner (violet : un outil qu'on active soi-même, pas un
   avertissement) ; le bandeau lui-même a disparu, pas ce langage
   (violet : un outil qu'on active soi-même, pas un avertissement). */
.header-edit-banner{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
  flex-wrap:wrap;
  background:linear-gradient(120deg, var(--violet-wash) 0%, var(--surface) 65%);
  border:1px solid var(--violet-dim);
  border-radius:var(--r-sm);
  padding:14px 24px;
  margin:16px auto;
  font-size:var(--fs-sm);
}
/* Bandeau de mise à jour (js/offline.js) — fixe, accent --gold comme
   .digest-banner : une info à traiter, ni la marque (violet) ni un
   avertissement (coral). z-index au-dessus des modales (300) : une
   nouvelle version doit rester joignable même en pleine saisie d'une
   fiche film. */
.update-banner{
  display:none;
  align-items:center;
  justify-content:center;
  gap:16px;
  position:fixed;
  left:0; right:0; bottom:0;
  z-index:400;
  background:var(--surface-2);
  border-top:1px solid var(--gold-dim);
  padding:12px 20px;
  font-size:var(--fs-sm);
  box-shadow:0 -8px 24px rgba(0,0,0,0.3);
}
.update-banner span{color:var(--text);}
@media (max-width:600px){
  .update-banner{flex-direction:column; gap:8px; text-align:center; padding:14px 16px;}
}
.stats-section-title{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-md);
  margin-bottom:14px;
}

/* Calendrier visuel de Prochainement (retour utilisateur : remplacer la
   simple liste triée par date par un vrai calendrier) — grille CSS pure,
   pas de librairie (voir js/upcoming.js, renderUpcomingCalendar()). */
.upcoming-cal-nav{
  display:flex; align-items:center; justify-content:space-between;
  margin-bottom:12px;
}
.upcoming-cal-nav .btn{padding:6px 10px; font-size:var(--fs-sm);}
.upcoming-cal-label{
  font-family:var(--font-display); font-weight:700; font-size:var(--fs-sm);
  text-transform:capitalize;
}
.upcoming-cal-grid{
  display:grid;
  grid-template-columns:repeat(7, 1fr);
  gap:4px;
}
.upcoming-cal-weekday{
  font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-xs); color:var(--text-muted);
  text-align:center; text-transform:uppercase; letter-spacing:.04em;
  padding-bottom:4px;
}
.upcoming-cal-day{
  min-height:68px;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--surface);
  padding:4px;
  display:flex; flex-direction:column; gap:3px;
}
.upcoming-cal-day.is-outside{opacity:.35;}
.upcoming-cal-day.is-today{border-color:var(--violet-dim); box-shadow:inset 0 0 0 1px var(--violet-dim);}
.upcoming-cal-daynum{font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-xs); color:var(--text-muted);}
.upcoming-cal-items{display:flex; flex-wrap:wrap; gap:2px;}
.upcoming-cal-poster{
  width:18px; height:27px; object-fit:cover; border-radius:var(--r-sm); flex:none;
  cursor:default;
}
.upcoming-cal-poster.is-clickable{cursor:pointer;}
.upcoming-cal-poster-placeholder{
  display:flex; align-items:center; justify-content:center;
  background:var(--surface-2); color:var(--text-muted);
}
.upcoming-cal-poster-placeholder svg{width:10px; height:10px;}
@media (max-width:600px){
  .upcoming-cal-day{min-height:44px;}
  .upcoming-cal-poster{width:14px; height:21px;}
}
.bar-chart{
  display:flex;
  align-items:flex-end;
  gap:6px;
  height:120px;
  /* padding-top réserve la place pour le libellé de la barre max (voir
     .bar-value) : overflow-x:auto impose implicitement overflow-y:auto,
     qui coupe tout ce qui dépasse la padding-box en haut. */
  padding-top:22px;
  overflow-x:auto;
  padding-bottom:2px;
}
.bar-col{
  display:flex;
  flex-direction:column;
  align-items:center;
  flex:1;
  min-width:22px;
  height:100%;
  cursor:default;
}
.bar-track{
  flex:1;
  width:100%;
  display:flex;
  align-items:flex-end;
  justify-content:center;
  background:var(--ink-surface);
  border-radius:var(--r-sm) var(--r-sm) 0 0;
}
.bar-fill{
  width:100%;
  min-height:2px;
  background:var(--gold);
  border-radius:var(--r-sm) var(--r-sm) 0 0;
  transition:opacity .1s;
  position:relative;
}
.bar-col:hover .bar-fill{opacity:0.8;}
/* Graphique des notes : les barres montent depuis le bas, en léger décalage
   d'une barre à l'autre. Mouvement réduit : affichage direct. */
.bar-fill{transform-origin:50% 100%; animation:barGrow .55s cubic-bezier(.2,.8,.3,1) both;}
.bar-col:nth-child(2) .bar-fill{animation-delay:.03s;}
.bar-col:nth-child(3) .bar-fill{animation-delay:.06s;}
.bar-col:nth-child(4) .bar-fill{animation-delay:.09s;}
.bar-col:nth-child(5) .bar-fill{animation-delay:.12s;}
.bar-col:nth-child(6) .bar-fill{animation-delay:.15s;}
.bar-col:nth-child(7) .bar-fill{animation-delay:.18s;}
.bar-col:nth-child(8) .bar-fill{animation-delay:.21s;}
.bar-col:nth-child(9) .bar-fill{animation-delay:.24s;}
.bar-col:nth-child(n+10) .bar-fill{animation-delay:.27s;}
@keyframes barGrow{from{transform:scaleY(0);} to{transform:scaleY(1);}}
@media (prefers-reduced-motion: reduce){ .bar-fill{animation:none;} }
.bar-value{
  position:absolute;
  top:-18px;
  left:50%;
  transform:translateX(-50%);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--gold);
  white-space:nowrap;
}
.bar-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:6px;
  white-space:nowrap;
}
/* Barre cliquable (v2.2, retour utilisateur : voir les films derrière une
   barre, ex. "notés 4") — seules les barres non vides le sont (voir
   renderBarChart(), js/stats.js), le curseur "default" hérité de .bar-col
   resterait sinon trompeur sur celles qui n'ont rien à montrer. */
.bar-col-clickable{cursor:pointer;}
.bar-col-clickable .bar-label{color:var(--text);}
.bar-col-clickable:focus-visible{outline:2px solid var(--violet-dim); outline-offset:2px; border-radius:var(--r-sm);}
.bar-col-active .bar-fill{background:var(--violet);}
.bar-col-active .bar-label{color:var(--violet-dim); font-weight:700;}

/* Évolution de la note moyenne — SVG à la main, voir renderLineChart(),
   js/stats.js. Repasse (retour utilisateur : "pas assez clair", "pas
   toujours bien proportionné") : aspect-ratio (pas une hauteur fixe) pour
   que .line-chart-svg garde EXACTEMENT les proportions de son viewBox
   (300×150) quelle que soit la largeur réelle — même recette que
   .film-poster{aspect-ratio:2/3} ailleurs dans ce fichier. C'est ce qui
   manquait pour que preserveAspectRatio (par défaut, sans "none") mette à
   l'échelle x et y du même facteur : la courbe garde sa vraie forme, les
   points restent des cercles. */
.line-chart{position:relative;}
.line-chart-svg{width:100%; aspect-ratio:300/150; display:block; margin-top:8px; overflow:visible;}
.line-chart-grid{stroke:var(--line); stroke-width:1; stroke-dasharray:2 3;}
.line-chart-axis-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  fill:var(--text-muted);
}
.line-chart-halo{fill:var(--gold); opacity:0.18;}
.line-chart-dot{fill:var(--gold);}
.line-chart-dot-last{fill:var(--gold); stroke:var(--ink); stroke-width:1.5;}
.line-chart-crosshair{stroke:var(--gold-dim); stroke-width:1; stroke-dasharray:2 2; vector-effect:non-scaling-stroke;}
.line-chart-capture{cursor:crosshair;}
/* Infobulle HTML (retour utilisateur : "pas assez clair" — le <title>
   natif du navigateur était minuscule, lent à apparaître, jamais tactile)
   — positionnée par wireLineChart() (js/stats.js) en pixels réels, donc
   PAS d'unités viewBox ici. Toujours au-dessus du point visé (transform
   -50% en x, jamais en y : sa position top est déjà celle voulue). */
.line-chart-tooltip{
  position:absolute;
  transform:translate(-50%, -100%) translateY(-8px);
  /* --surface-2/--text : un COUPLE de tokens qui basculent ENSEMBLE d'un
     thème à l'autre (jamais --ink tout seul avec var(--text) — --ink reste
     TOUJOURS sombre dans les deux thèmes, voir sa définition en tête de ce
     fichier, un texte clair posé dessus deviendrait illisible en thème
     clair, même bug que celui trouvé sur le tuto). */
  background:var(--surface-2);
  border:1px solid var(--line);
  color:var(--text);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:700;
  white-space:nowrap;
  padding:4px 8px;
  border-radius:var(--r-sm);
  pointer-events:none;
  box-shadow:0 4px 12px rgba(0,0,0,0.25);
  z-index:1;
}
/* Un repère par point plutôt qu'un sous-ensemble (renderLineChart(), voir
   son commentaire) devenait illisible dès qu'il y a beaucoup de mois
   d'historique. Repris en position:relative + repères posés en %,
   alignés sous LEUR point plutôt qu'un flex space-between qui répartit
   uniformément (et donc désaligné dès qu'un libellé sur deux est sauté). */
.line-chart-labels{
  position:relative;
  height:14px;
  margin-top:6px;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}
.line-chart-label{
  position:absolute;
  top:0;
  transform:translateX(-50%);
  white-space:nowrap;
}
/* Premier/dernier repère : centrés sur leur point les ferait déborder du
   conteneur (la moitié du texte partirait avant 0% / après 100%). Collés
   au bord qu'ils bordent à la place. */
.line-chart-label[data-edge="start"]{transform:translateX(0);}
.line-chart-label[data-edge="end"]{transform:translateX(-100%);}

/* Genre dominant par mois (retour utilisateur). */
.genre-month-row{
  display:flex;
  align-items:center;
  gap:12px;
  padding:8px 0;
  border-bottom:1px solid var(--line);
}
.genre-month-row:last-child{border-bottom:none;}
.genre-month-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  width:56px;
  flex:none;
}
.genre-month-genre{flex:1; font-weight:600; color:var(--gold);}
.genre-month-count{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}
.stats-drilldown{
  margin-top:16px;
  padding-top:14px;
  border-top:1px solid var(--line);
}
.stats-drilldown-title{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--text-muted);
  text-transform:uppercase;
  margin-bottom:10px;
}
/* Détail d'une critique (#filmReviewOverlay, js/stats.js) — note par
   critère + commentaire d'un film noté, en lecture seule. Même principe
   que .film-modal (affiche à gauche, contenu à droite, voir plus haut) :
   c'est la même idée de "fiche film" que dans la modale d'ajout/édition,
   pas la peine d'inventer une deuxième disposition pour elle. */
.modal.film-review-modal{max-width:900px;}
.film-review-layout{
  display:grid;
  grid-template-columns:200px 1fr;
  gap:32px;
  align-items:start;
}
.film-review-poster-col{display:flex; flex-direction:column; gap:16px;}
.film-review-poster-col .film-poster{width:100%; aspect-ratio:2/3; border-radius:var(--r-md);}
.film-review-title{font-family:var(--font-display); font-weight:700; font-size:var(--fs-lg); line-height:1.2;}
.film-review-year{font-size:var(--fs-sm); color:var(--text-muted); margin-top:2px; margin-bottom:18px;}
/* Radar des 7 critères (retour utilisateur), sous l'affiche plutôt
   qu'empilé en pleine largeur au-dessus de la liste à barres : les deux
   visualisent les mêmes 7 valeurs, ils restent dans la même colonne que
   l'affiche plutôt que d'étager affiche/radar/barres sur 3 niveaux.
   Voir renderCritRadar()/critReviewRowHtml() (js/stats.js). */
.film-review-poster-col .crit-radar-svg{max-width:100%; margin:0;}
/* Cas note manuelle (pas de radar/grille, juste ce texte) : .wl-note n'a
   qu'un margin-top ailleurs dans l'app, jamais de margin-bottom (repéré en
   vérifiant : le texte touchait presque "Commentaire" juste en dessous). */
.film-review-main-col .wl-note{margin-bottom:16px;}
.crit-radar-svg{
  display:block;
  width:100%;
  height:auto;
  /* Un <svg> a overflow:hidden par défaut (feuille de style du navigateur)
     — les libellés, volontairement posés à un rayon plus grand que la
     zone de données pour rester lisibles autour du radar, débordaient du
     viewBox et se faisaient couper net sur les bords (constaté en direct :
     "Ressenti global"/"Musique" tronqués à gauche, "Mise en scène"/
     "Jeu d'acteur"/"Esthétique visuelle" à droite). overflow:visible laisse
     le texte s'étendre hors du viewBox sans toucher au positionnement du
     radar lui-même. */
  overflow:visible;
}
.crit-radar-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  fill:var(--text-muted);
}
.crit-review-list{
  display:grid;
  grid-template-columns:1fr 1fr;
  column-gap:28px;
  row-gap:14px;
  margin-bottom:22px;
}
.crit-review-row{display:flex; flex-direction:column; gap:6px;}
.crit-review-row-top{display:flex; justify-content:space-between; align-items:baseline; gap:8px;}
.crit-review-label{font-size:var(--fs-sm); color:var(--text-muted);}
.crit-review-value{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-weight:700;
  font-size:var(--fs-sm);
  color:var(--gold);
  flex:none;
}
.crit-review-bar{
  height:5px;
  background:rgba(0,0,0,0.28);
  border-radius:var(--r-sm);
  overflow:hidden;
}
.crit-review-fill{height:100%; background:var(--violet); border-radius:var(--r-sm);}
.crit-review-comment{
  background:var(--surface-2);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:14px 16px;
  font-size:var(--fs-sm);
  line-height:1.55;
  white-space:pre-line;
  margin-bottom:18px;
}
.film-review-open-detail{width:100%;}
/* Sous ce seuil, la colonne affiche+radar redevient un bandeau en tête
   (même convention que .film-modal-layout/.detail-card ailleurs dans
   l'app) : petite affiche à gauche, radar réduit à côté plutôt que deux
   blocs centrés l'un sous l'autre en pleine largeur. */
@media (max-width:600px){
  .film-review-layout{grid-template-columns:1fr;}
  .film-review-poster-col{flex-direction:row; align-items:flex-start;}
  .film-review-poster-col .film-poster{width:110px; flex:none;}
  .film-review-poster-col .crit-radar-svg{max-width:180px;}
  .crit-review-list{grid-template-columns:1fr;}
}
/* Comparaison année sur année (retour utilisateur) — voir
   renderYearComparison() (js/stats.js). Deux colonnes symétriques plutôt
   qu'un tableau : peu de valeurs par côté, une carte se lit plus vite
   qu'une grille de cellules ici. */
.year-compare-grid{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:12px;
  margin-bottom:12px;
}
.year-compare-col{
  background:linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:16px;
  text-align:center;
}
.year-compare-col-empty{display:flex; flex-direction:column; justify-content:center; min-height:120px;}
.year-compare-year{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  text-transform:uppercase;
  letter-spacing:.05em;
  margin-bottom:8px;
}
.year-compare-total{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-xl);
  color:var(--violet);
  line-height:1;
}
.year-compare-sub{font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px;}
.year-compare-avg{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-weight:700;
  font-size:var(--fs-md);
  color:var(--gold);
  margin-top:10px;
}
.year-compare-avg-unit{font-weight:400; color:var(--text-muted);}
.year-compare-genre{
  display:inline-block;
  margin-top:8px;
  padding:3px 10px;
  border-radius:var(--r-full);
  background:var(--violet-wash);
  color:var(--violet);
  font-size:var(--fs-xs);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
}
.year-compare-delta{
  text-align:center;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--text-muted);
}
.year-compare-delta.is-up{color:var(--violet);}
.year-compare-delta.is-down{color:var(--text-muted);}
.stats-extremes{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:14px;
  margin-bottom:0;
}
.stats-extreme{
  display:flex;
  align-items:center;
  gap:12px;
  background:linear-gradient(160deg, var(--ink-surface) 0%, var(--surface) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:12px 14px;
}
/* v2.1.x — seul "Mieux noté" reçoit le halo (même logique de retenue que
   .ach-tier-card.maxed : un moment à célébrer). "Moins bien noté" reste
   neutre exprès — pas de dégradé rouge/négatif, juste l'info brute. */
.stats-extreme-best{
  position:relative;
  border-color:transparent;
  box-shadow:0 0 18px 2px rgba(232,163,61,0.16);
}
.stats-extreme-best::before{
  content:'';
  position:absolute; inset:0;
  border-radius:inherit;
  padding:1.5px;
  background:linear-gradient(120deg, var(--gold), var(--bronze), var(--violet));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask-composite:exclude;
  pointer-events:none;
}
.stats-extreme-poster{
  width:42px; height:62px;
  object-fit:cover;
  border-radius:var(--r-sm);
  border:1px solid var(--line);
  flex:none;
}
.stats-extreme-poster-placeholder{
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--surface);
  opacity:0.5;
}
.stats-extreme-poster-placeholder svg{width:42%; height:42%;}
.stats-extreme-info{min-width:0;}
.stats-extreme-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.06em;
  text-transform:uppercase;
  color:var(--text-muted);
  margin-bottom:6px;
}
.stats-extreme-title{
  font-family:var(--font-display);
  font-weight:600;
  font-size:var(--fs-md);
  margin-bottom:4px;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.stats-extreme-note{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--gold);
}

/* Succès */
.achievements-modal{max-width:760px;}
/* v2.7 : bloc plum plutôt qu'un fond plat — succès et compatibilité ciné
   sont deux moments "à part" (pas une donnée du quotidien comme une ligne
   de catalogue), le bloc de couleur les distingue au premier coup d'œil. */
.ach-summary{
  position:relative;
  background:linear-gradient(155deg, var(--plum) 0%, var(--plum-2) 100%);
  border:1px solid transparent;
  border-radius:var(--r-sm);
  padding:16px 18px;
  text-align:center;
  margin-bottom:28px;
}
/* v2.9, "Halation" : filet dégradé plutôt qu'une bordure plate — technique
   par masque (le pseudo-élément ne couvre QUE l'épaisseur de la bordure,
   jamais l'intérieur de la carte). */
.ach-summary::before{
  content:'';
  position:absolute; inset:0;
  border-radius:inherit;
  padding:1.5px;
  background:linear-gradient(120deg, var(--violet), var(--gold), var(--bronze));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask-composite:exclude;
  pointer-events:none;
}
.ach-summary-count{
  font-family:var(--font-display);
  font-size:var(--fs-xl);
  font-weight:800;
  color:var(--gold);
  font-variant-numeric:tabular-nums;
}
.ach-summary-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--text-muted);
  text-transform:uppercase;
  margin-top:2px;
}
.ach-summary-bar{
  height:6px;
  background:rgba(0,0,0,0.28); /* pas --surface-2 (v2.7) : se distinguait mal sur le nouveau fond plum */
  border-radius:var(--r-sm);
  overflow:hidden;
  margin-top:12px;
}
.ach-summary-fill{height:100%; background:var(--gold); border-radius:var(--r-sm);}

.ach-tier-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));
  gap:14px;
}
.ach-tier-card{
  background:linear-gradient(160deg, var(--ink-surface) 0%, var(--surface) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:14px;
}
.ach-tier-head{display:flex; align-items:center; gap:8px; margin-bottom:8px;}
.ach-tier-icon{font-size:var(--fs-md);}
.ach-tier-label{font-family:var(--font-display); font-weight:600; font-size:var(--fs-sm);}
.ach-tier-medals{margin-bottom:10px; font-size:var(--fs-md); letter-spacing:2px;}
.ach-medal{filter:grayscale(1) opacity(0.35);}
.ach-medal.earned{filter:none;}
.ach-tier-bar{
  height:5px;
  background:var(--surface-2);
  border-radius:var(--r-sm);
  overflow:hidden;
  margin-bottom:8px;
}
.ach-tier-fill{height:100%; background:var(--teal); border-radius:var(--r-sm);}
.ach-tier-progress{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}

/* --- Cartes à collectionner (TCG, retour utilisateur) --- voir js/tcg.js
   et supabase/migrations/041_add_tcg_cards.sql. Rareté mappée sur des
   teintes DÉJÀ en usage dans la palette Halation, jamais une couleur
   ajoutée pour l'occasion (même discipline que .film-sub-genre/.counter
   plus haut) : Commun reste neutre (aucun accent, un simple liseré) ;
   Rare en teal (déjà le ton "informationnel discret" de .manual-badge) ;
   Épique en violet (la couleur interactive/de marque de tout le site —
   une carte qui mérite qu'on s'y arrête) ; Légendaire en or (déjà réservé
   aux moments rares — succès, mise en avant). */
.tcg-modal{max-width:760px;}
.tcg-booster-bar{
  display:flex;
  align-items:center;
  justify-content:space-between;
  flex-wrap:wrap;
  gap:12px;
  padding:14px 16px;
  margin-bottom:20px;
  border-radius:var(--r-md);
  border:1px solid var(--line);
  background:linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 220%);
}
/* Rattrapage du catalogue (retour utilisateur) — voir backfillTcgCards()/
   handleScanCatalog(), js/tcg.js. Rangée discrète (texte seul une fois
   lancée, pas une barre de progression pleine largeur) : ce scan tourne
   déjà tout seul en tâche de fond à la 1ère ouverture, ce bouton n'est
   qu'un filet pour le relancer/le reprendre, pas l'action principale de
   cet écran. */
.tcg-scan-row{
  display:flex;
  align-items:center;
  gap:12px;
  margin-bottom:16px;
}
.tcg-scan-status{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}
.tcg-booster-count{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--text-muted);
}
.tcg-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(104px, 1fr));
  gap:12px;
}
.tcg-card{
  position:relative;
  aspect-ratio:2/3;
  border-radius:var(--r-sm);
  overflow:hidden;
  border:2px solid var(--line);
  background:var(--ink-surface);
}
.tcg-card img{width:100%; height:100%; object-fit:cover; display:block;}
/* Survol : la carte se soulève et la lueur de sa rareté s'accentue. Pas
   d'effet à l'arrêt, et rien si le mouvement est réduit. */
.tcg-card{transition:transform .2s ease, box-shadow .2s ease;}
.tcg-card:hover{transform:translateY(-3px);}
@media (prefers-reduced-motion: reduce){
  .tcg-card, .tcg-card:hover{transition:none; transform:none;}
}
.tcg-card .film-poster-placeholder{width:100%; height:100%; display:flex; align-items:center; justify-content:center; opacity:0.5;}
.tcg-card .film-poster-placeholder svg{width:38%; height:38%;}
.tcg-card-name{
  position:absolute;
  left:0; right:0; bottom:0;
  padding:16px 8px 7px;
  font-size:var(--fs-xs);
  font-weight:600;
  line-height:1.25;
  color:#fff;
  background:linear-gradient(0deg, rgba(15,14,22,0.92) 20%, transparent);
}
.tcg-card-qty{
  position:absolute;
  top:6px; right:6px;
  padding:2px 7px;
  border-radius:var(--r-full);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:700;
  color:#fff;
  background:rgba(15,14,22,0.78);
}
.tcg-card.rarity-rare{border-color:var(--rarity-rare); box-shadow:0 0 14px rgba(111,163,159,0.3);}
.tcg-card.rarity-epique{border-color:var(--violet); box-shadow:0 0 16px rgba(79,207,255,0.35);}
/* Confettis au 5/5 (celebrateConfetti, js/ui.js) : pastilles tombantes,
   retirées par le JS après l'animation. */
/* Curseur de volume (Paramètres → Son) : couleur de la palette, largeur fixe. */
#soundVolume{width:120px; accent-color:var(--violet); cursor:pointer;}
.confetti-layer{position:fixed; inset:0; pointer-events:none; overflow:hidden; z-index:9999;}
.confetti-piece{
  position:absolute; top:-12px; width:8px; height:12px; border-radius:var(--r-sm);
  animation-name:confettiFall; animation-timing-function:cubic-bezier(.2,.6,.4,1); animation-fill-mode:both;
}
@keyframes confettiFall{
  0%{transform:translateY(0) rotate(0deg); opacity:1;}
  100%{transform:translateY(105vh) rotate(540deg); opacity:0;}
}
.tcg-card.rarity-legendaire{border-color:var(--rarity-legendary); box-shadow:0 0 20px rgba(232,163,61,0.45);}
/* Légendaire : un reflet doré lent qui traverse la carte. Discret, et jamais
   si le mouvement est réduit. */
.tcg-card.rarity-legendaire::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(115deg, transparent 35%, rgba(255,240,200,0.22) 50%, transparent 65%);
  transform:translateX(-100%); animation:tcgShine 4.5s ease-in-out infinite;
}
@keyframes tcgShine{0%,55%{transform:translateX(-100%);} 100%{transform:translateX(100%);}}
@media (prefers-reduced-motion: reduce){
  .tcg-card.rarity-legendaire::after{animation:none; display:none;}
}

/* Petite étiquette de rareté dans une .wl-row (doublons, résultats de
   fabrication) — même famille que .manual-badge/.rewatch-badge déjà en
   place (liseré + texte coloré, Space Mono), jamais un nouveau langage
   visuel pour l'occasion. */
.tcg-rarity-tag{
  display:inline-block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.06em;
  text-transform:uppercase;
  border-radius:var(--r-sm);
  padding:2px 6px;
  font-weight:600;
  vertical-align:middle;
}
.tcg-rarity-tag.rarity-commun{color:var(--text-muted); border:1px solid var(--line);}
.tcg-rarity-tag.rarity-rare{color:var(--rarity-rare); border:1px solid var(--rarity-rare);}
.tcg-rarity-tag.rarity-epique{color:var(--violet); border:1px solid var(--violet);}
.tcg-rarity-tag.rarity-legendaire{color:var(--rarity-legendary); border:1px solid var(--rarity-legendary);}

/* --- Onglet Échanger : constructeur d'échange --- deux colonnes (ma
   collection / celle du partenaire), cartes plus petites qu'en Collection
   (voir .tcg-grid plus haut) pour que les deux tiennent côte à côte dans
   la largeur de la modale — voir renderTradeBuilder(), js/tcg.js. */
.tcg-grid-compact{
  grid-template-columns:repeat(auto-fill, minmax(72px, 1fr));
  gap:8px;
  max-height:280px;
  overflow-y:auto;
  padding:2px;
}
.tcg-grid-compact .tcg-card{cursor:pointer; transition:transform .12s, box-shadow .12s;}
.tcg-grid-compact .tcg-card:hover{transform:translateY(-2px);}
/* Sélection (retour utilisateur implicite : on doit voir immédiatement ce
   qu'on a déjà mis dans l'offre) — pastille violette distincte du liseré
   de rareté, jamais confondue avec lui (couleur interactive de marque,
   pas une des 4 couleurs de rareté). */
.tcg-grid-compact .tcg-card.selected::after{
  content:'✓';
  position:absolute;
  top:6px; left:6px;
  width:20px; height:20px;
  border-radius:50%;
  background:var(--violet);
  color:var(--ink);
  display:flex; align-items:center; justify-content:center;
  font-size:var(--fs-sm); font-weight:700;
  box-shadow:0 0 0 2px var(--ink-surface);
}
.tcg-trade-columns{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:16px;
  margin-bottom:16px;
}
.tcg-trade-col-title{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  text-transform:uppercase;
  letter-spacing:0.06em;
  color:var(--text-muted);
  margin-bottom:8px;
}
.tcg-trade-summary{
  font-size:var(--fs-sm);
  color:var(--text-muted);
  margin-bottom:14px;
  min-height:1.4em;
}
@media (max-width:600px){
  .tcg-trade-columns{grid-template-columns:1fr;}
}

/* --- Ouverture de booster --- 5 cartes dos tourné, retournées en cascade
   (voir openBoosterReveal(), js/tcg.js) — carte réelle en 3D (perspective
   sur le conteneur, rotateY sur chaque carte), même technique que .counter
   (le "cadran" qui bascule à la sauvegarde) mais ici pilotée par une
   classe posée en JS au bon moment plutôt qu'une seule animation fixe :
   le délai de chaque carte est dynamique (cascade), pas connu d'avance
   dans la feuille de style. */
.tcg-booster-modal{
  max-width:900px;
  background:transparent;
  border:none;
  box-shadow:none;
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
  text-align:center;
  animation:none; /* la modale elle-même n'a pas besoin du fondu modalIn générique : les cartes font déjà leur propre entrée */
}
/* Rappel de visionnage (watchlist) : une ligne discrète, couleur teal de la palette. */
.wl-reminder{background:var(--ink-surface); border:1px solid var(--line); border-left:3px solid var(--teal); border-radius:var(--r-sm); padding:10px 14px; margin-bottom:12px; font-size:var(--fs-md); color:var(--text-muted);}
/* Badge d'échanges reçus en attente (ligne Cartes du profil). */
.tcg-trade-badge{display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px; padding:0 6px; margin-left:8px; border-radius:var(--r-full); background:var(--coral); color:#120f1a; font-size:var(--fs-sm); font-weight:700; vertical-align:middle;}
.tcg-trade-badge[hidden]{display:none;}
/* Détail d'une carte (tcgCardOverlay) : carte centrée, rareté en bordure. */
.tcg-card-modal{max-width:340px; text-align:center;}
/* Récap de l'année : écrans superposés en fondu, points de progression en bas. */
.recap-modal{max-width:380px; text-align:center; min-height:300px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; cursor:pointer;}
.recap-slides{position:relative; width:100%; min-height:200px;}
.recap-slide{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; opacity:0; transform:translateY(8px); transition:opacity .45s ease, transform .45s ease; pointer-events:none;}
.recap-slide.active{opacity:1; transform:none;}
.recap-big{font-size:var(--fs-2xl); font-weight:700; color:var(--gold); line-height:1.1;}
.recap-big.recap-title{font-size:var(--fs-xl); color:var(--text);}
.recap-unit{font-size:var(--fs-md); color:var(--text-muted);}
.recap-line{color:var(--text-muted); font-size:var(--fs-md);}
.recap-line b{color:var(--text);}
.recap-dots{display:flex; gap:8px;}
.recap-dot{width:8px; height:8px; border-radius:50%; background:var(--line); transition:background .3s;}
.recap-dot.active{background:var(--violet);}
@media (prefers-reduced-motion: reduce){ .recap-slide{transition:none; transform:none;} }
.tcg-card-detail{display:flex; flex-direction:column; align-items:center; gap:10px; border:2px solid var(--line); border-radius:var(--r-md); padding:16px; background:var(--ink-surface);}
.tcg-card-detail.rarity-rare{border-color:var(--rarity-rare);}
.tcg-card-detail.rarity-epique{border-color:var(--violet);}
.tcg-card-detail.rarity-legendaire{border-color:var(--rarity-legendary); box-shadow:0 0 22px rgba(232,163,61,0.35);}
.tcg-card-detail-img{width:200px; height:300px; object-fit:cover; border-radius:var(--r-sm);}
.tcg-card-detail-name{font-weight:700; font-size:var(--fs-md);}
.tcg-card-detail-actions{display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin-top:14px;}
.tcg-intro{display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; background:var(--ink-surface); border:1px solid var(--line); border-left:3px solid var(--violet); border-radius:var(--r-sm); padding:12px 14px; margin-bottom:14px; font-size:var(--fs-md); color:var(--text-muted);}
.tcg-intro strong{color:var(--text);}
.tcg-intro-text{flex:1 1 260px;}
.tcg-completion-row{display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:10px; margin-bottom:12px;}
.tcg-export-actions{display:flex; gap:8px; flex-wrap:wrap;}
.tcg-booster-actions{display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:14px;}
.tcg-booster-pack{
  display:flex;
  justify-content:center;
  flex-wrap:wrap;
  gap:18px;
  perspective:1200px;
  margin-bottom:24px;
}
.tcg-booster-card{
  width:140px; height:210px;
  position:relative;
  transform-style:preserve-3d;
  transition:transform .7s cubic-bezier(.34,1.56,.64,1);
}
.tcg-booster-card.revealed{transform:rotateY(180deg);}
/* Téléphone : 5 cartes côte à côte ne tiennent pas, on garde le même ratio
   2:3 en plus petit pour que l'ouverture tienne dans l'écran. */
@media (max-width:600px){
  .tcg-booster-pack{gap:10px;}
  .tcg-booster-card{width:108px; height:162px;}
}
.tcg-booster-card-face{
  position:absolute; inset:0;
  backface-visibility:hidden;
  -webkit-backface-visibility:hidden;
  border-radius:var(--r-md);
  border:2px solid var(--line);
  overflow:hidden;
  display:flex; align-items:center; justify-content:center;
}
.tcg-booster-card-back{
  background:linear-gradient(160deg, var(--violet) 0%, var(--plum) 100%);
}
/* Dos de carte (v3.1) — même wordmark que l'entête plutôt qu'une icône :
   le K (variante E) vit sur l'icône de l'app, pas à côté du nom en toutes
   lettres (retour utilisateur, voir le commentaire sur #homeLink,
   index.html) — même règle ici qu'ailleurs dans l'appli. */
.tcg-booster-wordmark{
  font-family:var(--font-display); font-weight:800; font-size:var(--fs-lg);
  letter-spacing:0.04em; color:var(--text);
}
.tcg-booster-card-front{
  transform:rotateY(180deg);
  background:var(--surface-2);
  display:block;
}
.tcg-booster-card-front img{width:100%; height:100%; object-fit:cover; display:block;}
.tcg-booster-card-front .tcg-card-name{font-size:var(--fs-sm); padding:20px 10px 9px;}
.tcg-booster-card-front.rarity-rare{border-color:var(--rarity-rare); box-shadow:0 0 20px rgba(111,163,159,0.4);}
.tcg-booster-card-front.rarity-epique{border-color:var(--violet); box-shadow:0 0 24px rgba(79,207,255,0.5);}
.tcg-booster-card-front.rarity-legendaire{
  border-color:var(--rarity-legendary);
  box-shadow:0 0 30px rgba(232,163,61,0.6);
  animation:tcgLegendaryGlow 1.6s ease-in-out infinite;
}
@keyframes tcgLegendaryGlow{
  0%, 100%{box-shadow:0 0 30px rgba(232,163,61,0.6);}
  50%{box-shadow:0 0 44px rgba(232,163,61,0.85);}
}
/* Sheen holographique à la révélation (retour utilisateur, étude
   comparative sur un simulateur d'ouverture de boosters Pokémon : "pendant
   le chargement d'une carte rare, un dégradé holographique en biais
   s'affiche — le joueur sait que c'est du lourd avant même que l'artwork
   charge") — adapté ici en un passage lumineux en diagonale UNE FOIS, au
   moment précis où la carte finit de se retourner (.revealed, déjà posé
   par openBoosterReveal(), js/tcg.js), jamais sur une Commune. Décalage de
   .55s : la face avant est masquée par backface-visibility jusqu'à ce que
   la rotation 3D dépasse 90°, inutile de jouer le sheen avant que la carte
   soit effectivement visible (le flip dure .7s, voir .tcg-booster-card). */
.tcg-booster-card-front.rarity-rare::after,
.tcg-booster-card-front.rarity-epique::after,
.tcg-booster-card-front.rarity-legendaire::after{
  content:'';
  position:absolute; inset:0;
  background:linear-gradient(115deg, transparent 25%, rgba(255,255,255,0.6) 40%, rgba(255,255,255,0.2) 50%, transparent 65%);
  background-size:250% 250%;
  background-position:-150% -150%;
  pointer-events:none;
  opacity:0;
}
.tcg-booster-card.revealed .tcg-booster-card-front.rarity-rare::after,
.tcg-booster-card.revealed .tcg-booster-card-front.rarity-epique::after,
.tcg-booster-card.revealed .tcg-booster-card-front.rarity-legendaire::after{
  animation:tcgHoloSweep 1.1s ease-out .55s both;
}
@keyframes tcgHoloSweep{
  0%{opacity:0; background-position:-150% -150%;}
  15%{opacity:1;}
  70%{opacity:1;}
  100%{opacity:0; background-position:150% 150%;}
}
@media (prefers-reduced-motion:reduce){
  .tcg-booster-card{transition:none;}
  .tcg-booster-card-front.rarity-legendaire{animation:none;}
  .tcg-booster-card.revealed .tcg-booster-card-front.rarity-rare::after,
  .tcg-booster-card.revealed .tcg-booster-card-front.rarity-epique::after,
  .tcg-booster-card.revealed .tcg-booster-card-front.rarity-legendaire::after{animation:none; opacity:0;}
}
@media (max-width:600px){
  .tcg-booster-card{width:108px; height:162px;}
}

/* --- Assistant (chatbot, retour utilisateur, "un début") --- voir
   js/chatbot.js. Bulles façon messagerie (bot à gauche, toi à droite) —
   langage déjà familier, pas la peine d'inventer une présentation à part
   pour une conversation. */
.chatbot-modal{
  max-width:480px;
  display:flex;
  flex-direction:column;
  height:min(600px, 80vh);
}
.chatbot-messages{
  flex:1;
  overflow-y:auto;
  display:flex;
  flex-direction:column;
  gap:10px;
  padding:4px 2px 12px;
  margin-bottom:12px;
}
.chatbot-msg{
  max-width:85%;
  padding:10px 14px;
  border-radius:var(--r-md);
  font-size:var(--fs-sm);
  line-height:1.55;
  white-space:pre-line;
}
.chatbot-msg-bot{
  align-self:flex-start;
  background:var(--surface-2);
  border:1px solid var(--line);
  border-bottom-left-radius:4px;
}
.chatbot-msg-user{
  align-self:flex-end;
  background:var(--violet);
  color:var(--ink);
  border-bottom-right-radius:4px;
}
.chatbot-msg-error{
  align-self:flex-start;
  background:rgba(255,107,87,0.1);
  border:1px solid var(--coral-dim);
  color:var(--coral);
  border-bottom-left-radius:4px;
}
/* Indicateur "en train d'écrire" — 3 points qui pulsent en cascade, même
   principe que .skeleton-block ailleurs (une attente qui se voit plutôt
   qu'un silence plat) mais en boucle légère plutôt qu'un dégradé qui
   défile : une réponse de chatbot n'a pas de progression connue à
   montrer, juste "ça travaille". */
.chatbot-typing{display:inline-flex; gap:4px; padding:3px 2px;}
.chatbot-typing span{
  width:6px; height:6px;
  border-radius:50%;
  background:var(--text-muted);
  animation:chatbotTypingPulse 1.2s ease-in-out infinite;
}
.chatbot-typing span:nth-child(2){animation-delay:.15s;}
.chatbot-typing span:nth-child(3){animation-delay:.3s;}
@keyframes chatbotTypingPulse{
  0%, 60%, 100%{opacity:0.3; transform:scale(0.8);}
  30%{opacity:1; transform:scale(1);}
}
@media (prefers-reduced-motion:reduce){
  .chatbot-typing span{animation:none; opacity:0.6;}
}
/* Chaque message glisse doucement à son apparition (et pas seulement les
   suggestions) : l'échange se lit comme une conversation, pas comme un
   bloc qui se redessine à chaque réponse. */
/* Seule la dernière bulle s'anime : la conversation est redessinée entière à
   chaque message, il ne faut pas faire clignoter les anciennes. */
.chatbot-msg:last-child{animation:chatbotMsgIn .28s ease both;}
@keyframes chatbotMsgIn{from{opacity:0; transform:translateY(4px);} to{opacity:1; transform:none;}}
@media (prefers-reduced-motion: reduce){
  .chatbot-msg:last-child{animation:none;}
}
.chatbot-suggestions{display:flex; flex-wrap:wrap; gap:8px; margin:4px 0 2px;}
.chatbot-chip{
  font:inherit; font-size:var(--fs-md); padding:7px 12px; border-radius:var(--r-full);
  border:1px solid var(--line); background:var(--ink-surface); color:var(--text);
  cursor:pointer; transition:transform .15s ease, border-color .15s ease, background .15s ease;
  animation:chatbotChipIn .35s ease both;
}
.chatbot-chip:nth-child(2){animation-delay:.04s;}
.chatbot-chip:nth-child(3){animation-delay:.08s;}
.chatbot-chip:nth-child(4){animation-delay:.12s;}
.chatbot-chip:nth-child(5){animation-delay:.16s;}
.chatbot-chip:nth-child(6){animation-delay:.2s;}
.chatbot-chip:hover, .chatbot-chip:focus-visible{border-color:var(--violet); transform:translateY(-1px);}
.chatbot-chip:active{transform:scale(.97);}
@keyframes chatbotChipIn{from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:none;}}
@media (prefers-reduced-motion: reduce){
  .chatbot-chip{animation:none; transition:none;}
  .chatbot-chip:hover, .chatbot-chip:focus-visible{transform:none;}
}
.chatbot-input-row{display:flex; gap:10px; align-items:flex-end;}
.chatbot-input-row textarea{
  flex:1;
  resize:none;
  max-height:120px;
  line-height:1.4;
}
@media (max-width:600px){
  .chatbot-modal{height:85vh;}
}

/* Onglet Retours (admin, v2.1) — une ligne par retour, regroupées par
   catégorie dans des .stats-section (mêmes classes que l'onglet
   Statistiques, pas un nouveau conteneur). */
.feedback-row{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:14px;
  padding:12px 0;
  border-top:1px solid var(--line);
}
.feedback-row:first-of-type{border-top:none;}
.feedback-row.is-resolved{opacity:0.5;}
.feedback-row-main{min-width:0;}
.feedback-message{margin:0; font-size:var(--fs-sm); line-height:1.5;}
.feedback-meta{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:5px;
}
.feedback-row .btn{flex:none; font-size:var(--fs-xs); padding:8px 12px;}

/* Onglet Stats & logs (admin, v2.1) — les compteurs réutilisent
   .stat-tile/.stat-value/.stat-label tels quels (mêmes classes que la
   modale Statistiques personnelle, js/stats.js) ; seul le journal brut
   ci-dessous a besoin de nouvelles classes. */
.event-row{
  display:flex;
  align-items:baseline;
  gap:10px;
  padding:8px 0;
  border-top:1px solid var(--line);
  font-size:var(--fs-sm);
}
.event-row:first-of-type{border-top:none;}
.event-type-badge{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:700;
  letter-spacing:0.03em;
  text-transform:uppercase;
  padding:2px 7px;
  border-radius:var(--r-full);
  flex:none;
  background:var(--surface-2);
  border:1px solid var(--line);
  color:var(--text-muted);
}
.event-type-error{color:var(--coral); border-color:var(--coral-dim); background:rgba(255,107,87,0.08);}
.event-type-signup{color:var(--green); border-color:var(--green-dim); background:var(--green-wash);}
.event-type-pwa_install{color:var(--violet); border-color:var(--violet-dim); background:var(--violet-wash);}
.event-detail{flex:1; min-width:0; color:var(--text-muted); overflow-wrap:anywhere;}
.event-date{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  opacity:0.7;
  flex:none;
  white-space:nowrap;
}

.ach-hidden-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
  gap:14px;
}
.ach-hidden-card{
  background:linear-gradient(160deg, var(--ink-surface) 0%, var(--surface) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:14px 12px;
  text-align:center;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:4px;
}
.ach-hidden-card.locked{opacity:0.55;}
.ach-hidden-icon{font-size:var(--fs-lg);}
/* Même correctif que .film-sub/.wl-title/.wl-note (v3.2, retour
   utilisateur : "tous les carrés [aient] la même longueur et largeur")
   — titre et description de succès sont du texte libre, de longueur très
   variable d'un succès à l'autre ; sans limite, une carte à description
   longue grandissait par rapport à ses voisines dans la même grille. */
.ach-hidden-title{
  font-family:var(--font-display); font-weight:600; font-size:var(--fs-sm);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}
.ach-hidden-card.locked .ach-hidden-title{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-weight:400;
  color:var(--text-muted);
  letter-spacing:0.1em;
}
.ach-hidden-desc{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  line-height:1.4;
  display:-webkit-box;
  -webkit-box-orient:vertical;
  -webkit-line-clamp:3;
  overflow:hidden;
}
/* v2.1.x, "Halation" plus loin : même bordure en dégradé que .ach-summary
   ci-dessus (par masque, voir le commentaire dessus), réservée aux 2 seuls
   états "à célébrer" de cette grille — un palier poussé jusqu'à l'or, un
   succès secret enfin débloqué. Jamais sur un palier en cours ni un succès
   verrouillé : la carte reste neutre tant qu'il n'y a rien de spécial à
   montrer, cohérent avec la restriction déjà appliquée ailleurs (jamais de
   verre sur l'en-tête, jamais de dégradé de texte hors "Note moyenne"). */
.ach-tier-card.maxed,
.ach-hidden-card.unlocked{
  position:relative;
  border-color:transparent;
  box-shadow:0 0 18px 2px rgba(232,163,61,0.16);
}
.ach-tier-card.maxed::before,
.ach-hidden-card.unlocked::before{
  content:'';
  position:absolute; inset:0;
  border-radius:inherit;
  padding:1.5px;
  background:linear-gradient(120deg, var(--gold), var(--bronze), var(--violet));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask-composite:exclude;
  pointer-events:none;
}

/* --- Interface admin (js/admin.js) : réutilise .wl-row (déjà utilisé pour
   la watchlist/propositions) pour les lignes succès/happenings, plutôt que
   de redéfinir un layout en ligne de plus. */
.admin-modal{max-width:780px;}
.admin-row-disabled{opacity:0.5;}
.admin-tier-input{width:64px; padding:6px 8px; font-family:var(--font-text); font-variant-numeric:tabular-nums;}
.admin-row .wl-actions{flex-wrap:wrap; justify-content:flex-end; row-gap:6px;}
.wl-add-form{
  border-bottom:1px solid var(--line);
  padding-bottom:20px;
  margin-bottom:20px;
}
.wl-add-form .field:last-of-type{margin-bottom:14px;}

/* "Surprends-moi" : roulette façon ouverture de caisse (retour utilisateur,
   motion design) — une bande d'affiches défile puis ralentit pile sur le
   tirage, repère doré au centre, plutôt qu'un résultat qui apparaît d'un
   coup. Même famille que .oldboy-corridor/.paprika-parade (bande qui
   défile dans un cadre fermé), mais la distance d'arrivée n'est PAS connue
   à l'avance ici (dépend du tirage et de la largeur réelle de l'écran) :
   transform + distance calculés en JS à chaque tirage
   (spinSurpriseRoulette(), js/watchlist.js), cette feuille ne pose que la
   transition et l'habillage. */
.surprise-roulette{
  position:relative;
  height:150px;
  margin-bottom:18px;
  border-radius:var(--r-md);
  border:1px solid var(--line);
  background:var(--surface-2);
  overflow:hidden;
  /* Fondu aux deux bords plutôt qu'une coupe nette des affiches qui
     entrent/sortent du cadre. */
  -webkit-mask-image:linear-gradient(90deg, transparent, black 10%, black 90%, transparent);
  mask-image:linear-gradient(90deg, transparent, black 10%, black 90%, transparent);
}
.surprise-roulette-track{
  display:flex;
  align-items:center;
  gap:10px;
  height:100%;
  padding:0 10px;
  will-change:transform;
  transform:translateX(0);
  /* Décélération franche et longue (façon ouverture de caisse) : départ
     rapide, arrêt net — jamais linéaire (ne "ralentirait" pas) ni un simple
     ease-out générique (pas assez marqué pour le suspense visé). Rejoué à
     chaque tirage depuis translateX(0) (voir spinSurpriseRoulette()) :
     transition posée ici une fois pour toutes, jamais redéfinie en JS. */
  transition:transform 5s cubic-bezier(.1,.65,.2,1);
}
@media (prefers-reduced-motion:reduce){
  .surprise-roulette-track{transition:none;}
}
.surprise-roulette-item{
  flex:none;
  width:88px; height:130px;
  border-radius:var(--r-sm);
  overflow:hidden;
  border:2px solid var(--line);
  background:var(--ink-surface);
  transition:border-color .2s, box-shadow .2s, transform .2s;
}
.surprise-roulette-item img{width:100%; height:100%; object-fit:cover; display:block;}
.surprise-roulette-item .film-poster-placeholder{width:100%; height:100%; display:flex; align-items:center; justify-content:center; opacity:0.5;}
.surprise-roulette-item .film-poster-placeholder svg{width:42%; height:42%;}
/* Case gagnante (posée une fois la bande arrêtée, voir
   spinSurpriseRoulette()) — même langage "Halation" que
   .primary-tab[aria-selected] (halo permanent, pas juste au survol) : LA
   case qui compte doit se voir au premier regard, pas seulement par sa
   position sous le repère. */
.surprise-roulette-item.winner{
  border-color:var(--gold);
  box-shadow:0 0 0 1px rgba(232,163,61,0.4), 0 0 26px 4px rgba(232,163,61,0.45);
  transform:scale(1.04);
  animation:surpriseWinnerPulse .6s ease-out;
}
@keyframes surpriseWinnerPulse{
  0%{transform:scale(1);}
  45%{transform:scale(1.12);}
  100%{transform:scale(1.04);}
}
@media (prefers-reduced-motion:reduce){
  .surprise-roulette-item.winner{animation:none;}
}
/* Repère central : la case qui s'arrête pile dessous est le tirage — même
   idée qu'un repère d'ouverture de caisse, teinte or plutôt qu'une couleur
   neutre pour rester dans la palette Halation plutôt qu'en emprunter une
   nouvelle. */
.surprise-roulette-marker{
  position:absolute;
  top:0; bottom:0; left:50%;
  width:2px;
  background:var(--gold);
  transform:translateX(-50%);
  box-shadow:0 0 12px 2px rgba(232,163,61,0.6);
  pointer-events:none;
}
.surprise-roulette-marker::before, .surprise-roulette-marker::after{
  content:'';
  position:absolute;
  left:50%;
  width:0; height:0;
  border-left:5px solid transparent;
  border-right:5px solid transparent;
  transform:translateX(-50%);
}
.surprise-roulette-marker::before{top:-1px; border-top:6px solid var(--gold);}
.surprise-roulette-marker::after{bottom:-1px; border-bottom:6px solid var(--gold);}
@media (max-width:600px){
  .surprise-roulette{height:118px;}
  .surprise-roulette-item{width:70px; height:103px;}
}

.surprise-info{text-align:center;}
.surprise-info .wl-title{font-size:var(--fs-lg);}
.surprise-info .wl-note{margin-top:8px; max-width:44ch; margin-left:auto; margin-right:auto;}
.surprise-on{margin-top:var(--sp-1); font-size:var(--fs-sm); color:var(--text-2);}
.surprise-message{margin:var(--sp-5) 0; text-align:center; font-size:var(--fs-sm); color:var(--text-2);}

/* Plateforme du tirage (retour de Flix, 22/10) : les pastilles de « Mes
   abonnements » (.wl-sub-chip), en choix unique. Une seule ligne qui défile
   au téléphone, pour garder la bande à sa place. */
.surprise-services{position:relative; display:flex; flex-wrap:wrap; gap:var(--sp-2); margin:0 0 var(--sp-4);}
.surprise-services .wl-sub-chip{flex:none;}
.surprise-services .wl-sub-chip[aria-checked="true"]{color:var(--text-1); border-color:var(--accent); background:var(--accent-wash);}
.surprise-services .wl-sub-chip:disabled{opacity:0.45; cursor:default;}
.surprise-services .wl-sub-chip:disabled:hover{color:var(--text-2); border-color:var(--line);}
.surprise-services .wl-sub-chip[aria-checked="true"]:disabled{opacity:1;}
/* « + n autres » : les plateformes hors abonnements, en simple lien. */
.surprise-services-more{
  flex:none; height:32px; padding:0 var(--sp-2);
  background:none; border:none; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2);
  transition:color var(--dur-fast);
}
.surprise-services-more:hover{color:var(--text-1);}
.surprise-services-wait{font-size:var(--fs-xs); color:var(--text-2); line-height:32px;}
@media (max-width:600px){
  /* Fondu à droite : on voit qu'il y a d'autres plateformes plus loin. */
  .surprise-services{
    flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch;
    padding-right:var(--sp-5);
    -webkit-mask-image:linear-gradient(90deg, black calc(100% - var(--sp-5)), transparent);
    mask-image:linear-gradient(90deg, black calc(100% - var(--sp-5)), transparent);
  }
  .surprise-services::-webkit-scrollbar{display:none;}
}

/* ============================================================
   .wl-row — ligne compacte générique, réutilisée par de nombreuses
   listes très différentes (amis, membres de groupe, propositions, votes,
   admin...) : reste une simple rangée horizontale flex. Watchlist/Top
   (voir #wlList/#topList plus bas) sont les deux seules listes de ce
   type à affiches qui reçoivent en plus le mode grille de la bascule —
   les autres (amis, groupes...) n'ont pas de bouton de bascule et
   n'ont donc jamais à changer de disposition.
   ============================================================ */
/* padding/margin un peu plus généreux (retour utilisateur : "on manque
   d'espace") — alignés sur .film-row, la même ligne mais côté catalogue. */
.wl-row{
  display:flex;
  align-items:center;
  gap:14px;
  background:linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 220%);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:15px 18px;
  margin-bottom:10px;
  position:relative;
  perspective:500px; /* v2.8 : vraie profondeur pour la bascule de .counter, voir counterFlip plus haut */
}
/* Série suivie (v2.3) : toute la ligne ouvre la fiche détail au clic (comme
   .film-row), voir renderTrackedShows(), js/series.js. #topList (js/top.js)
   a le même traitement via .top-row plus haut dans ce fichier. */
/* Watchlist : survol doux (même langage que les séries et les suggestions),
   désactivé avec prefers-reduced-motion. */
#wlList .wl-row{transition:border-color .15s, transform .15s, box-shadow .15s;}
/* Entrée en cascade des premières lignes (les suivantes apparaissent sans
   délai : une longue liste ne doit pas attendre). */
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row{animation:wlRowIn .3s ease both;}
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row:nth-child(2){animation-delay:.04s;}
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row:nth-child(3){animation-delay:.08s;}
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row:nth-child(4){animation-delay:.12s;}
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row:nth-child(5){animation-delay:.16s;}
:is(#wlList, #friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList) > .wl-row:nth-child(n+6){animation-delay:0s;}
@keyframes wlRowIn{from{opacity:0; transform:translateY(6px);} to{opacity:1; transform:none;}}

#wlList .wl-row:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}
@media (prefers-reduced-motion: reduce){
  #wlList .wl-row, #wlList .wl-row:hover, :is(#friendsList, #groupsList, #seriesList, #suggestionsList, #friendSuggestionsList, #wlList) > .wl-row{transition:none; transform:none; animation:none;}
}
#seriesList .wl-row{cursor:pointer; transition:border-color .15s, transform .15s, box-shadow .15s;}
#seriesList .wl-row:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}
/* Watchlist (retour utilisateur : "je ne peux pas cliquer sur le film pour
   arriver sur sa fiche de présentation comme dans le top"). Seulement les
   lignes qui ONT une fiche TMDB (voir renderWatchlist(), js/watchlist.js) :
   un titre ajouté à la main sans correspondance TMDB n'a nulle part où
   mener. Même traitement au survol que #seriesList/.top-row juste au-dessus,
   pour rester cohérent partout où une ligne ouvre une fiche film au clic. */
.wl-row-clickable{cursor:pointer; transition:border-color .15s, transform .15s, box-shadow .15s;}
.wl-row-clickable:hover{border-color:var(--violet-dim); transform:translateY(-1px); box-shadow:0 10px 24px rgba(0,0,0,0.35);}
/* Suggestions (v2.1.x, js/suggestions.js) : halo au survol seulement — pas
   en permanence, la liste reste calme au repos — pour distinguer "on te
   propose ça" d'une ligne de watchlist normale juste en dessous. */
#suggestionsList .wl-row{transition:border-color .15s, transform .15s, box-shadow .15s;}
#suggestionsList .wl-row:hover{
  border-color:var(--violet-dim);
  transform:translateY(-1px);
  box-shadow:0 10px 24px rgba(0,0,0,0.35), 0 0 24px 3px rgba(79,207,255,0.18);
}
.wl-main{flex:1; min-width:0;}
.wl-title{
  font-family:var(--font-display);
  font-weight:600;
  font-size:var(--fs-md);
  overflow-wrap:anywhere;
}
.wl-year{color:var(--text-muted); font-weight:400;}
.wl-note{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:4px;
  line-height:1.4;
}
/* Alerte "un ami a noté un film de ta watchlist" (retour utilisateur,
   js/watchlist.js) — doré comme .new-episode-badge (séries) : même rôle,
   un signal qui invite à l'action plutôt qu'un simple statut. */
.wl-friend-rating{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--gold);
  margin-top:4px;
  line-height:1.4;
}
.wl-actions{display:flex; gap:6px; flex:none;}
.wl-actions .btn{padding:8px 12px; font-size:var(--fs-xs);}

/* Bouton « ⋯ » et son menu (openRowMenu(), js/ui.js) — code de design :
   bouton icône 40px sans fond au repos, menu = surface flottante. */
.row-more-btn{
  display:flex; align-items:center; justify-content:center;
  width:40px; height:40px; flex:none;
  border:none; border-radius:var(--r-full); background:none;
  color:var(--text-muted); cursor:pointer;
  transition:background-color .15s, color .15s, transform .1s ease-out;
}
.row-more-btn svg{width:20px; height:20px;}
.row-more-btn:hover, .row-more-btn[aria-expanded="true"]{background:var(--surface-2); color:var(--text);}
.row-more-btn:active{transform:scale(0.97);}
.row-menu{
  position:fixed; z-index:600;
  display:flex; flex-direction:column; min-width:210px; padding:6px;
  background:rgba(var(--glass-rgb),0.92);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid rgba(var(--text-rgb),0.09); border-radius:var(--r-md);
  box-shadow:0 16px 40px rgba(0,0,0,0.45);
  animation:menuIn .15s ease-out;
}
.row-menu-item{
  display:block; width:100%; text-align:left;
  padding:10px 12px; border:none; border-radius:var(--r-sm); background:none;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:500;
  color:var(--text); cursor:pointer;
}
.row-menu-item:hover, .row-menu-item:focus-visible{background:var(--surface-2); outline:none;}
.row-menu-item.danger{color:var(--coral);}
@media (prefers-reduced-motion: reduce){ .row-menu{animation:none;} }

/* Note rapide depuis la watchlist (retour utilisateur : convertir un item
   en film noté avec une seule note globale, sans ouvrir la grille complète)
   — formulaire compact, replié par défaut, révélé sous le titre au clic sur
   "⚡ Note rapide" (voir js/watchlist.js). */
.wl-quick-form{display:flex; align-items:center; gap:6px; margin-top:8px; flex-wrap:wrap;}
.wl-quick-note-input{
  width:68px; padding:6px 8px; border-radius:var(--r-sm);
  border:1px solid var(--line); background:var(--surface-2); color:var(--text);
  font-family:var(--font-text); font-variant-numeric:tabular-nums; font-size:var(--fs-sm); text-align:center;
}
.wl-quick-form .btn{padding:6px 10px; font-size:var(--fs-xs);}

.comment-list{display:flex; flex-direction:column; gap:10px; margin-bottom:14px;}
.comment-item{
  display:flex;
  align-items:flex-start;
  gap:10px;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:10px 12px;
}
.comment-body-wrap{flex:1; min-width:0;}
.comment-author{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--violet);
  margin-bottom:3px;
}
.comment-body{font-size:var(--fs-sm); line-height:1.5; word-wrap:break-word;}
/* Réactions rapides sur une critique d'ami (retour utilisateur) — palette
   fixe toujours visible sous le commentaire, voir commentReactionsHtml()
   (js/filmDetail.js). Pastilles discrètes au repos (pas de bordure tant
   qu'on n'a pas réagi) pour ne pas alourdir chaque commentaire de 5
   contours ; seule celle où l'on a réagi (.active) se détache vraiment. */
.comment-reactions{
  display:flex;
  gap:4px;
  margin-top:6px;
}
.reaction-btn{
  display:flex;
  align-items:center;
  gap:3px;
  background:none;
  border:1px solid transparent;
  border-radius:var(--r-full);
  padding:2px 7px;
  font-size:var(--fs-sm);
  line-height:1.4;
  cursor:pointer;
  transition:background .12s, border-color .12s;
}
.reaction-btn:hover{background:var(--surface-2);}
.reaction-btn.active{border-color:var(--violet-dim); background:var(--violet-wash);}
.reaction-count{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
}
.reaction-btn.active .reaction-count{color:var(--violet);}
.comment-delete{
  background:none;
  border:none;
  color:var(--text-muted);
  cursor:pointer;
  font-size:var(--fs-sm);
  flex:none;
  padding:2px 4px;
}
.comment-delete:hover{color:var(--coral);}
.comment-input-row{display:flex; gap:8px; align-items:flex-end;}
.comment-input-row textarea{flex:1;}

/* Visionnages (dans le formulaire d'un film) */
.viewings-list{display:flex; flex-direction:column; gap:4px; margin-bottom:10px;}
.viewing-row{
  display:flex;
  align-items:center;
  gap:8px;
  background:var(--ink-surface);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:7px 10px;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
}
.viewing-date{color:var(--text); white-space:nowrap;}
.viewing-first{color:var(--text-muted); font-size:var(--fs-xs); text-transform:uppercase; letter-spacing:0.04em;}
.viewing-note{color:var(--text-muted); font-style:italic; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.viewing-remove{
  margin-left:auto;
  background:none;
  border:none;
  color:var(--text-muted);
  cursor:pointer;
  font-size:var(--fs-sm);
  padding:2px 4px;
  flex:none;
}
.viewing-remove:hover{color:var(--coral);}
.viewings-add-row{display:flex; gap:8px; flex-wrap:wrap;}
.viewings-add-row input[type="date"]{
  background:var(--surface);
  border:1px solid var(--line);
  color:var(--text);
  font-family:var(--font-text);
  font-size:var(--fs-sm);
  padding:9px 10px;
  border-radius:var(--r-sm);
  flex:none;
}
.viewings-add-row input[type="text"]{flex:1; min-width:140px;}

/* Journal chronologique */
.journal-modal{max-width:640px;}
.journal-month{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.08em;
  text-transform:uppercase;
  color:var(--violet);
  margin:20px 0 8px;
}
.journal-month:first-child{margin-top:0;}
.journal-row{
  display:flex;
  align-items:center;
  gap:14px;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:10px 14px;
  margin-bottom:6px;
}
.journal-main{flex:1; min-width:0;}
.journal-title{font-family:var(--font-display); font-weight:600; font-size:var(--fs-md);}
.journal-note{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  margin-top:3px;
  font-style:italic;
}
.journal-date{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  flex:none;
  text-transform:capitalize;
}

/* Nouveautés (v2.1+) — voir js/changelog.js. Même gabarit de modale que le
   Journal juste au-dessus (.journal-modal), une entrée par mise à jour
   plutôt qu'un tableau : le contenu (texte libre, longueur variable) s'y
   prête mieux qu'une rangée .wl-row pensée pour poster/titre/actions. */
.changelog-entry{
  border-bottom:1px solid var(--line-soft);
  padding:16px 0;
}
.changelog-entry:first-child{padding-top:0;}
.changelog-entry:last-child{border-bottom:none; padding-bottom:0;}
.changelog-entry-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:10px;
  margin-bottom:8px;
}
.changelog-version{
  display:inline-block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:600;
  letter-spacing:0.04em;
  color:var(--violet);
  background:var(--violet-wash);
  border:1px solid var(--violet-dim);
  border-radius:var(--r-full);
  padding:3px 10px;
}
.changelog-date{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  text-transform:capitalize;
}
.changelog-title{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--fs-md);
  margin-bottom:4px;
}
.changelog-body{
  color:var(--text-muted);
  font-size:var(--fs-sm);
  line-height:1.55;
}
/* Classées par catégorie (changelogBodyHtml(), js/changelog.js) : une ligne
   repliable par titre de la note, même filet et même étiquette en capitales
   que les sections de Bientôt (.up-label). */
.changelog-title + .changelog-body{margin-top:var(--sp-3);}
.cl-cat{border-top:1px solid var(--line-soft);}
.cl-cat:last-child{border-bottom:1px solid var(--line-soft);}
/* Entre deux notes, le filet de .changelog-entry suffit. */
.changelog-entry:not(:last-child) .cl-cat:last-child{border-bottom:0;}
.cl-cat summary{display:flex; align-items:center; gap:var(--sp-2); min-height:44px; cursor:pointer; list-style:none;}
.cl-cat summary::-webkit-details-marker{display:none;}
.cl-cat summary:focus-visible{outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--r-sm);}
.cl-cat-name{flex:1; min-width:0; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; color:var(--text-1);}
.cl-cat-n{font-size:var(--fs-xs); color:var(--text-3); font-variant-numeric:tabular-nums;}
.cl-cat-chev{display:flex; color:var(--text-3); transition:transform var(--dur-fast);}
.cl-cat-chev svg{width:14px; height:14px;}
.cl-cat[open] .cl-cat-chev{transform:rotate(90deg);}
.cl-cat summary:hover .cl-cat-chev, .cl-cat[open] .cl-cat-n{color:var(--text-2);}
.cl-items{display:grid; gap:var(--sp-2); padding:0 0 var(--sp-3) 1.1em;}
.cl-cat .cl-items{padding-top:var(--sp-1);}
.cl-items li{color:var(--text-2); font-size:var(--fs-sm); line-height:1.5;}
.cl-items li::marker{color:var(--accent);}
.cl-text{color:var(--text-2); font-size:var(--fs-sm); line-height:1.5;}
@media (prefers-reduced-motion:reduce){ .cl-cat-chev{transition:none;} }

/* Discrète par construction (v2.1, demande explicite) : même traitement
   visuel que .version-tag (Space Mono, petite taille, opacité réduite) —
   une signature, pas un élément qui réclame l'attention. */
.app-footer{
  text-align:center;
  padding:28px 16px 20px;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  letter-spacing:0.04em;
  color:var(--text-muted);
}
/* opacité posée sur chaque élément, pas sur .app-footer lui-même : un
   opacity sur le conteneur créerait un contexte de composition qui
   plafonnerait aussi ":hover" du bouton en dessous — impossible de le
   faire ressortir à l'interaction sans ça. */
.app-footer-credit{opacity:0.5;}
.app-footer-link{
  background:none;
  border:none;
  padding:0;
  font:inherit;
  color:var(--text-muted);
  opacity:0.5;
  text-decoration:underline;
  text-underline-offset:2px;
  cursor:pointer;
  transition:opacity .15s;
}
.app-footer-link:hover, .app-footer-link:focus-visible{opacity:0.9;}
/* Blanc pur (pas un ton de la palette) : la fiabilité du scan passe avant
   la cohérence de marque sur ce seul élément — c'est le rôle entier du
   cadre. */
.share-qr-frame{
  width:180px; height:180px;
  margin:0 auto 18px;
  padding:12px;
  background:#ffffff;
  border-radius:var(--r-md);
  display:flex;
  align-items:center;
  justify-content:center;
}
.share-qr-frame img, .share-qr-frame canvas, .share-qr-frame table{display:block;}
.share-modal .public-profile-link-row{margin-bottom:16px;}
.toast{
  position:fixed;
  bottom:24px;
  left:50%;
  transform:translateX(-50%) translateY(10px);
  background:var(--surface-2);
  border:1px solid var(--line);
  color:var(--text);
  padding:10px 18px;
  border-radius:var(--r-sm);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  opacity:0;
  transition:opacity .2s, transform .2s;
  pointer-events:none;
  z-index:100;
}
.toast.show{opacity:1; transform:translateX(-50%) translateY(0);}

/* Toast d'annulation (undo), voir showUndoToast() dans js/ui.js — même
   position que .toast mais interactive (pointer-events auto) et avec un
   bouton, donc un peu plus large/haute ; les deux ne s'affichent jamais en
   même temps dans la pratique (une suppression n'émet pas aussi un
   showToast()), pas la peine de les empiler proprement l'une sur l'autre. */
.undo-toast{
  position:fixed;
  bottom:24px;
  left:50%;
  transform:translateX(-50%) translateY(10px);
  background:var(--surface-2);
  border:1px solid var(--line);
  color:var(--text);
  padding:10px 10px 10px 18px;
  border-radius:var(--r-sm);
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  display:flex;
  align-items:center;
  gap:14px;
  opacity:0;
  transition:opacity .2s, transform .2s;
  z-index:100;
  max-width:calc(100vw - 32px);
}
/* Même bug de spécificité que .bulk-actions-bar[hidden] plus bas (trouvé
   en le corrigeant là-bas, rétro-appliqué ici) : display:flex ci-dessus a
   la même spécificité que le [hidden]{display:none} par défaut du
   navigateur, donc l'attribut hidden posé par showUndoToast()/hideUndoToast()
   (js/ui.js) n'empêchait ni la présence en layout ni le focus clavier du
   bouton "Annuler" quand le toast est censé être invisible (opacity:0 seul
   ne bloque pas les clics/le focus) — un Tab pouvait tomber dessus sans
   que rien ne soit visible à l'écran. */
.undo-toast[hidden]{display:none;}
.undo-toast.show{opacity:1; transform:translateX(-50%) translateY(0);}
.undo-toast-msg{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.undo-toast-btn{
  flex-shrink:0;
  background:none;
  border:1px solid var(--violet);
  color:var(--violet);
  font-family:inherit;
  font-size:var(--fs-sm);
  font-weight:700;
  padding:6px 12px;
  border-radius:var(--r-sm);
  cursor:pointer;
}
.undo-toast-btn:hover{background:var(--violet); color:var(--surface-2);}
@media (max-width:600px){
  .undo-toast{bottom:calc(24px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
}

/* Barre d'actions groupées sur le catalogue (retour utilisateur), voir
   js/app.js (#bulkSelectBtn/#bulkActionsBar) — même position fixe en bas
   que .toast/.undo-toast, mais assez large pour 4 boutons + le décompte,
   jamais affichée en même temps qu'un toast normal (les deux mécanismes ne
   se déclenchent jamais ensemble). */
.bulk-actions-bar{
  position:fixed;
  bottom:24px;
  left:50%;
  transform:translateX(-50%);
  background:var(--surface-2);
  border:1px solid var(--line);
  border-radius:var(--r-md);
  padding:10px 16px;
  display:flex;
  align-items:center;
  gap:16px;
  flex-wrap:wrap;
  justify-content:center;
  max-width:calc(100vw - 32px);
  z-index:100;
  box-shadow:0 10px 30px rgba(0,0,0,0.4);
}
/* Bug réel trouvé en vérifiant en direct dans le navigateur : la règle
   display:flex ci-dessus a la MÊME spécificité (0,1,0) que le
   [hidden]{display:none} par défaut du navigateur — à spécificité égale,
   une feuille d'auteur l'emporte toujours sur la feuille UA, donc l'attribut
   hidden posé par renderBulkActionsBar() (js/app.js) n'avait plus aucun
   effet, la barre restait affichée en permanence. [hidden] ici a une
   spécificité plus élevée (classe + attribut) et regagne la priorité. */
.bulk-actions-bar[hidden]{display:none;}
.bulk-actions-count{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);
  color:var(--text-muted);
  white-space:nowrap;
}
.bulk-actions-buttons{display:flex; gap:8px; flex-wrap:wrap;}
@media (max-width:600px){
  .bulk-actions-bar{bottom:calc(24px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
}

/* --- Happenings (js/happenings.js) --- */

/* Fight Club : un seul flash, jamais répété (voir la note sur l'épilepsie
   photosensible dans js/happenings.js). Clair sur fond très sombre pour
   trancher franchement, contrairement au reste de l'app. */
.happening-flash{
  position:fixed; inset:0;
  z-index:300;
  display:flex; align-items:center; justify-content:center;
  background:var(--text);
  opacity:0;
  pointer-events:none;
  padding:0 24px;
  text-align:center;
}
.happening-flash.show{opacity:1; transition:opacity .06s ease-out;}
.happening-flash span{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xl);
  font-weight:700;
  letter-spacing:0.1em;
  color:var(--ink);
}

/* Old Boy : le couloir en un seul plan. */
.happening-modal{max-width:520px; text-align:center;}
.happening-modal .modal-head{text-align:left;}
.oldboy-corridor{
  overflow:hidden;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--surface-2);
  padding:24px 0;
  margin-top:8px;
}
.oldboy-corridor-track{
  display:flex;
  gap:56px;
  font-size:var(--fs-xl);
  width:max-content;
  animation:oldboyWalk 3.2s linear;
}
.oldboy-corridor-track.static{animation:none; transform:translateX(-140px);}
@keyframes oldboyWalk{
  from{transform:translateX(60px);}
  to{transform:translateX(-320px);}
}
.happening-caption{
  margin-top:16px;
  font-size:var(--fs-sm);
  font-style:italic;
  color:var(--text-muted);
}

/* Whiplash : le métronome de Fletcher, jamais content du tempo. Bras qui
   bat inlassablement d'un côté à l'autre — la même exception
   prefers-reduced-motion que le couloir d'Old Boy ci-dessus (animation
   autonome lancée par le clic, pas un flash répété). */
.whiplash-metronome{
  display:flex; align-items:flex-end; justify-content:center;
  height:120px;
  margin-top:8px;
}
.whiplash-metronome-arm{
  width:4px; height:90px;
  background:linear-gradient(to top, var(--gold), var(--bronze));
  border-radius:var(--r-sm);
  transform-origin:bottom center;
  animation:whiplashBeat 0.55s ease-in-out infinite;
}
.whiplash-metronome-arm.static{animation:none; transform:rotate(18deg);}
@keyframes whiplashBeat{
  0%, 100%{transform:rotate(-24deg);}
  50%{transform:rotate(24deg);}
}

/* Paprika : la parade, l'image la plus reconnaissable du film — même
   technique que le couloir d'Old Boy (bande en boucle), sens et tempo
   différents pour ne pas être un simple copié-collé déguisé. */
.paprika-parade{
  overflow:hidden;
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  background:var(--surface-2);
  padding:20px 0;
  margin-top:8px;
}
.paprika-parade-track{
  display:flex;
  gap:40px;
  font-size:var(--fs-xl);
  width:max-content;
  animation:paprikaMarch 4.5s linear;
}
.paprika-parade-track.static{animation:none; transform:translateX(-260px);}
@keyframes paprikaMarch{
  from{transform:translateX(-320px);}
  to{transform:translateX(60px);}
}

/* Mission : Impossible — combustion du message, une seule fois au clic (pas
   un flash répété : safe pour la photosensibilité, comme Fight Club
   ci-dessus). Sautée entièrement en JS pour prefers-reduced-motion (voir
   runMissionImpossibleHappening()), pas seulement neutralisée en CSS. */
.mim-burn .modal{animation:mimBurn 0.9s ease-in forwards;}
@keyframes mimBurn{
  0%{opacity:1; filter:brightness(1);}
  40%{filter:brightness(2) sepia(1) saturate(3);}
  100%{opacity:0; filter:brightness(0.3); transform:scale(0.92);}
}

/* The Dark Knight — la pièce, un seul tour au clic. */
.dk-coin{font-size:var(--fs-2xl); text-align:center; margin:16px 0;}
.dk-coin.flipping{animation:dkFlip 0.9s ease-in-out;}
@keyframes dkFlip{
  0%{transform:rotateY(0deg) scale(1);}
  50%{transform:rotateY(900deg) scale(1.3);}
  100%{transform:rotateY(1800deg) scale(1);}
}

/* The Truman Show : le ciel peint (teal, la couleur la plus "ciel" déjà
   dans la palette) qui bascule vers le noir du dehors, une seule fois au
   clic. Palette Kinet telle quelle, jamais une couleur littérale ajoutée
   juste pour cette scène. */
.truman-sky{
  height:90px;
  border-radius:var(--r-sm);
  margin-top:8px;
  background:linear-gradient(to bottom, var(--teal), var(--surface-2));
  transition:background 1s ease;
}
.truman-sky.opened{background:linear-gradient(to bottom, var(--ink), var(--surface));}

/* Inception : la toupie, en boucle tant que la modale reste ouverte (voir
   runInceptionHappening() : jamais d'arrêt ni de chute scriptée, l'ambiguïté
   EST le gag). Même exception prefers-reduced-motion que le couloir d'Old
   Boy plus haut (animation autonome lancée par le clic) : figée via la
   classe .static posée en JS, neutralisée ici en repli. */
.inception-top{
  font-size:var(--fs-2xl);
  text-align:center;
  margin:16px 0;
  animation:inceptionSpin 0.6s linear infinite;
}
.inception-top.static{animation:none;}
@keyframes inceptionSpin{
  from{transform:rotate(0deg);}
  to{transform:rotate(360deg);}
}

/* The Whale : reste un peu trop longtemps... */
.happening-fly{
  position:fixed;
  top:38%;
  left:-70px;
  font-size:var(--fs-2xl);
  z-index:300;
  pointer-events:none;
}
.happening-fly.fly{animation:happeningFly 1.1s cubic-bezier(.4,0,.6,1) forwards;}
@keyframes happeningFly{
  0%{left:-70px; transform:rotate(0deg);}
  100%{left:110%; transform:rotate(680deg);}
}

/* The Odyssey : l'épreuve de l'arc. Un arc SVG se bande réellement (corde +
   flèche qui reculent) au rythme des clics, voir updateOdysseyBow() dans
   js/happenings.js. Simples changements d'attributs SVG pilotés par le
   clic, pas d'animation autonome : pas besoin d'exception
   prefers-reduced-motion ici (même raisonnement que la barre ci-dessous,
   déjà jugée sans risque). */
.odyssey-bow{
  width:100%;
  max-width:260px;
  height:auto;
  margin:14px auto 0;
  display:block;
}
.odyssey-bow-guide{stroke:var(--line); stroke-width:1; stroke-dasharray:3 4;}
/* Encoche du sommet : repère statique (jamais mis à jour en JS) montrant où
   le bout libre de la corde doit venir s'accrocher — voir updateOdysseyBow()
   dans js/happenings.js. */
.odyssey-notch{stroke:var(--text-muted); stroke-width:1.5; stroke-dasharray:2 3;}
.odyssey-bow-path{fill:none; stroke:var(--gold-dim); stroke-width:6; stroke-linecap:round;}
.odyssey-string-path{fill:none; stroke:var(--text-muted); stroke-width:1.5; stroke-linecap:round;}
/* Bout libre de la corde, porté à la main vers l'encoche — voir le rayon
   qui grandit légèrement à l'accroche (100%) dans updateOdysseyBow(). */
.odyssey-string-end{fill:var(--gold);}
.odyssey-track{
  margin-top:14px;
  height:10px;
  border-radius:var(--r-sm);
  background:var(--surface-2);
  border:1px solid var(--line);
  overflow:hidden;
}
.odyssey-fill{
  height:100%;
  width:0%;
  background:linear-gradient(90deg, var(--gold-dim), var(--gold));
  transition:width .1s linear;
}
.odyssey-bend-btn{
  width:100%;
  margin-top:16px;
  padding:16px;
  font-size:var(--fs-md);
  letter-spacing:0.1em;
  user-select:none;
  touch-action:manipulation; /* pas de délai double-tap-zoom sur du mash rapide */
}
.odyssey-bend-btn:disabled{opacity:0.6; cursor:default;}

/* La Cité de Dieu : le défi photo — canvas généré par renderCogPhoto()
   (js/happenings.js), affiché responsive. */
.cog-modal{max-width:460px;}
#cogIntro input[type="file"]{margin-top:12px; width:100%;}
.cog-canvas{
  display:block;
  width:100%;
  height:auto;
  border-radius:var(--r-sm);
  border:1px solid var(--line);
  margin-top:12px;
  background:var(--surface-2);
}

/* Se7en : la boîte. Un seul secousse bref au clic sur "Ouvrir" — gardé
   court et non répété comme le flash de Fight Club (voir la note sur
   l'épilepsie photosensible dans js/happenings.js), même si une secousse
   pure n'a pas ce risque-là ; surtout, ça reste fidèle à la scène : un
   sursaut, jamais un vrai flash de contenu. */
#sevenOpenBtn{margin-top:16px;}
.seven-shake{animation:sevenShake .4s ease-in-out;}
@keyframes sevenShake{
  0%, 100%{transform:translateX(0);}
  20%{transform:translateX(-6px);}
  40%{transform:translateX(5px);}
  60%{transform:translateX(-4px);}
  80%{transform:translateX(3px);}
}

/* Parasite : le caillou flotte doucement (la crue) tant que le plan n'est
   pas fait, puis s'enfonce une seule fois au clic — même idée que le ciel
   de Truman Show juste au-dessus (un état qui bascule, jamais un aller-
   retour), jamais résolu pour de bon comme la toupie d'Inception : le texte
   change, le caillou ne redevient jamais net. */
.parasite-rock{
  font-size:var(--fs-2xl);
  text-align:center;
  margin:16px 0;
  animation:parasiteDrift 2.4s ease-in-out infinite;
}
.parasite-rock.static{animation:none;}
.parasite-rock.sunk{animation:parasiteSink 1.2s ease-in forwards;}
@keyframes parasiteDrift{
  0%, 100%{transform:translateY(0) rotate(-4deg);}
  50%{transform:translateY(-10px) rotate(4deg);}
}
@keyframes parasiteSink{
  from{transform:translateY(0); opacity:1;}
  to{transform:translateY(28px); opacity:0.15;}
}

@media (prefers-reduced-motion:reduce){
  .oldboy-corridor-track{animation:none; transform:translateX(-140px);}
  .whiplash-metronome-arm{animation:none; transform:rotate(18deg);}
  .paprika-parade-track{animation:none; transform:translateX(-260px);}
  .inception-top{animation:none;}
  .truman-sky{transition:none;}
  .parasite-rock{animation:none;}
  .parasite-rock.sunk{animation:none; transform:translateY(28px); opacity:0.15;}
}

/* ============================================================
   Mode GRILLE / Mode LISTE — js/ui.js, setViewMode('grid'|'list').
   Réécrit après retour utilisateur (v2.0.1) : la première version rendait
   le mode grille "par défaut" (sélecteurs nus #filmList/.film-row) et le
   mode liste comme une exception qui devait réinitialiser chaque
   propriété une à une (grid-area compris) — source du bug "le mode liste
   ne prend que la gauche de la page" (une réinitialisation manquante
   laissait des enfants avec une grid-area qui n'existait plus dans le
   nouveau grid-template-areas). Les deux modes sont maintenant écrits de
   façon SYMÉTRIQUE, chacun sous son propre sélecteur body[data-view-mode],
   sans rien qui doive être "annulé" par l'autre — document.body a TOUJOURS
   l'attribut posé dès le chargement (setViewMode(getViewMode()) dans
   js/ui.js), jamais l'état "aucun mode choisi" qui aurait justifié un
   mode implicite par défaut. Les deux réutilisent le même DOM généré par
   render()/renderWatchlist()/renderTop() (poster, .film-main/.wl-main,
   .star-btn ou .wl-actions, .counter) — aucune fonction de rendu n'a
   besoin de savoir dans quel mode elle dessine.
   ============================================================ */
/* gap:22px (16px avant) — retour utilisateur : "on manque d'espace, ça
   manque d'être épuré". La grille la plus vue de l'app (catalogue) avait
   des cartes qui se touchaient presque ; ce seul chiffre change pour
   #filmList/#wlList/#seriesList/#topList d'un coup, aucune règle à
   dupliquer par page. */
body[data-view-mode="grid"] #filmList,
body[data-view-mode="grid"] #wlList,
body[data-view-mode="grid"] #seriesList,
body[data-view-mode="grid"] #topList{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(158px, 1fr));
  gap:22px;
  align-items:start;
}
body[data-view-mode="grid"] #filmList .film-row{
  display:grid;
  grid-template-columns:1fr;
  grid-template-areas:"poster" "main" "footer";
  row-gap:10px;
  padding:12px;
  /* Bug retour utilisateur (S25, "à voir" en grille : affiches
     disproportionnées) : une carte de grille a par défaut min-width:auto
     (pas 0), et .film-title/.wl-title en dessous sont en white-space:nowrap
     (troncature avec ellipsis, voir plus bas) — leur largeur MINIMALE de
     contenu, sans ce 0, vaut alors leur texte COMPLET non coupé, pas
     seulement la largeur visible tronquée. Un titre un peu long forçait
     donc sa carte (et la colonne de grille entière, qui hérite de ce
     minimum) à s'élargir bien au-delà des colonnes voisines — 2 colonnes
     de tailles très différentes plutôt que 2 cases égales, l'affiche de
     la plus large paraissant "disproportionnée" à côté de l'autre. */
  min-width:0;
}
body[data-view-mode="grid"] #filmList .film-poster,
body[data-view-mode="grid"] #filmList .film-poster-placeholder{
  grid-area:poster;
  width:100%; height:auto; aspect-ratio:2/3;
  border-radius:var(--r-sm);
}
body[data-view-mode="grid"] #filmList .film-main{grid-area:main;}
/* Même correctif que .wl-title/.wl-note juste plus bas (v2.0.15, retour
   utilisateur "les carrés ne sont pas de la même taille") — jamais
   étendu à #filmList à l'époque. .film-title tronque déjà en 1 ligne,
   mais .film-sub (année + genres, voir render() dans js/app.js) n'avait
   aucune limite : un film avec 2 genres longs ("Animation,
   Science-Fiction") passait sur 2 lignes à côté d'un voisin resté sur 1,
   rendant sa carte plus haute que les autres de la même rangée. Retour
   utilisateur v3.2 : "tous les carrés [aient] la même longueur et
   largeur". */
body[data-view-mode="grid"] #filmList .film-sub{
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
/* .star-btn et .counter partagent la même zone "footer" plutôt qu'une 4e
   zone dédiée : justify-self les place chacun à une extrémité de la même
   ligne, comme deux enfants d'une rangée flex, sans changer le DOM. */
body[data-view-mode="grid"] #filmList .star-btn{grid-area:footer; justify-self:start; align-self:center;}
body[data-view-mode="grid"] #filmList .counter{grid-area:footer; justify-self:end; align-self:center;}
body[data-view-mode="grid"] #filmList .film-row.skeleton{grid-template-areas:"poster" "main" "footer";}
body[data-view-mode="grid"] #filmList .skeleton-poster{width:100%; aspect-ratio:2/3; height:auto;}

body[data-view-mode="grid"] #wlList .wl-row,
body[data-view-mode="grid"] #seriesList .wl-row,
body[data-view-mode="grid"] #topList .wl-row{
  display:flex;
  flex-direction:column;
  align-items:stretch;
  padding:12px;
  /* min-width:0 — même bug/raisonnement que .film-row juste au-dessus
     (S25, "affiches disproportionnées") : sans lui, un titre long en
     white-space:nowrap forçait sa carte (et sa colonne de grille) à
     déborder de sa juste part, laissant les cartes voisines rétrécir en
     comparaison. */
  min-width:0;
}
body[data-view-mode="grid"] #wlList .film-poster,
body[data-view-mode="grid"] #seriesList .film-poster,
body[data-view-mode="grid"] #topList .film-poster,
body[data-view-mode="grid"] #wlList .film-poster-placeholder,
body[data-view-mode="grid"] #seriesList .film-poster-placeholder,
body[data-view-mode="grid"] #topList .film-poster-placeholder{
  width:100%; height:auto; aspect-ratio:2/3;
  border-radius:var(--r-sm);
}
/* Cartes de hauteur inégale en grille (v2.0.15) — retour utilisateur :
   "les carrés ne sont pas de la même taille". .wl-title/.wl-note (à la
   différence de .film-title, qui tronque déjà sur une ligne) n'avaient
   aucune limite : une note perso longue ("pourquoi je veux le voir")
   faisait grandir SA carte de plusieurs dizaines de pixels par rapport
   aux voisines, décalant toute la rangée en grille (une rangée entière
   s'aligne sur son item le plus haut). Même traitement que .film-title
   pour le titre (1 ligne, ellipsis) ; -webkit-line-clamp pour la note
   (2 lignes max, pas une simple ligne — un vrai texte perso mérite plus
   qu'un mot avant de couper). Rien ne change en mode LISTE : une rangée
   plus haute qu'une autre y reste normale (liste verticale, pas de grille
   2D à aligner). */
body[data-view-mode="grid"] #wlList .wl-title,
body[data-view-mode="grid"] #seriesList .wl-title,
body[data-view-mode="grid"] #topList .wl-title{
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
body[data-view-mode="grid"] #wlList .wl-note,
body[data-view-mode="grid"] #seriesList .wl-note,
body[data-view-mode="grid"] #topList .wl-note{
  display:-webkit-box;
  -webkit-box-orient:vertical;
  -webkit-line-clamp:2;
  overflow:hidden;
}
/* #seriesList retiré de ces deux règles (v2.3) : la tuile série n'a plus de
   .wl-actions du tout (Ouvrir/Retirer), voir renderTrackedShows() dans
   js/series.js — la ligne entière ouvre la série au clic, comme .film-row. */
/* flex-wrap (v2.40) : la watchlist a maintenant 3 boutons par ligne
   (Rapide/Noter/Retirer) -- sur 2 seuls (séries/top), les deux tiennent
   toujours sur une ligne (flex-basis 45% < 100%), donc ce wrap ne les
   affecte pas ; sur 3, "Retirer" seul passe proprement à la ligne
   suivante plutôt que de déborder de la carte. */
body[data-view-mode="grid"] #wlList .wl-actions{margin-top:2px; flex-wrap:wrap;}
body[data-view-mode="grid"] #wlList .wl-actions .btn{flex:1 1 45%; white-space:nowrap;}
body[data-view-mode="grid"] #topList .counter{align-self:flex-end;}

/* Mode LISTE : une bannière pleine largeur par film plutôt qu'une ligne
   fine — une seule colonne (jamais de grille multi-colonnes, donc jamais
   le bug "prend seulement la gauche de la page"), affiche un peu plus
   grande qu'avant (64×96) pour rester dans l'esprit "affiche visible" de
   toute l'app, dégradé de fond pour donner du relief à la bannière. */
body[data-view-mode="list"] #filmList,
body[data-view-mode="list"] #wlList,
body[data-view-mode="list"] #seriesList,
body[data-view-mode="list"] #topList{
  display:flex;
  flex-direction:column;
  gap:10px;
}
body[data-view-mode="list"] #filmList .film-row,
body[data-view-mode="list"] #wlList .wl-row,
body[data-view-mode="list"] #seriesList .wl-row,
body[data-view-mode="list"] #topList .wl-row{
  display:flex;
  flex-direction:row;
  align-items:center;
  gap:18px;
  padding:14px 20px;
  min-height:96px;
  background:linear-gradient(120deg, var(--surface) 0%, var(--surface-2) 130%);
}
body[data-view-mode="list"] #filmList .film-poster,
body[data-view-mode="list"] #filmList .film-poster-placeholder,
body[data-view-mode="list"] #wlList .film-poster,
body[data-view-mode="list"] #seriesList .film-poster,
body[data-view-mode="list"] #topList .film-poster,
body[data-view-mode="list"] #wlList .film-poster-placeholder,
body[data-view-mode="list"] #seriesList .film-poster-placeholder,
body[data-view-mode="list"] #topList .film-poster-placeholder{
  width:64px; height:96px; flex:none;
  border-radius:var(--r-sm);
}
body[data-view-mode="list"] #filmList .film-title,
body[data-view-mode="list"] #wlList .wl-title,
body[data-view-mode="list"] #seriesList .wl-title,
body[data-view-mode="list"] #topList .wl-title{
  font-size:var(--fs-md);
}
body[data-view-mode="list"] #wlList .wl-actions{flex:none;}
body[data-view-mode="list"] #wlList .wl-actions .btn{flex:none;}

/* --- Détail d'une série (#/series/:id, js/series.js) et fiche film
   communautaire (#/film/:tmdbId, js/filmDetail.js, classe partagée) ---
   Repris en grille (retour utilisateur, backlog : "même mise en page que
   les films") pour rejoindre le même langage que .film-modal-layout et
   .film-review-layout, avec une affiche nettement plus grande que
   l'ancien 130×195px fixe plutôt qu'une simple vignette à côté du texte. */
/* Photo de plateau (retour utilisateur : "on n'a jamais l'image du film,
   juste l'affiche") — voir renderDetailBackdrop() (js/tmdb.js), appelée
   depuis js/filmDetail.js et js/series.js. TMDB fournit un backdrop_path
   (format large, 16:9) séparé du poster_path (2:3) déjà utilisé partout
   ailleurs — jamais rappelé jusqu'ici, alors que c'est la seule vraie photo
   du film que renvoie l'API, le reste n'étant que des affiches. Toujours en
   PLUS de l'affiche, jamais à sa place : la vignette 2:3 reste la donnée de
   référence (grille, watchlist, recherche...), ce bandeau est un décor
   d'ambiance propre à la fiche détail. Masqué entièrement si TMDB n'a pas
   de backdrop_path pour ce titre (pas systématique, surtout sur un film
   ancien/confidentiel) — jamais un cadre vide à sa place. */
.detail-backdrop{
  position:relative;
  height:340px;
  border-radius:var(--r-md);
  overflow:hidden;
  margin-bottom:28px;
  border:1px solid var(--line);
  background:var(--surface-2);
}
.detail-backdrop img{
  width:100%; height:100%;
  object-fit:cover;
  display:block;
  opacity:0;
  animation:detailBackdropIn .5s ease-out both;
}
@keyframes detailBackdropIn{from{opacity:0;} to{opacity:1;}}
/* Voile + teinte Halation par-dessus la photo, jamais l'inverse (jamais de
   couleur ajoutée à même l'image) — même raisonnement que --plum/--plum-2
   dans :root (voir le commentaire associé) : une vraie photo assombrie
   reste un choix de gradation, volontairement identique quel que soit le
   thème, pas une surface qui doit s'éclaircir avec le reste de la page.
   rgba(15,14,22,...) est la valeur littérale de --ink, jamais retéintée
   entre thèmes non plus. Lisibilité du titre/genre juste en dessous d'abord
   (voile bas), lueur violet/or en diagonale ensuite, pour une vraie photo
   qui reste "Kinet" plutôt qu'une capture brute plaquée telle quelle. */
.detail-backdrop::after{
  content:'';
  position:absolute; inset:0;
  background:
    linear-gradient(0deg, rgba(15,14,22,0.65) 0%, rgba(15,14,22,0.1) 55%, transparent 78%);
}
@media (prefers-reduced-motion:reduce){
  .detail-backdrop img{animation:none; opacity:1;}
}
@media (max-width:600px){
  .detail-backdrop{height:200px; border-radius:var(--r-md); margin-bottom:20px;}
}
/* Repli (v3.2) : le panneau reste visible même sans photo de plateau —
   affiche en plein cadre si dispo, sinon un dégradé neutre plutôt qu'un
   cadre vide. Voir renderShowDetailHeader(), js/series.js. */
.detail-backdrop.is-placeholder{
  background:radial-gradient(130% 140% at 15% 10%, var(--surface-3) 0%, var(--surface-2) 55%, var(--ink-surface) 100%);
}
.detail-backdrop.is-placeholder img{display:none;}
.detail-title{
  font-family:var(--font-display);
  font-weight:800;
  font-size:var(--fs-xl);
  line-height:1.08;
  letter-spacing:-0.01em;
}
.detail-meta{
  font-family:var(--font-text);
  font-size:var(--fs-sm);
  letter-spacing:0.05em;
  text-transform:uppercase;
  color:var(--violet);
  margin-top:8px;
}

.fiche{margin-top:24px; border-top:1px solid var(--line);}
.fiche-row{display:flex; gap:16px; padding:11px 0; border-bottom:1px solid var(--line);}
.fiche-label{
  width:110px; flex:none;
  font-size:var(--fs-xs); letter-spacing:0.08em; text-transform:uppercase;
  color:var(--text-muted); padding-top:1px;
}
.fiche-value{font-size:var(--fs-sm); min-width:0;}
.fiche-value.mono{font-family:var(--font-text); font-variant-numeric:tabular-nums;}

/* Voix éditoriale (serif italique, même registre que .auth-pitch-sub) —
   premier résumé de film/série à l'utiliser plutôt que le simple
   .auth-intro générique. */
.synopsis{
  font-family:var(--font-text);
  font-style:normal;
  font-weight:500;
  font-size:var(--fs-md);
  line-height:1.65;
  color:var(--text);
  margin-top:20px;
  margin-bottom:24px;
  max-width:68ch;
}

/* Flèches "film/série d'à côté" (retour utilisateur : "j'aimerais naviguer
   à ceux d'à côté"). Dans l'entête de la fiche, à droite (audit C3 : fixées
   au milieu de l'écran, elles recouvraient le contenu). Boutons icône du
   code de design : 40px, sans fond au repos. */
.detail-nav{display:flex; gap:4px; margin-left:auto; flex:none;}
.detail-nav-arrow{
  width:40px; height:40px;
  border-radius:var(--r-full);
  border:none;
  background:none;
  color:var(--text-muted);
  font-size:var(--fs-xl);
  line-height:1;
  display:flex; align-items:center; justify-content:center;
  cursor:pointer;
  transition:background-color .15s ease, color .15s ease, transform .1s ease-out;
}
.detail-nav-arrow:hover{background:var(--surface-2); color:var(--text);}
.detail-nav-arrow:active{transform:scale(0.97);}
.detail-nav-arrow:focus-visible{outline:2px solid var(--violet); outline-offset:2px;}
@media (prefers-reduced-motion:reduce){
  .detail-nav-arrow{transition:none;}
}
@media (max-width:600px){
}

/* Où regarder (fiche film communautaire ET fiche série, retour
   utilisateur), voir renderWatchProvidersHtml()/watchProviderChipsHtml()
   (js/tmdb.js). Logos en petites puces carrées, pas de couleur ajoutée : ce
   sont déjà les couleurs de marque de chaque service, une teinte Kinet par
   dessus n'aurait aucun sens ici (contrairement au reste de l'app). */
.watch-providers{display:flex; flex-direction:column; gap:10px;}
.watch-provider-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
.watch-provider-row-label{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  flex:none;
  width:92px;
}
.watch-provider-chips{display:flex; flex-wrap:wrap; gap:6px;}
.watch-provider-chip{
  width:32px; height:32px;
  border-radius:var(--r-sm);
  overflow:hidden;
  border:1px solid var(--line);
  flex:none;
  background:var(--surface-2);
}
.watch-provider-chip img{width:100%; height:100%; object-fit:cover; display:block;}
/* Attribution imposée par TMDB pour cet endpoint (voir le commentaire sur
   renderWatchProvidersHtml(), js/tmdb.js) — jamais supprimable, mais rendue
   aussi discrète que possible (retour utilisateur) : petite, grise, sans
   soulignement ni flèche, presque invisible tant qu'on ne la cherche pas. */
.watch-provider-justwatch{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  opacity:0.6;
  align-self:flex-start;
  margin-top:2px;
}
.watch-provider-justwatch:hover{opacity:1;}
@media (max-width:600px){
  .watch-provider-row{flex-direction:column; align-items:flex-start; gap:6px;}
  .watch-provider-row-label{width:auto;}
}

/* Casting (fiche film, retour utilisateur : "pour les films ce serait bien
   de pouvoir voir le cast aussi") — bande défilable à l'horizontale, pas
   une liste : même raisonnement que .bar-chart (overflow-x:auto) ailleurs
   dans ce fichier, se touche naturellement au doigt sur mobile, aucune
   media query à part pour ça. */
.cast-strip{display:flex; gap:14px; overflow-x:auto; padding-bottom:6px;}
.cast-card{flex:none; width:84px; text-align:center;}
.cast-photo{
  width:84px; height:84px;
  border-radius:50%;
  object-fit:cover;
  border:1px solid var(--line);
  background:var(--surface-2);
  display:block;
}
.cast-photo-placeholder{display:flex; align-items:center; justify-content:center; color:var(--text-muted);}
.cast-photo-placeholder svg{width:38%; height:38%;}
.cast-name{
  font-size:var(--fs-sm); font-weight:600; margin-top:6px; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.cast-character{
  font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* Notes des amis sur CE film précis (retour utilisateur, étude comparative
   Letterboxd/Goodreads — "Amis" n'affichait jusqu'ici que les likes, jamais
   qui parmi eux a vu/noté ce film) — voir renderFilmDetailFriendRatings(),
   js/filmDetail.js. Même principe de bande défilable que .cast-strip
   juste au-dessus, mais avec .friend-avatar (déjà utilisé partout ailleurs
   pour un visage d'ami) plutôt qu'un portrait TMDB. */
.friend-rating-strip{display:flex; gap:14px; overflow-x:auto; padding-bottom:6px; margin-bottom:10px;}
.friend-rating-chip{flex:none; display:flex; flex-direction:column; align-items:center; gap:6px; width:64px; text-align:center;}
.friend-rating-chip .friend-avatar, .friend-rating-chip .friend-avatar-placeholder{width:48px; height:48px;}
.friend-rating-chip-name{
  font-size:var(--fs-xs); font-weight:600; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.friend-rating-chip .counter{width:32px; height:32px; font-size:var(--fs-xs); border-width:1.5px;}

/* Films similaires sur la fiche film (retour utilisateur, étude
   comparative Letterboxd/Goodreads — "Readers also enjoyed") — voir
   renderFilmDetailSimilar(), js/filmDetail.js. Même bande défilable que
   .cast-strip juste au-dessus, affiches plutôt que portraits (format
   2:3 TMDB standard), cliquables vers la fiche de CE film recommandé. */
.similar-strip{display:flex; gap:14px; overflow-x:auto; padding-bottom:6px;}
.similar-card{flex:none; width:104px; text-align:left; cursor:pointer;}
.similar-card img, .similar-card .film-poster-placeholder{
  width:104px; height:156px;
  border-radius:var(--r-sm);
  object-fit:cover;
  border:1px solid var(--line);
  background:var(--surface-2);
  display:flex; align-items:center; justify-content:center;
  transition:border-color 0.15s ease;
}
.similar-card:hover img, .similar-card:hover .film-poster-placeholder{border-color:var(--violet-dim);}
.similar-card-title{
  font-size:var(--fs-sm); font-weight:600; margin-top:6px; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.similar-card-year{font-size:var(--fs-xs); color:var(--text-muted); margin-top:2px;}
@media (prefers-reduced-motion:reduce){
  .similar-card img, .similar-card .film-poster-placeholder{transition:none;}
}
.list-cover-card{
  border:1px solid var(--line);
  border-radius:var(--r-md);
  overflow:hidden;
  cursor:pointer;
  background:var(--surface-2);
  transition:border-color 0.15s ease, transform 0.15s ease;
}
.list-cover-card:hover{border-color:var(--violet-dim); transform:translateY(-2px);}
.list-cover-posters{
  display:grid;
  grid-template-columns:1fr 1fr;
  grid-template-rows:1fr 1fr;
  gap:1px;
  aspect-ratio:2/1.2;
  background:var(--line);
}
.list-cover-posters img{width:100%; height:100%; object-fit:cover; display:block;}
.list-cover-posters-empty{
  aspect-ratio:2/1.2;
  display:flex; align-items:center; justify-content:center;
  color:var(--text-muted);
  font-size:var(--fs-xl);
}
@media (prefers-reduced-motion:reduce){
  .list-cover-card{transition:none;}
  .list-cover-card:hover{transform:none;}
}
#filmDetailLikeBtn.active{border-color:var(--coral-dim); color:var(--coral);}
/* Statut TMDB, coloré comme .rate-* mais avec un vocabulaire différent :
   ici il ne s'agit pas d'un barème de qualité, juste de savoir si la
   série reviendra ou non — vert (en cours) / gris (terminée), jamais de
   rouge (ça suggérerait un jugement de valeur qui n'a pas lieu d'être). */
.status-badge{
  display:inline-block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:600;
  letter-spacing:0.04em;
  text-transform:uppercase;
  border-radius:var(--r-full);
  padding:4px 10px;
}
.status-badge.ongoing{color:var(--green); background:var(--green-wash); border:1px solid var(--green-dim);}
.status-badge.ended{color:var(--text-muted); background:var(--surface-2); border:1px solid var(--line);}

/* Badge "nouvel épisode" (retour utilisateur, js/series.js hasNewEpisode())
   — même gabarit que .status-badge, doré (rôle "accent des données/notes",
   voir --gold plus haut) plutôt que vert : ce n'est pas un statut mais une
   invite à l'action, à distinguer visuellement d'"En diffusion". */
.new-episode-badge{
  display:inline-block;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  font-weight:600;
  letter-spacing:0.04em;
  text-transform:uppercase;
  border-radius:var(--r-full);
  padding:3px 9px;
  color:var(--gold);
  background:var(--gold-wash);
  border:1px solid var(--gold-dim);
  vertical-align:middle;
}
/* Pastille sur l'affiche, mode grille seulement (body[data-view-mode]) :
   .wl-title est tronqué sur une seule ligne en grille (voir plus haut,
   "cartes de hauteur inégale") -- .new-episode-badge textuel juste au-dessus
   peut disparaître dans l'ellipsis d'un titre long. .wl-row a déjà
   position:relative (perspective de .counter), sert d'ancre sans wrapper
   supplémentaire autour de l'affiche. Cachée par défaut (mode liste) : le
   badge texte du titre y suffit déjà, inutile de doubler le signal. */
.new-episode-dot{
  display:none;
  position:absolute;
  top:6px; left:6px;
  width:10px; height:10px;
  border-radius:50%;
  background:var(--gold);
  box-shadow:0 0 0 2px var(--surface-2);
  z-index:1;
}
body[data-view-mode="grid"] #seriesList .new-episode-dot{display:block;}

/* --- Accordéon de saisons (#seriesSeasonsList) --- */
/* v3.2, retour utilisateur ("pour les séries reprend la draft... c'était
   parfait") — la draft Scope n'avait ni boîte ni fond sur une saison,
   juste une étiquette + un trait d'espacement (.season{margin-top:30px},
   .season-head en texte nu). Carte retirée ici (bordure/radius/fond/
   overflow:hidden) : seul le repliage saison par saison reste (utile dès
   qu'un show a beaucoup de saisons, la draft n'avait pas ce problème à
   résoudre) — display:none/.open ci-dessous n'a jamais dépendu de la
   boîte, seulement du style. */
.season-block{margin-top:28px;}
.season-block:first-child{margin-top:0;}
.season-header{
  width:100%;
  display:flex;
  align-items:baseline;
  gap:12px;
  background:none;
  border:none;
  color:var(--text);
  padding:0 0 4px;
  cursor:pointer;
  text-align:left;
  font-family:var(--font-text);
}
.season-name{font-family:var(--font-display); font-weight:700; font-size:var(--fs-md);}
.season-progress{
  margin-left:auto;
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--violet);
}
.season-caret{
  flex:none;
  font-size:var(--fs-sm);
  color:var(--text-muted);
  transition:transform .15s;
}
.season-block.open .season-caret{transform:rotate(180deg);}
.season-episodes{display:none; padding:0;}
.season-block.open .season-episodes{display:block;}
/* .episode-row = la ligne entière (case à cocher + éventuels contrôles
   "note"/"vu ×N" ci-dessous, v2.39 — restructurée en <div> plutôt que
   <label> pour que ces contrôles additionnels ne soient pas eux-mêmes
   interprétés comme des clics sur la case à cocher). .episode-row-main
   reste le <label> proprement dit (case + titre), qui garde tout le style
   déjà en place. */
.episode-row{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:8px 10px;
  padding:8px 0;
  border-top:1px solid var(--line-soft);
}
.episode-row:first-of-type{border-top:none;}
@keyframes checkboxPop{0%{transform:scale(1);} 40%{transform:scale(1.22);} 100%{transform:scale(1);}}
@media (prefers-reduced-motion: reduce){
}
.episode-num{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  flex:none;
  width:34px;
}
.episode-title{flex:1; min-width:0; font-size:var(--fs-sm);}
.episode-air{
  font-family:var(--font-text); font-variant-numeric:tabular-nums;
  font-size:var(--fs-xs);
  color:var(--text-muted);
  flex:none;
}
/* Note + nombre de fois vu (v2.39, retour utilisateur) : visibles
   seulement une fois l'épisode coché vu — jamais l'essentiel de la ligne,
   toujours discrets à côté. */
.episode-extra{
  display:flex;
  align-items:center;
  gap:10px;
  flex:none;
  margin-left:auto;
}

/* Tablette (v2.1, retour utilisateur : recherche/filtre/grille doivent
   rester sur la même ligne) : .toolbar garde son flex-wrap de base
   (voir plus haut) plutôt qu'un flex-direction:column forcé qui séparait
   systématiquement recherche+tri d'un côté et vue grille/liste de
   l'autre — recherche/tri rétrécissent (min-width:0) au lieu d'imposer
   100% de la largeur, laissant la rangée grille/liste tenir juste à côté
   tant qu'il reste la place ; ne bascule sur sa propre ligne qu'en
   dernier recours, comme n'importe quel flex-wrap normal. */
@media (max-width:860px){
  .toolbar-search{min-width:0; flex:1 1 200px;}

  /* Vitrine de connexion : la grille 2 colonnes ne tient plus côte à côte
     — repasse en flex empilé. column-reverse sur les 2 enfants directs
     (.auth-pitch, .auth-card) : la carte de connexion passe en premier
     pour que le geste "se connecter" reste immédiat sur mobile, suivie de
     la démo jouable puis de l'argumentaire — plus simple qu'avant
     (v2.0.10-v2.0.15) : sans la vitrine de cartes factices à réordonner
     séparément, un seul column-reverse suffit désormais. */
  #authContainer{padding:32px 0;}
  .auth-hero{display:flex; flex-direction:column-reverse; gap:28px; margin:0 auto;}
}

@media (max-width:600px){
  header{padding:26px 18px 20px;}
  .container{padding:0 18px;}
  h1{font-size:var(--fs-xl);}
  .stats-extremes{grid-template-columns:1fr;}
  /* v2.1.x, retour utilisateur : la rangée d'icônes + avatar/pseudo,
     repositionnée sous les onglets Films/Séries par l'ancienne règle ici
     (position:static + flex-wrap), restait confuse — "tout au même
     endroit... ça fait très tâche", "étrange" que ce soit entre le titre
     et la toolbar plutôt que tout en haut ou tout en bas. Remplacée par
     #mobileTabbar (fixe, en bas — voir index.html pour le raisonnement
     complet) : .user-bar disparaît entièrement ici plutôt que d'être
     réagencée. */
  .user-bar{display:none;}
  /* .user-bar disparu ci-dessus : Top films/Avis/Nouveautés (entête)
     n'ont plus aucun accès sur mobile à part ici (modale profil). */
  .mm-row.profile-mobile-only{display:flex;}
  /* Barre flottante (v3.0, draft Scope) plutôt que plaquée bord à bord :
     marge + coins arrondis + ombre au lieu d'un trait dur en haut — même
     matière (verre/blur) qu'avant, juste plus un "objet posé" qu'une
     barre d'app Android 2015. Hauteur gardée proche de --mobile-tabbar-h
     (60px) malgré l'inset : les calculs de marge ailleurs (.page, .fab,
     .toast, tous calc(... + var(--mobile-tabbar-h) + ...)) gardent assez
     de mou pour ne pas avoir à les retoucher un par un. */
  .mobile-tabbar{
    display:flex;
    position:fixed;
    left:12px; right:12px; bottom:calc(10px + env(safe-area-inset-bottom, 0px));
    z-index:30; /* sous .overlay (50, modales) : une fiche/modale ouverte
      doit couvrir la barre, pas l'inverse. Sous .fab (40) aussi, mais sans
      conséquence : .fab est décalé au-dessus d'elle plus bas dans ce même
      bloc, ils ne se chevauchent jamais spatialement. */
    height:56px;
    border-radius:var(--r-md);
    background:rgba(var(--glass-rgb),0.88);
    backdrop-filter:blur(20px) saturate(160%);
    -webkit-backdrop-filter:blur(20px) saturate(160%);
    border:1px solid var(--line);
    box-shadow:0 14px 32px rgba(0,0,0,0.4);
  }
  /* Espace pour ne jamais laisser #mobileTabbar recouvrir le bas d'une
     page/liste — .page avait déjà 60px de marge pour d'autres raisons
     (voir plus haut), on y ajoute la hauteur de la barre + sa zone de
     sécurité plutôt que de choisir entre les deux. */
  .page{padding-bottom:calc(60px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
  #appContainer{padding-bottom:calc(var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
  /* .fab et .update-banner se décalent au-dessus de la barre plutôt que
     de passer dessous — les deux restaient à bottom:22px/0 (pensés pour
     un bas d'écran libre, avant #mobileTabbar). */
  .fab{bottom:calc(22px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
  .update-banner{bottom:calc(var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
  .toast{bottom:calc(24px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
  /* .fab reprend le coin bas-droite ici (masqué par défaut, visible
     seulement ≤600px) — passe le retour-en-haut à gauche plutôt que de
     superposer les deux, en le décalant lui aussi au-dessus de la barre. */
  .scroll-top-btn{left:18px; right:auto; bottom:calc(18px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}

  /* Grille plus serrée sur mobile : 2 colonnes plutôt que la largeur
     minimale de 158px, qui laisserait une seule colonne trop large sur un
     téléphone étroit. */
  body[data-view-mode="grid"] #filmList,
  body[data-view-mode="grid"] #wlList,
  body[data-view-mode="grid"] #seriesList,
  body[data-view-mode="grid"] #topList{grid-template-columns:repeat(2, 1fr); gap:16px;}
  /* Bannière plus compacte sur mobile — affiche resserrée pour laisser de
     la place au texte sans forcer le retour à la ligne (qui casserait la
     bannière plutôt que de simplement la rétrécir). */
  body[data-view-mode="list"] #filmList .film-row,
  body[data-view-mode="list"] #wlList .wl-row,
  body[data-view-mode="list"] #seriesList .wl-row,
  body[data-view-mode="list"] #topList .wl-row{padding:10px 14px; gap:12px; min-height:76px;}
  body[data-view-mode="list"] #filmList .film-poster,
  body[data-view-mode="list"] #filmList .film-poster-placeholder,
  body[data-view-mode="list"] #wlList .film-poster,
  body[data-view-mode="list"] #seriesList .film-poster,
  body[data-view-mode="list"] #topList .film-poster,
  body[data-view-mode="list"] #wlList .film-poster-placeholder,
  body[data-view-mode="list"] #seriesList .film-poster-placeholder,
  body[data-view-mode="list"] #topList .film-poster-placeholder{width:46px; height:68px;}

  /* Watchlist en vue liste sur mobile (audit responsive) : #wlList a 3
     boutons (Rapide/Noter/Retirer, voir js/watchlist.js) contre 2 ailleurs
     (Séries/Top) — .wl-actions{flex:none} (règle desktop plus haut dans ce
     fichier) les empêchait de rétrécir ET la ligne ne pouvait pas non plus
     wrapper, donc les 3 boutons se retrouvaient écrasés par-dessus le titre
     une fois celui-ci poussé à une largeur ridicule. Même traitement que la
     vue carte (v2.40.1, .wl-actions{flex-wrap:wrap} plus haut) : les
     actions passent sur leur propre ligne plutôt que de cohabiter de force
     avec le titre. */
  body[data-view-mode="list"] #wlList .wl-row{flex-wrap:wrap;}
  body[data-view-mode="list"] #wlList .wl-actions{
    flex:1 1 100%;
    flex-wrap:wrap;
    justify-content:flex-end;
  }

  /* Recherche seule reste sur la ligne (v2.1.x, retour utilisateur :
     recherche+tri+genre tous les 3 devenaient illisibles/tronqués sur
     téléphone) — tri et genre repliés derrière #filtersToggleBtn, ouverts
     en feuille du bas (.toolbar-filters.open), voir le commentaire complet
     sur #toolbarFilters dans index.html. "+ Ajouter" quitte la barre pour
     le bouton flottant (.fab) : un item de moins à caser sur cette ligne.
     .toolbar-search{min-width:240px} (desktop, plus haut dans ce fichier)
     empêchait TOUT le groupe recherche+filtres de descendre sous 240px
     même une fois l'input libre de rétrécir — sur un écran réellement
     étroit (ex. Galaxy S25, ~360px de large), ça suffisait à faire
     passer grille/liste + export sur une 2e ligne (retour utilisateur :
     "j'ai 2 lignes... c'est pas top"). Cette redéfinition-ci lève le
     plancher pour de vrai — MAIS ne suffisait encore qu'au catalogue
     Films : la règle tablette (flex:1 1 200px, juste au-dessus, 760px)
     restait active à toutes les largeurs ≤760px faute de flex-basis
     réécrit ici, et sur Séries (recherche seule + "+ Suivre", pas de
     bouton Filtres pour partager la ligne) ce basis de 200px suffisait à
     lui seul à faire passer grille/liste + Suivre en dessous (constaté
     en repartant du retour utilisateur — Films semblait réglé, Séries ne
     l'était pas). flex-basis:0 ici referme vraiment l'écart, partout où
     .toolbar-search est utilisée. */
  .toolbar-search{min-width:0; flex-basis:0; gap:6px;}
  .toolbar-search input[type="text"]{min-width:0; flex:1 1 auto;}
  #filtersToggleBtn{display:inline-flex; flex:none;}
  .toolbar{gap:8px;}
  .toolbar-actions{gap:6px;}
  #openAddBtn{display:none;}
  .fab{display:flex;}

  .toolbar-filters{
    display:none;
    position:fixed; inset:0;
    z-index:400;
    align-items:flex-end;
    justify-content:center;
    background:rgba(6,5,10,0.55);
    backdrop-filter:blur(8px);
    -webkit-backdrop-filter:blur(8px);
    padding:0;
  }
  .toolbar-filters.open{display:flex; animation:overlayIn .15s ease-out;}
  .toolbar-filters-sheet{
    display:flex;
    flex-direction:column;
    gap:14px;
    width:100%;
    max-width:520px;
    background:var(--surface);
    border:1px solid var(--line);
    border-radius:var(--r-md) var(--r-md) 0 0;
    padding:20px 20px calc(20px + env(safe-area-inset-bottom, 0px));
    animation:modalIn .18s cubic-bezier(.2,.8,.3,1);
  }
  .toolbar-filters-head{
    display:flex;
    align-items:center;
    justify-content:space-between;
    font-family:var(--font-display);
    font-weight:700;
    font-size:var(--fs-md);
  }
  .toolbar-filters-sheet select{width:100%;}
  @media (prefers-reduced-motion:reduce){
    .toolbar-filters.open, .toolbar-filters-sheet{animation:none;}
  }

  .overlay{padding:20px 12px;}
  .modal{padding:24px;}
  /* 2 colonnes, une seule sur les très petits écrans, sans seuil de plus
     (audit D5 : deux seuils, 600 et 860 px). */
  .ach-tier-grid, .ach-hidden-grid{grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));}
  .stat-tiles{grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));}
  body[data-view-mode="grid"] #wlList .wl-actions{width:100%;}
  .journal-row{flex-wrap:wrap;}
  .journal-date{margin-left:auto;}

  /* La note "· 8.50 / 5" (.live-score) se faisait écraser par les boutons
     du footer sur la même ligne et retombait sur 2 lignes — chacun sa
     ligne, boutons alignés à droite (et capables de wrap à leur tour s'il
     y en a 3 : Supprimer/Annuler/Enregistrer). */
  .modal-footer{flex-wrap:wrap; gap:12px 16px;}
  .modal-footer .right{flex:1 1 100%; flex-wrap:wrap; justify-content:flex-end;}
}

/* Admin : gestion des cartes (onglet Cartes) — voir js/admin.js. */
.admin-override{color:var(--gold); font-weight:600;}
.admin-card-row .wl-actions select{min-width:150px;}

/* ==========================================================
   LOT 3 DE L'AUDIT : grilles d'affiches nues (code de design,
   composant « Affiche »). Films, séries, top et À voir partagent la même
   carte : affiche 2:3 sans cadre, titre sur une ligne, une ligne d'info,
   note en chiffre coloré dans le coin de l'affiche. Placé en fin de
   feuille pour l'emporter sur les anciennes règles de cartes encadrées.
   ========================================================== */
body[data-view-mode="grid"] :is(#filmList, #seriesList, #topList, #wlList){
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));
  gap:var(--sp-5) var(--sp-4);
  align-items:start;
}
body[data-view-mode="grid"] :is(#filmList .film-row, #seriesList .wl-row, #topList .wl-row, #wlList .wl-row){
  position:relative;
  display:flex; flex-direction:column; align-items:stretch; gap:6px;
  padding:0; margin:0; min-width:0; min-height:0;
  background:none; border:none; border-radius:0; box-shadow:none;
  transform:none; animation:none;
}
body[data-view-mode="grid"] :is(#filmList .film-row, #seriesList .wl-row, #topList .wl-row, #wlList .wl-row):hover{
  background:none; border:none; box-shadow:none; transform:none;
}
body[data-view-mode="grid"] :is(#filmList, #seriesList, #topList, #wlList) :is(.film-poster, .film-poster-placeholder){
  width:100%; height:auto; aspect-ratio:2/3;
  border:none; border-radius:var(--r-sm);
  margin-bottom:4px;
  transition:transform var(--dur-fast) var(--ease-out);
}
body[data-view-mode="grid"] :is(#filmList .film-row, #seriesList .wl-row, #topList .wl-row, #wlList .wl-row-clickable):hover :is(.film-poster, .film-poster-placeholder){
  transform:translateY(-2px);
}
body[data-view-mode="grid"] :is(#filmList .film-row, #seriesList .wl-row, #topList .wl-row, #wlList .wl-row):focus-visible{
  outline:none;
}
body[data-view-mode="grid"] :is(#filmList .film-row, #seriesList .wl-row, #topList .wl-row, #wlList .wl-row):focus-visible :is(.film-poster, .film-poster-placeholder){
  outline:2px solid var(--accent); outline-offset:2px;
}
/* Titre et ligne d'info */
body[data-view-mode="grid"] :is(#filmList .film-title, #seriesList .wl-title, #topList .wl-title, #wlList .wl-title){
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:500; line-height:1.35;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
body[data-view-mode="grid"] :is(#filmList .film-sub, #seriesList .wl-note, #topList .wl-note, #wlList .wl-note, #wlList .wl-friend-rating){
  font-size:var(--fs-xs); color:var(--text-2); letter-spacing:0; margin-top:0;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  display:block; -webkit-line-clamp:unset;
}
/* Ce que la grille ne montre pas (reste en vue liste et sur la fiche) */
body[data-view-mode="grid"] :is(#seriesList, #topList, #wlList) .wl-year,
body[data-view-mode="grid"] #filmList :is(.review-badge, .rewatch-badge),
body[data-view-mode="grid"] #seriesList .new-episode-badge{display:none;}
/* La note : chiffre coloré dans le coin de l'affiche, sans anneau */
body[data-view-mode="grid"] :is(#filmList, #seriesList, #topList) .counter{
  position:absolute; top:6px; right:6px; z-index:1;
  width:auto; height:auto; min-width:0;
  padding:4px 7px;
  border:none; border-radius:var(--r-sm);
  background:rgba(var(--glass-rgb),0.78);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  font-size:var(--fs-sm); font-weight:600; line-height:1;
}
body[data-view-mode="grid"] :is(#filmList, #seriesList, #topList) .counter:not(.rate-low):not(.rate-mid):not(.rate-high){display:none;}
/* Favori : étoile en surimpression, en haut à gauche. Visible quand le film
   est favori ; sinon au survol (souris) ou discrète (écran tactile). */
body[data-view-mode="grid"] #filmList .star-btn{
  position:absolute; top:6px; left:6px; z-index:1;
  width:30px; height:30px; padding:0;
  display:flex; align-items:center; justify-content:center;
  border-radius:var(--r-full);
  background:rgba(var(--glass-rgb),0.78);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  font-size:var(--fs-md); color:var(--text-2);
}
body[data-view-mode="grid"] #filmList .star-btn.active{color:var(--accent);}
@media (hover:hover){
  body[data-view-mode="grid"] #filmList .star-btn:not(.active){opacity:0; transition:opacity var(--dur-fast);}
  body[data-view-mode="grid"] #filmList .film-row:hover .star-btn,
  body[data-view-mode="grid"] #filmList .star-btn:focus-visible{opacity:1;}
}
@media (hover:none){
  body[data-view-mode="grid"] #filmList .star-btn:not(.active){opacity:0.6;}
}
body[data-view-mode="grid"] #seriesList .new-episode-dot{background:var(--accent); box-shadow:0 0 0 2px rgba(var(--glass-rgb),0.9);}
/* À voir : le menu « ⋯ » en surimpression, en haut à droite de l'affiche */
body[data-view-mode="grid"] #wlList .wl-actions{
  position:absolute; top:4px; right:4px; z-index:1; margin:0; width:auto;
}
body[data-view-mode="grid"] #wlList .row-more-btn{
  width:32px; height:32px;
  background:rgba(var(--glass-rgb),0.78);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  color:var(--text);
}
body[data-view-mode="grid"] #wlList .wl-quick-form{margin-top:6px;}
/* Téléphone : 3 colonnes */
@media (max-width:600px){
  body[data-view-mode="grid"] :is(#filmList, #seriesList, #topList, #wlList){
    grid-template-columns:repeat(3, minmax(0, 1fr));
    gap:var(--sp-4) var(--sp-3);
  }
}
/* « Afficher plus » du catalogue (défilement continu, js/app.js) */
.load-more-btn{
  display:block; margin:var(--sp-6) auto 0;
  background:none; border:1px solid var(--line-strong); border-radius:var(--r-full);
  height:40px; padding:0 20px;
  color:var(--text-2); font-size:var(--fs-sm); font-weight:600; cursor:pointer;
  transition:color var(--dur-fast), border-color var(--dur-fast);
}
.load-more-btn:hover{color:var(--text); border-color:var(--text-2);}
#filmDetailRatingRing{cursor:pointer; transition:transform var(--dur-press) var(--ease-out);}
#filmDetailRatingRing:active{transform:scale(0.97);}
#filmDetailNotes .wl-note{margin-top:0;}

/* ==========================================================
   LOT 5 DE L'AUDIT : ajouter un film depuis la recherche, entête plus
   court sur téléphone.
   ========================================================== */
/* Le bouton flottant « + » est remplacé par la recherche (audit M2/M13) ;
   il reste dans le HTML pour le JS qui le lit. */
#fabAddBtn{display:none !important;}
/* Objectif annuel sans trait au-dessus (moins de lignes, audit M13) */
.annual-goal-widget{border-top:none; padding-top:0;}
@media (max-width:600px){
  header{margin-bottom:var(--sp-4);}
  .annual-goal-widget{padding:0;}
  .toolbar{margin-bottom:var(--sp-5);}
}

/* ==========================================================
   LOT 6 DE L'AUDIT : formulaires repliés, chiffres en blanc.
   ========================================================== */
/* Formulaires de création repliés derrière une action (audit M3, M8) */
.create-disclosure{margin-bottom:var(--sp-5);}
.create-disclosure > summary{
  display:inline-flex; align-items:center; gap:var(--sp-2);
  list-style:none; cursor:pointer;
  font-size:var(--fs-sm); font-weight:600; color:var(--accent);
  padding:var(--sp-2) 0;
}
.create-disclosure > summary::-webkit-details-marker{display:none;}
.create-disclosure > summary:hover{color:var(--text);}
.create-disclosure[open] > summary{margin-bottom:var(--sp-3);}
.create-disclosure .wl-add-form{border-bottom:none; padding-bottom:0; margin-bottom:0;}
.ended-disclosure > summary{color:var(--text-2); font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700;}
.ended-disclosure > summary::after{content:"▾"; font-size:var(--fs-sm); color:var(--text-2); transition:transform var(--dur-fast);}
.ended-disclosure[open] > summary::after{transform:rotate(180deg);}
.ended-row{cursor:pointer;}
/* Statistiques : chiffres en blanc, sans boîtes (audit M10) */
.stat-tile{background:none; border:none; border-top:1px solid var(--line-strong); border-radius:0; padding:var(--sp-3) 0 0; text-align:left;}
.stat-tile .stat-value,
.stat-tile[class*="accent-"] .stat-value{color:var(--text); background:none; -webkit-background-clip:border-box; background-clip:border-box;}
.stat-tiles-4{grid-template-columns:repeat(4, minmax(0, 1fr));}
@media (max-width:600px){ .stat-tiles-4{grid-template-columns:repeat(2, minmax(0, 1fr));} }

/* ==========================================================
   LOT 7 DE L'AUDIT : pastilles d'icônes neutres dans le menu profil
   (une couleur par rubrique, audit D7). Le rouge reste pour une action
   destructive.
   ========================================================== */
.mm-icon.ic-violet, .mm-icon.ic-gold, .mm-icon.ic-teal, .mm-icon.ic-bronze{
  background:rgba(var(--text-rgb),0.06); color:var(--text-2);
}

/* ==========================================================
   OPTION B (draft navigation du 15/10, validée) : Films / Séries en
   interrupteur dans le titre, objectif annuel à droite, recherche + filtre
   + affichage sur une ligne, page Profil, Statistiques/Succès/Cartes/
   Journal affichés comme des pages. Draft :
   https://claude.ai/artifact/MeK2Q8Ydg7AufobkFsSVDU
   ========================================================== */
/* Les petits onglets sous le logo laissent la place à l'interrupteur.
   #primaryTabs reste dans le DOM : js/auth.js et js/router.js le lisent. */
#primaryTabs{display:none !important;}
/* Entête : le mot, les pages, l'avatar. Le pseudo vit sur la page Profil. */
#userDisplayName{display:none;}
.user-avatar-btn.active{outline:2px solid var(--accent); outline-offset:2px;}

/* Titre interrupteur + objectif annuel */
.catalog-head{
  display:flex; align-items:baseline; flex-wrap:wrap;
  gap:var(--sp-4);
  margin-bottom:var(--sp-5);
}
.catalog-switch{display:flex; align-items:baseline; gap:var(--sp-4);}
.catalog-switch-btn{
  background:none; border:0; padding:0; cursor:pointer;
  font-family:var(--font-display); font-weight:800; font-size:var(--fs-xl);
  letter-spacing:-0.02em; line-height:1.1;
  color:var(--text-3);
  transition:color var(--dur-fast);
}
.catalog-switch-btn:hover{color:var(--text-2);}
.catalog-switch-btn.is-active{color:var(--text-1); cursor:default;}
.catalog-switch-btn:focus-visible{outline:2px solid var(--accent); outline-offset:4px; border-radius:var(--r-sm);}
.catalog-head #annualGoalWidget{margin-left:auto; min-width:180px;}
.catalog-head #annualGoalWidget:empty{display:none;}
.catalog-head .annual-goal-widget{gap:6px; margin:0; padding:0; border:0;}
.catalog-head .annual-goal-head{gap:var(--sp-2);}
.catalog-head .annual-goal-count{margin-left:0; flex:1; color:var(--text-2);}
.catalog-head .annual-goal-bar{height:4px;}
.catalog-head .annual-goal-fill,
.catalog-head .annual-goal-widget.is-complete .annual-goal-fill{background:var(--accent);}
.catalog-head .annual-goal-widget.is-complete .annual-goal-count{color:var(--text-1);}
.catalog-head .annual-goal-done-msg{font-size:var(--fs-xs); color:var(--accent);}
.annual-goal-edit{display:flex; color:var(--text-3);}
.annual-goal-edit svg{width:14px; height:14px; display:block;}
.annual-goal-edit:hover{color:var(--text-1);}
.catalog-head .annual-goal-form{flex-wrap:nowrap; gap:var(--sp-2);}
.catalog-head .annual-goal-label{font-size:var(--fs-sm); font-weight:600; color:var(--text-2); white-space:nowrap;}
.catalog-head .annual-goal-form input[type="number"]{width:76px; padding:6px 10px; border-radius:var(--r-full);}
.catalog-head .annual-goal-form .btn{padding:7px 14px; border-radius:var(--r-full);}

/* Recherche, filtre et affichage dans une seule pilule */
#appContainer .toolbar, #seriesPage .toolbar{flex-wrap:nowrap; gap:var(--sp-2); margin:0 0 var(--sp-5);}
.toolbar-search{
  flex:1; min-width:0;
  height:42px; padding:0 6px 0 16px; gap:10px;
  border:1px solid var(--line); border-radius:var(--r-full);
  background:var(--surface);
  transition:border-color var(--dur-fast);
}
.toolbar-search:focus-within{border-color:var(--violet-dim);}
.toolbar-search-icon{width:18px; height:18px; flex:none; color:var(--text-2);}
.toolbar-search > input[type="text"]{
  flex:1; min-width:0; height:100%;
  padding:0; border:0; border-radius:0; background:none; box-shadow:none;
}
.toolbar-search > input[type="text"]:focus{border:0; box-shadow:none;}
/* Retour de Flix (15/10, téléphone) : le contour de focus générique
   dessinait un carré autour du champ, sans bord, au milieu de la pilule.
   La pilule montre déjà le focus (:focus-within ci-dessus). */
.toolbar-search > input[type="text"]:focus-visible{outline:none;}
.toolbar-search > input[type="text"]::placeholder{color:var(--text-3);}
.toolbar-search #filtersToggleBtn,
.toolbar-search .view-toggle-btn{
  width:34px; height:34px; padding:0; flex:none;
  display:grid; place-items:center;
  border:0; border-radius:var(--r-full);
  background:none; box-shadow:none;
  color:var(--text-2);
  transition:background-color var(--dur-fast), color var(--dur-fast);
}
.toolbar-search #filtersToggleBtn:hover,
.toolbar-search .view-toggle-btn:hover{background:var(--surface-2); color:var(--text-1);}
.toolbar-search #filtersToggleBtn svg,
.toolbar-search .view-toggle-btn svg{width:18px; height:18px; display:block;}
.toolbar-search #filtersToggleBtn.has-active-filter::after{top:5px; right:5px;}
/* Une seule icône d'affichage : celle du mode en cours, un clic bascule
   (gestionnaire dans js/ui.js). */
.toolbar-search .view-toggle{border:0; padding:0; background:none; gap:0;}
.toolbar-search .view-toggle-btn:not(.is-active){display:none;}
.toolbar-search .view-toggle-btn.is-active{background:none; color:var(--text-2);}
.toolbar-search .view-toggle-btn.is-active:hover{background:var(--surface-2); color:var(--text-1);}
/* Ailleurs (À voir, Top films, Bientôt), la bascule garde ses deux
   icônes, sans cadre : icône active en blanc, l'autre en gris. */
.view-toggle{border:0; background:none; padding:0;}
.view-toggle-btn{width:34px; height:34px; border-radius:var(--r-full); color:var(--text-3);}
.view-toggle-btn svg{width:18px; height:18px; display:block;}
.view-toggle-btn.is-active{background:none; color:var(--text-1);}
/* Compteur à droite de la pilule, masqué sur téléphone (draft) */
.toolbar-actions{flex-wrap:nowrap;}
.toolbar .count-line{margin:0; white-space:nowrap; color:var(--text-2);}
#seriesAddBtn{border-radius:var(--r-full);}

/* Barre du bas : l'onglet Profil s'allume sur sa page */
.mobile-tab.active :is(.mobile-tab-avatar, .mobile-tab-avatar-fallback){outline:2px solid var(--accent); outline-offset:1px; border-radius:50%;}

/* Page Profil (#profilePage, js/profilePage.js) */
.pp-head{display:flex; align-items:center; gap:var(--sp-4); margin-top:var(--sp-2);}
.pp-avatar{
  width:68px; height:68px; flex:none; overflow:hidden;
  border-radius:50%; border:1px solid var(--line-strong); background:var(--surface-2);
  display:grid; place-items:center;
}
.pp-avatar img{width:100%; height:100%; object-fit:cover;}
.pp-avatar .user-avatar-fallback svg{width:28px; height:28px;}
.pp-id{flex:1; min-width:0;}
.pp-name{
  margin:0; font-family:var(--font-display); font-weight:800; font-size:var(--fs-xl);
  letter-spacing:-0.02em; line-height:1.1; overflow-wrap:break-word;
}
.pp-sub{margin-top:2px; font-size:var(--fs-sm); color:var(--text-2); font-variant-numeric:tabular-nums;}
.pp-edit{
  flex:none; background:none; border:0; padding:var(--sp-2) 0; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);
}
.pp-edit:hover{color:var(--text-1);}
.pp-figures{display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:var(--sp-4); margin:28px 0 var(--sp-6);}
.pp-figure{display:grid; gap:2px; padding-top:10px; border-top:1px solid var(--line-strong);}
.pp-figure b{font-family:var(--font-display); font-weight:800; font-size:var(--fs-xl); line-height:1.1; font-variant-numeric:tabular-nums;}
.pp-figure span{font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.pp-label{margin:0 0 10px; font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.pp-favs{display:grid; grid-template-columns:repeat(4, minmax(0, 120px)); gap:var(--sp-3); margin-bottom:var(--sp-6);}
.pp-fav{display:grid; gap:6px; min-width:0; padding:0; border:0; background:none; color:inherit; text-align:left; cursor:pointer;}
.pp-fav-poster{display:block; width:100%; aspect-ratio:2/3; object-fit:cover; border-radius:var(--r-sm); transition:transform var(--dur-fast) var(--ease-out);}
.pp-fav:hover .pp-fav-poster{transform:translateY(-2px);}
.pp-fav:focus-visible{outline:none;}
.pp-fav:focus-visible .pp-fav-poster{outline:2px solid var(--accent); outline-offset:2px;}
.pp-fav-title{font-size:var(--fs-sm); font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.pp-favs-empty{
  grid-column:1 / -1; justify-self:start;
  padding:var(--sp-4) var(--sp-5); border:1.5px dashed var(--line-strong); border-radius:var(--r-sm);
  background:none; color:var(--text-2); font-family:var(--font-text); font-size:var(--fs-sm); cursor:pointer;
  transition:border-color var(--dur-fast), color var(--dur-fast);
}
.pp-favs-empty:hover{border-color:var(--accent); color:var(--accent);}
.pp-rows{margin-bottom:var(--sp-6); border-top:1px solid var(--line);}
.pp-row{
  display:flex; align-items:center; gap:var(--sp-3);
  width:100%; padding:14px 0;
  border:0; border-bottom:1px solid var(--line); background:none;
  color:var(--text-1); font-family:var(--font-text); font-size:var(--fs-md); text-align:left;
  cursor:pointer;
}
.pp-row[hidden], .pp-row[data-mobile-only]{display:none;}
.pp-row:focus-visible{outline:2px solid var(--accent); outline-offset:-2px; border-radius:var(--r-sm);}
.pp-row-main{flex:1; min-width:0; display:flex; flex-direction:column; gap:2px;}
.pp-row-title{display:flex; align-items:center; gap:var(--sp-2);}
.pp-row-sub{font-size:var(--fs-sm); color:var(--text-2);}
.pp-val{font-size:var(--fs-sm); color:var(--text-2); font-variant-numeric:tabular-nums; white-space:nowrap;}
.pp-chev{display:flex; color:var(--text-3); transition:transform var(--dur-fast), color var(--dur-fast);}
.pp-chev svg{width:16px; height:16px;}
.pp-row:hover .pp-chev{color:var(--text-2);}
.pp-row-link .pp-row-title{font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
/* « Modifier le profil » : la fenêtre ne garde que photo, pseudo et films */
.pf-hub{display:none !important;}

/* Statistiques, Succès, Cartes, Journal : affichés comme des pages, sous
   l'entête (z-index 50) et la barre du bas (30). --app-header-h est posé
   par js/profilePage.js. */
.overlay.as-page{
  z-index:25;
  padding:var(--app-header-h, 96px) 0 0;
  background:var(--ink);
  backdrop-filter:none; -webkit-backdrop-filter:none;
}
/* --ink reste sombre en thème clair : le fond de page clair à la place
   (audit du thème clair du 23/10, texte foncé sur fond noir sinon). */
:root[data-theme="light"] .overlay.as-page{background:var(--ink-surface);}
.overlay.as-page .modal{
  width:100%; max-width:none; min-height:100%; margin:0;
  padding:var(--sp-2) max(32px, calc((100% - 1080px) / 2 + 32px)) var(--sp-8);
  background:none; backdrop-filter:none; -webkit-backdrop-filter:none;
  border:0; border-radius:0; box-shadow:none;
  animation:pageIn .18s cubic-bezier(.2,.8,.3,1);
}
.overlay.as-page.closing .modal{animation:none;}
/* Page défilée puis rubrique ouverte : le verrou de défilement (html.scroll-locked)
   fait remonter l'entête collant hors de l'écran. Tant qu'une rubrique est
   affichée en page, l'entête est fixé en haut (le contenu derrière est couvert). */
html:has(.overlay.as-page:is(.open, .closing)) header{
  position:fixed; top:env(safe-area-inset-top, 0px); left:0; right:0;
}
@media (prefers-reduced-motion:reduce){ .overlay.as-page .modal{animation:none;} }

@media (max-width:600px){
  .catalog-head #annualGoalWidget{margin-left:0; width:100%; min-width:0;}
  .toolbar .count-line{display:none;}
  .pp-head{flex-wrap:wrap;}
  .pp-figures{grid-template-columns:repeat(2, minmax(0, 1fr));}
  .pp-favs{grid-template-columns:repeat(4, minmax(0, 1fr)); gap:var(--sp-2);}
  .pp-row[data-mobile-only]:not([hidden]){display:flex;}
  .overlay.as-page .modal{padding:var(--sp-2) 18px calc(60px + var(--mobile-tabbar-h) + env(safe-area-inset-bottom, 0px));}
}

/* ==========================================================
   AUDIT D6 : un seul jeu de pictos au trait (uiIcon(), js/data.js) à la
   place des émojis et des glyphes de l'interface. Les émojis restent
   dans les commentaires, les réactions et les jeux (succès, cartes).
   ========================================================== */
.ui-icon{display:inline-block; width:1em; height:1em; vertical-align:-0.14em; flex:none; pointer-events:none;}
.ui-icon-filled{fill:currentColor;}
.friend-avatar-placeholder{opacity:1; color:var(--text-2);}
.friend-avatar-placeholder .ui-icon{width:18px; height:18px;}
.star-btn .ui-icon{display:block;}
.star-btn.active .ui-icon, #filmDetailFavBtn.active .ui-icon{fill:currentColor;}
body[data-view-mode="grid"] #filmList .star-btn .ui-icon{width:16px; height:16px;}
.review-badge .ui-icon{width:13px; height:13px;}
.rewatch-badge .ui-icon{width:11px; height:11px;}
.poster-wall-fav .ui-icon{filter:drop-shadow(0 1px 2px rgba(0,0,0,0.6));}
.wl-title > .ui-icon:first-child{color:var(--text-2); margin-right:2px;}

/* ==========================================================
   À VOIR : plateformes sur les affiches (draft du 15/10, option A) et
   « Mes abonnements » (watchlistServicesHtml(), renderMyServicesChips(),
   js/watchlist.js). Logos dans le coin bas de l'affiche, deux au plus.
   ========================================================== */
.wl-services{display:flex; align-items:center; gap:var(--sp-1); flex:none; pointer-events:none;}
.wl-services img{width:24px; height:24px; border-radius:var(--r-sm); display:block; box-shadow:0 0 0 1px rgba(0,0,0,0.35), 0 2px 6px rgba(0,0,0,0.45);}
.wl-services-more{
  height:24px; min-width:24px; padding:0 6px;
  display:grid; place-items:center;
  border-radius:var(--r-full);
  background:rgba(var(--glass-rgb),0.78);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  color:var(--text); font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums;
}
/* Grille : calque de la taille de l'affiche (2:3, en haut de la case),
   logos posés dans son coin bas gauche. */
body[data-view-mode="grid"] #wlList .wl-services{
  position:absolute; top:0; left:0; right:0; z-index:1;
  aspect-ratio:2/3; align-items:flex-end; padding:6px;
  transition:transform var(--dur-fast) var(--ease-out);
}
body[data-view-mode="grid"] #wlList .wl-row-clickable:hover .wl-services{transform:translateY(-2px);}
@media (max-width:600px){
  body[data-view-mode="grid"] #wlList .wl-services{padding:5px; gap:3px;}
  body[data-view-mode="grid"] #wlList .wl-services img{width:20px; height:20px;}
  body[data-view-mode="grid"] #wlList .wl-services-more{height:20px; min-width:20px; padding:0 5px;}
}
/* Le filtre et « Mes abonnements » ensemble à droite, « Surprends-moi » à gauche. */
#wlProviderFilter{margin-left:auto;}
.wl-subs-toggle{
  height:36px; padding:0 var(--sp-2);
  background:none; border:none; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2);
  transition:color var(--dur-fast);
}
.wl-subs-toggle:hover, .wl-subs-toggle[aria-expanded="true"]{color:var(--text-1);}
.wl-subs{margin:0 0 var(--sp-4);}
.wl-subs-hint{margin:0 0 var(--sp-2); font-size:var(--fs-xs); color:var(--text-2);}
.wl-subs-chips{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.wl-sub-chip{
  display:inline-flex; align-items:center; gap:var(--sp-2);
  height:32px; padding:0 var(--sp-3) 0 var(--sp-1);
  border:1px solid var(--line); border-radius:var(--r-full); background:none; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2);
  transition:border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
}
.wl-sub-chip:not(:has(img)){padding-left:var(--sp-3);}
.wl-sub-chip img{width:24px; height:24px; border-radius:var(--r-full); display:block;}
.wl-sub-chip:hover{color:var(--text-1); border-color:var(--line-strong);}
.wl-sub-chip[aria-pressed="true"]{color:var(--text-1); border-color:var(--accent); background:var(--accent-wash);}
.wl-sub-chip:active{transform:scale(0.97);}

/* ==========================================================
   SUGGESTIONS SOUS LA RECHERCHE (draft du 15/10, option A) : meilleur
   résultat puis rubriques, films ou séries à ajouter depuis TMDB
   (js/globalSearch.js). Niveau 2 du code de design : ce qui flotte.
   ========================================================== */
.search-suggest{
  position:absolute; top:calc(100% + var(--sp-2)); left:0; z-index:40;
  width:min(560px, 100%);
  max-height:min(70vh, 560px); overflow-y:auto; overscroll-behavior:contain;
  padding:var(--sp-2) 0;
  background:color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid var(--line); border-radius:var(--r-md);
  box-shadow:var(--shadow-float);
  text-align:left; cursor:default;
}
.sg-label{padding:var(--sp-3) var(--sp-4) var(--sp-1); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.sg-row{display:flex; align-items:center; gap:var(--sp-3); min-height:56px; padding:var(--sp-1) var(--sp-4); cursor:pointer;}
.sg-row:hover, .sg-row.is-active{background:var(--surface-2);}
.sg-thumb{width:32px; height:48px; flex:none; border-radius:var(--r-sm); object-fit:cover; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.sg-thumb svg{width:16px; height:16px;}
.sg-face{width:36px; height:36px; flex:none; border-radius:var(--r-full); object-fit:cover; background:var(--surface-2); color:var(--text-2); display:grid; place-items:center;}
.sg-glyph{width:36px; height:36px; flex:none; border-radius:var(--r-sm); background:var(--surface-2); color:var(--text-2); display:grid; place-items:center;}
.sg-face .ui-icon, .sg-glyph .ui-icon{width:20px; height:20px;}
.sg-main{flex:1; min-width:0; display:flex; flex-direction:column;}
.sg-title{font-size:var(--fs-sm); font-weight:400; color:var(--text-1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.sg-title b{font-weight:600;}
.sg-sub{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.sg-note{flex:none; font-size:var(--fs-sm); font-weight:600; font-variant-numeric:tabular-nums;}
.sg-note.rate-low{color:var(--note-low);}
.sg-note.rate-mid{color:var(--note-mid);}
.sg-note.rate-high{color:var(--note-high);}
.sg-add{width:28px; height:28px; flex:none; border-radius:var(--r-full); border:1px solid var(--line-strong); color:var(--text-2); display:grid; place-items:center; font-size:var(--fs-md); line-height:1;}
/* Le meilleur résultat, un cran plus grand */
.sg-top .sg-row{min-height:64px;}
.sg-top .sg-thumb{width:40px; height:60px;}
.sg-top .sg-face, .sg-top .sg-glyph{width:44px; height:44px;}
.sg-top .sg-title{font-size:var(--fs-md);}
.sg-more{padding:var(--sp-3) var(--sp-4); margin-top:var(--sp-1); border-top:1px solid var(--line); font-size:var(--fs-sm); font-weight:600; color:var(--accent); cursor:pointer;}
.sg-more + .sg-more{margin-top:0; border-top:none;}
.sg-more:hover, .sg-more[aria-selected="true"]{background:var(--surface-2);}
.sg-empty{padding:var(--sp-3) var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);}
.sg-hint{display:flex; gap:var(--sp-4); margin-top:var(--sp-1); padding:var(--sp-3) var(--sp-4) var(--sp-1); border-top:1px solid var(--line); font-size:var(--fs-xs); color:var(--text-3);}
.sg-hint kbd{font-family:var(--font-text); font-size:var(--fs-xs); padding:1px 6px; border:1px solid var(--line-strong); border-radius:var(--r-full); color:var(--text-2);}
@media (hover:none){ .sg-hint{display:none;} }
@media (max-width:600px){
  .search-suggest{width:100%; max-height:60vh;}
  .sg-hint{display:none;}
}

/* ==========================================================
   CONNEXION : mur d'affiches (piste A, audit et draft du 15/10).
   Le mur part du haut de la page, derrière l'entête, et s'efface vers
   la gauche (vers le bas sur téléphone). Titre, phrase et connexion
   posés dessus, sans boîte. Un seul bouton principal, tout en pilule.
   ========================================================== */
#authContainer{position:static; padding:var(--sp-7) var(--sp-6) var(--sp-8);}
#authContainer [hidden]{display:none !important;}
.auth-wall{
  position:absolute; top:0; right:0; z-index:0;
  width:64vw; height:max(100vh, 760px); overflow:hidden; pointer-events:none;
  display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:var(--sp-3); padding:var(--sp-3);
  align-content:start; opacity:0.62;
  -webkit-mask-image:linear-gradient(to right, transparent 0%, transparent 18%, #000 55%);
  mask-image:linear-gradient(to right, transparent 0%, transparent 18%, #000 55%);
}
.auth-wall img{width:100%; aspect-ratio:2/3; object-fit:cover; border-radius:var(--r-sm); display:block; background:var(--surface-2);}
.auth-wall img:nth-child(6n+2), .auth-wall img:nth-child(6n+4), .auth-wall img:nth-child(6n+6){transform:translateY(-40%);}
/* L'entête laisse voir le mur, sans numéro de version sur cette page */
html:has(#authContainer:not([style*="none"])) header{background:none; backdrop-filter:none; -webkit-backdrop-filter:none;}
html:has(#authContainer:not([style*="none"])) #versionTag{display:none !important;}
#authContainer .auth-hero{
  position:relative; z-index:1;
  display:flex; flex-direction:column; gap:var(--sp-5);
  max-width:440px; margin:0; padding:0;
}
#authContainer .auth-pitch{max-width:none; display:grid; gap:var(--sp-4);}
#authContainer .auth-headline{font-size:var(--fs-2xl); line-height:1.02; margin:0;}
#authContainer .auth-pitch-sub{font-size:var(--fs-md); font-weight:400; color:var(--text-2); max-width:44ch; margin:0;}
.auth-form{display:grid; gap:var(--sp-3); width:100%; max-width:360px;}
.auth-form-title{font-family:var(--font-text); font-size:var(--fs-md); font-weight:600; margin:0 0 2px;}
#authContainer .google-btn{
  height:44px; width:100%; border-radius:var(--r-full);
  background:transparent; color:var(--text-1); border:1px solid var(--line-strong);
  font-size:var(--fs-sm); font-weight:600;
}
#authContainer .google-btn:hover{background:var(--surface-2);}
#authContainer .auth-divider{margin:0; color:var(--text-3);}
.auth-email{
  height:44px; width:100%; padding:0 18px;
  border:1px solid var(--line); border-radius:var(--r-full); background:var(--surface);
  font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-1);
}
.auth-email::placeholder{color:var(--text-3);}
.auth-email:focus{outline:none; border-color:var(--accent);}
#authContainer #authSendBtn{height:44px; width:100%; border-radius:var(--r-full); background:var(--accent); color:var(--accent-ink); font-size:var(--fs-sm); font-weight:600;}
#authContainer .auth-status{margin:0; color:var(--text-2); font-size:var(--fs-sm);}
#authContainer .auth-status:empty{display:none;}
#authContainer .auth-card-foot{margin:0; font-size:var(--fs-xs); color:var(--text-2); text-align:left;}
.auth-sent{display:grid; gap:var(--sp-3); max-width:360px;}
.auth-sent-icon{width:44px; height:44px; border-radius:var(--r-full); background:var(--accent-wash); color:var(--accent); display:grid; place-items:center;}
.auth-sent-icon svg{width:22px; height:22px;}
.auth-sent-title{font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; letter-spacing:-0.01em; margin:0;}
.auth-sent-text{margin:0; font-size:var(--fs-sm); color:var(--text-2); line-height:1.55;}
.auth-sent-text b{color:var(--text-1); font-weight:600;}
.auth-sent-links{display:flex; flex-wrap:wrap; gap:var(--sp-5);}
.auth-sent-links button{background:none; border:none; padding:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
@media (max-width:600px){
  #authContainer{padding:0 var(--sp-4) var(--sp-7);}
  .auth-wall{
    left:0; right:0; width:auto; height:300px;
    grid-template-columns:repeat(4, minmax(0, 1fr)); gap:var(--sp-2); padding:var(--sp-2);
    opacity:0.7;
    -webkit-mask-image:linear-gradient(to bottom, #000 25%, transparent 80%);
    mask-image:linear-gradient(to bottom, #000 25%, transparent 80%);
  }
  .auth-wall img:nth-child(n+13){display:none;}
  .auth-wall img{transform:none !important;}
  html:has(#authContainer:not([style*="none"])) header{background:linear-gradient(to bottom, rgba(var(--glass-rgb),0.85), rgba(var(--glass-rgb),0));}
  #authContainer .auth-hero{padding-top:150px; gap:var(--sp-4);}
}

/* ==========================================================
   BIENTÔT : fil par échéance (piste A, audit et draft du 15/10).
   Lignes simples sans boîte, la date à droite, « Au cinéma » en dernier
   (renderUpcoming(), js/upcoming.js). Calendrier en option, sans cases
   encadrées : un filet par semaine, aujourd'hui en bleu.
   ========================================================== */
#upcomingPage .page-header{align-items:center;}
.up-summary{margin:0 0 var(--sp-5); font-size:var(--fs-sm); color:var(--text-2);}
.up-summary b{color:var(--text-1); font-weight:600;}
.up-summary:empty{display:none;}
.up-section{margin-top:var(--sp-6);}
.up-section:first-child{margin-top:0;}
.up-label{display:flex; align-items:baseline; gap:var(--sp-2); margin:0 0 var(--sp-1); font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.up-label span{font-weight:400; letter-spacing:0; text-transform:none; color:var(--text-3);}
.up-row{display:flex; align-items:center; gap:var(--sp-4); min-height:72px; padding:var(--sp-1) 0; border-bottom:1px solid var(--line); cursor:pointer;}
.up-row:hover .up-title, .up-row:focus-visible .up-title{color:var(--accent);}
.up-thumb{width:40px; height:60px; flex:none; border-radius:var(--r-sm); object-fit:cover; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.up-thumb svg{width:18px; height:18px;}
.up-main{flex:1; min-width:0;}
.up-title{font-size:var(--fs-sm); font-weight:600; color:var(--text-1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
.up-sub{display:flex; align-items:center; gap:var(--sp-2); font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.up-logo{width:16px; height:16px; flex:none; border-radius:var(--r-sm);}
.up-when{flex:none; display:flex; flex-direction:column; text-align:right;}
.up-when b{font-size:var(--fs-sm); font-weight:600; font-variant-numeric:tabular-nums; color:var(--text-1);}
.up-when span{font-size:var(--fs-xs); color:var(--text-2);}
.up-when.is-now b{color:var(--accent);}
.up-section.is-compact .up-row{min-height:56px;}
.up-section.is-compact .up-thumb{width:32px; height:48px;}
.up-add{
  flex:none; height:32px; padding:0 var(--sp-3);
  border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-1);
  transition:border-color var(--dur-fast), color var(--dur-fast);
}
.up-add:hover{border-color:var(--accent); color:var(--accent);}
.up-add.is-added{border-color:var(--accent); color:var(--accent); cursor:default;}
@media (max-width:600px){
  .up-row{gap:var(--sp-3);}
  .up-when b{font-size:var(--fs-xs);}
}
/* Calendrier (option) */
.upcoming-cal-nav{justify-content:flex-start; gap:var(--sp-2); margin-bottom:var(--sp-2);}
.upcoming-cal-arrow{width:32px; height:32px; border:none; border-radius:var(--r-full); background:none; cursor:pointer; color:var(--text-2); font-size:var(--fs-lg); line-height:1;}
.upcoming-cal-arrow:hover:not(:disabled){background:var(--surface-2); color:var(--text-1);}
.upcoming-cal-arrow:disabled{opacity:0.3; cursor:default;}
.upcoming-cal-label{font-size:var(--fs-md); min-width:9em; text-align:center;}
.upcoming-cal-grid{gap:0;}
.upcoming-cal-day{min-height:84px; padding:var(--sp-2) var(--sp-1); background:none; border:none; border-top:1px solid var(--line); border-radius:0;}
.upcoming-cal-day.is-today{box-shadow:none;}
.upcoming-cal-day.is-today .upcoming-cal-daynum{color:var(--accent); font-weight:600;}
.upcoming-cal-poster{width:28px; height:42px;}
@media (max-width:600px){
  .upcoming-cal-day{min-height:56px;}
  .upcoming-cal-poster{width:20px; height:30px;}
}

/* ==========================================================
   AMIS : fil d'affiches (piste A) avec les lignes d'amis de la piste B
   (audit et draft du 15/10). Titre interrupteur Amis · Groupes · Listes,
   lignes simples sans boîte, la compatibilité « en phase » à droite.
   ========================================================== */
.social-switch{flex-wrap:wrap; row-gap:var(--sp-1);}
#amisPage .wl-row{background:none; border:none; border-bottom:1px solid var(--line); border-radius:0; padding:var(--sp-2) 0; margin:0; box-shadow:none; transform:none;}
#amisPage .wl-row:hover{background:none; border-color:var(--line); box-shadow:none; transform:none;}
.fr-section{margin-top:var(--sp-6);}
.fr-section:first-of-type{margin-top:var(--sp-2);}
.fr-label{display:flex; align-items:baseline; gap:var(--sp-2); margin:0 0 var(--sp-1); font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.fr-label span{font-weight:400; letter-spacing:0; text-transform:none; color:var(--text-3);}
.fr-main{flex:1; min-width:0;}
.fr-name{font-size:var(--fs-sm); font-weight:600; color:var(--text-1);}
.fr-sub{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.fr-note{flex:none; font-size:var(--fs-sm); font-weight:600; font-variant-numeric:tabular-nums;}
.fr-note.rate-low, .fr-poster-note.rate-low{color:var(--note-low);}
.fr-note.rate-mid, .fr-poster-note.rate-mid{color:var(--note-mid);}
.fr-note.rate-high, .fr-poster-note.rate-high{color:var(--note-high);}
/* Lignes d'amis */
.fr-row, .fr-feed{display:flex; align-items:center; gap:var(--sp-3); min-height:72px; padding:var(--sp-1) 0; border-bottom:1px solid var(--line); cursor:pointer;}
.fr-row .friend-avatar{width:44px; height:44px;}
.fr-row:hover .fr-name, .fr-row:focus-visible .fr-name{color:var(--accent);}
.fr-last{display:flex; align-items:center; gap:var(--sp-2); flex:none;}
.fr-last img{width:28px; height:42px; border-radius:var(--r-sm); object-fit:cover; display:block;}
.fr-compat{flex:none; min-width:72px; text-align:right; display:flex; flex-direction:column;}
.fr-compat b{font-size:var(--fs-sm); font-weight:600; font-variant-numeric:tabular-nums; color:var(--text-1);}
.fr-compat span{font-size:var(--fs-xs); color:var(--text-2);}
.fr-compat.is-low span{color:var(--text-3);}
/* Fil « Ce qu'ils regardent » */
.fr-feed .friend-avatar{width:32px; height:32px;}
.fr-feed-text{font-size:var(--fs-sm); line-height:1.4; color:var(--text-1);}
.fr-feed-text b{font-weight:600;}
.fr-feed .fr-film{transition:color var(--dur-fast);}
.fr-feed:hover .fr-film, .fr-feed:focus-visible .fr-film{color:var(--accent);}
.fr-feed-poster{width:36px; height:54px; border-radius:var(--r-sm); object-fit:cover; flex:none;}
/* Recommandé par tes amis : en affiches */
.fr-posters{display:grid; grid-template-columns:repeat(auto-fill, minmax(120px, 1fr)); gap:var(--sp-5) var(--sp-4);}
.fr-poster{display:grid; gap:6px; min-width:0; cursor:pointer;}
.fr-poster-img{position:relative; aspect-ratio:2/3; border-radius:var(--r-sm); overflow:hidden; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.fr-poster-img img{width:100%; height:100%; object-fit:cover; display:block;}
.fr-poster-img > svg{width:32px; height:32px;}
.fr-poster-note{position:absolute; top:6px; right:6px; padding:4px 7px; border-radius:var(--r-sm); background:rgba(var(--glass-rgb),0.78); font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums;}
.fr-poster-title{font-size:var(--fs-sm); font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.fr-poster-sub{font-size:var(--fs-xs); color:var(--text-2); margin-top:-4px;}
.fr-poster:hover .fr-poster-title{color:var(--accent);}
/* Demande reçue : en tête, mise en avant */
.fr-requests{margin-top:var(--sp-2);}
#amisPage .fr-request{border:1px solid var(--accent); border-radius:var(--r-md); background:var(--accent-wash); padding:var(--sp-3) var(--sp-4); margin-bottom:var(--sp-2);}
#amisPage .fr-request:hover{border-color:var(--accent);}
.fr-request .wl-title{font-weight:400; font-size:var(--fs-sm);}
.fr-request .wl-title b{font-weight:600;}
.fr-text-btn{background:none; border:none; padding:0 var(--sp-2); cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-2);}
.fr-text-btn:hover{color:var(--text-1);}
.fr-line-btn{height:32px; padding:0 var(--sp-3); border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-1); transition:border-color var(--dur-fast), color var(--dur-fast); flex:none;}
.fr-line-btn:hover{border-color:var(--accent); color:var(--accent);}
/* « + Ajouter un ami » replié */
.fr-add{margin-top:var(--sp-6);}
.fr-add-field{margin-bottom:var(--sp-4);}
#friendSearchInput{height:40px; width:100%; max-width:420px; padding:0 var(--sp-4); border:1px solid var(--line); border-radius:var(--r-full); background:var(--surface); font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-1);}
#friendSearchInput:focus{outline:none; border-color:var(--accent);}
.fr-add .fr-label{margin-top:var(--sp-4);}
#amisPage .list-toggle-btn{width:auto; margin:var(--sp-2) 0 0; background:none; border:none; padding:0; color:var(--accent); font-size:var(--fs-sm); font-weight:600;}
@media (max-width:600px){
  .fr-last{display:none;}
  .fr-compat{min-width:64px;}
  /* À côté de « Retour », les trois mots tiennent sur une ligne */
  .social-switch{gap:var(--sp-3);}
  .social-switch .catalog-switch-btn{font-size:var(--fs-lg);}
  /* 3 colonnes, comme les autres grilles d'affiches */
  .fr-posters{grid-template-columns:repeat(3, minmax(0, 1fr)); gap:var(--sp-4) var(--sp-3);}
}

/* ==========================================================
   TOP FILMS, piste A du 15/10 (draft Top films, choix de Flix).
   Onglets en texte dans l'en-tête, podium des trois premiers, liste
   avec ta note ou « + À voir », films à une seule note à part.
   Voir renderTopFilms(), js/top.js.
   ========================================================== */
#topPage .view-toggle[hidden]{display:none;}
#topPage .page-header{flex-wrap:wrap; row-gap:var(--sp-3); margin-bottom:var(--sp-4);}
#topPage .top-tabs{display:flex; flex-wrap:nowrap; gap:var(--sp-5); margin:0;}
.top-tab{background:none; border:0; border-bottom:2px solid transparent; border-radius:0; padding:0 0 var(--sp-1); cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2); white-space:nowrap; transition:color var(--dur-fast), border-color var(--dur-fast);}
.top-tab:hover{color:var(--text-1);}
.top-tab.is-active{color:var(--text-1); border-bottom-color:var(--accent);}
.top-tab:focus-visible{outline:2px solid var(--accent); outline-offset:4px; border-radius:var(--r-sm);}
.top-how{margin:0 0 var(--sp-5); max-width:62ch; font-size:var(--fs-xs); color:var(--text-2);}
.top-how[hidden]{display:none;}
.top-board{max-width:860px;}
/* Podium : trois affiches, la première un peu plus haut */
.top-podium{display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:var(--sp-5); align-items:end; max-width:600px; margin:var(--sp-5) 0 var(--sp-4);}
.top-pod{display:grid; gap:6px; min-width:0; cursor:pointer;}
.top-pod-img{position:relative; aspect-ratio:2/3; border-radius:var(--r-sm); overflow:hidden; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.top-pod.is-first .top-pod-img{transform:translateY(calc(-1 * var(--sp-3)));}
.top-pod-poster{width:100%; height:100%; object-fit:cover; display:block;}
.top-pod-img .top-poster-empty svg{width:32px; height:32px;}
.top-pod-rank{position:absolute; top:6px; left:6px; min-width:26px; height:26px; padding:0 6px; border-radius:var(--r-full); background:rgba(var(--glass-rgb),0.78); display:grid; place-items:center; font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums; color:var(--text-1);}
.top-pod.is-first .top-pod-rank{background:var(--accent); color:var(--accent-ink);}
.top-pod-note{position:absolute; top:6px; right:6px; padding:3px 6px; border-radius:var(--r-sm); background:rgba(var(--glass-rgb),0.78); font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums;}
.top-pod-title{font-size:var(--fs-sm); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
.top-pod-sub{font-size:var(--fs-xs); color:var(--text-2); margin-top:-4px;}
.top-pod:hover .top-pod-title, .top-pod:focus-visible .top-pod-title{color:var(--accent);}
.top-pod:focus-visible{outline:none;}
.top-pod:focus-visible .top-pod-img{outline:2px solid var(--accent); outline-offset:2px;}
/* Lignes */
.top-line{display:flex; align-items:center; gap:var(--sp-3); min-height:72px; padding:var(--sp-1) 0; border-bottom:1px solid var(--line); cursor:pointer;}
.top-line:hover .top-line-title, .top-line:focus-visible .top-line-title{color:var(--accent);}
.top-line-rank{flex:none; width:28px; text-align:right; font-size:var(--fs-sm); font-weight:600; color:var(--text-2); font-variant-numeric:tabular-nums;}
.top-line-poster{flex:none; width:40px; height:60px; border-radius:var(--r-sm); object-fit:cover; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.top-line-poster svg{width:18px; height:18px;}
.top-line-main{flex:1; min-width:0;}
.top-line-title{font-size:var(--fs-sm); font-weight:600; color:var(--text-1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
.top-line-sub{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.top-line-note{flex:none; width:40px; text-align:right; font-size:var(--fs-md); font-weight:600; font-variant-numeric:tabular-nums;}
.top-me{flex:none; width:104px; text-align:right; font-size:var(--fs-xs); color:var(--text-2);}
.top-me b{font-size:var(--fs-sm); font-weight:600;}
.top-add{height:30px; padding:0 var(--sp-3); border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-1); white-space:nowrap; transition:border-color var(--dur-fast), color var(--dur-fast);}
.top-add:hover{border-color:var(--accent); color:var(--accent);}
.top-add:disabled{opacity:0.5; cursor:default;}
:is(.top-pod-note, .top-line-note, .top-me b).rate-low{color:var(--note-low);}
:is(.top-pod-note, .top-line-note, .top-me b).rate-mid{color:var(--note-mid);}
:is(.top-pod-note, .top-line-note, .top-me b).rate-high{color:var(--note-high);}
/* Une seule note pour l'instant */
.top-solo{margin-top:var(--sp-7);}
.top-solo-label{display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--sp-1) var(--sp-2); margin:0 0 var(--sp-1); font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.top-solo-label span{font-weight:400; letter-spacing:0; text-transform:none; color:var(--text-3);}
.top-more{margin-top:var(--sp-3); background:none; border:0; padding:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
@media (max-width:600px){
  /* Onglets sur leur propre ligne, sous « Retour » et le titre */
  #topPage .top-tabs{order:3; width:100%;}
  .top-podium{gap:var(--sp-3); margin-top:var(--sp-4);}
  .top-pod.is-first .top-pod-img{transform:translateY(calc(-1 * var(--sp-2)));}
  .top-pod-title{font-size:var(--fs-xs);}
  .top-line{gap:var(--sp-2);}
  .top-line-rank{width:22px;}
  .top-line-poster{width:36px; height:54px;}
  .top-line-note{width:34px;}
  .top-me{width:auto; min-width:58px;}
}

/* ==========================================================
   PARAMÈTRES en page (#settingsPage, draft Profil du 15/10).
   Mêmes rangées que la page Profil (.pp-label, .pp-rows, .pp-row) :
   plus de fenêtre, plus de pictos colorés. Voir js/settings.js.
   ========================================================== */
#settingsPage .page-header{margin-bottom:var(--sp-5);}
#settingsPage .pp-rows{max-width:720px;}
#settingsPage .pp-label{max-width:720px;}
.st-row-static{cursor:default;}
.st-seg{display:inline-flex; flex:none; gap:2px; padding:3px; border:1px solid var(--line); border-radius:var(--r-full);}
.st-seg button{padding:5px 12px; border:0; border-radius:var(--r-full); background:none; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-2); transition:background-color var(--dur-fast), color var(--dur-fast);}
.st-seg button:hover{color:var(--text-1);}
.st-seg button[aria-checked="true"]{background:var(--accent); color:var(--accent-ink);}
.st-range{flex:none; width:180px; accent-color:var(--accent);}
/* Pays des plateformes : recherche et quelques pays sous la ligne « Pays »
   (renderSettingsRegionList(), js/settings.js). Le champ garde le style des
   autres champs (16 px, sinon iPhone agrandit la page). */
.st-region{padding:var(--sp-3) 0 var(--sp-2); border-bottom:1px solid var(--line);}
#settingsRegionBtn[aria-expanded="true"] .pp-chev{transform:rotate(90deg);}
.st-region input[type="text"]{width:100%; min-width:0;}
.st-region-list{display:flex; flex-direction:column; margin-top:var(--sp-2);}
.st-region-opt{display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2); width:100%; min-height:40px; padding:0 var(--sp-1); background:none; border:0; border-radius:var(--r-sm); cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-1); text-align:left; transition:background-color var(--dur-fast), color var(--dur-fast);}
.st-region-opt:hover{background:var(--surface-2);}
.st-region-opt:focus-visible{outline:2px solid var(--accent); outline-offset:-2px;}
.st-region-opt[aria-selected="true"]{color:var(--accent); font-weight:600;}
.st-region-opt[aria-selected="true"]::after{content:'\2713'; font-weight:600;}
.st-region-empty{padding:var(--sp-2) var(--sp-1); font-size:var(--fs-sm); color:var(--text-2);}
.st-row-danger .pp-row-title{color:var(--note-low);}
.st-install-body{font-size:var(--fs-sm); color:var(--text-2);}
.st-install-body .wl-note{margin:2px 0 0; font-size:var(--fs-sm); color:var(--text-2);}
.st-install-body .btn{margin-top:var(--sp-2);}
.st-confirm{max-width:440px;}
.st-confirm-text{margin:0 0 var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);}
.st-confirm .btn.danger:disabled{opacity:0.45; cursor:default;}
@media (max-width:600px){
  .st-range{width:120px;}
  .st-seg button{padding:5px 10px;}
}

/* ==========================================================
   DONNER UN AVIS (#avisPage) et ADMIN (#adminPage), draft Profil du
   15/10. Rangées du Profil, pastilles de type, réponses de l'admin.
   Voir js/feedback.js et la fin de js/admin.js.
   ========================================================== */
/* Pastilles de compte (Profil, entête) */
.pp-count, .nav-count{display:inline-grid; place-items:center; flex:none; min-width:20px; height:20px; padding:0 6px; border-radius:var(--r-full); background:var(--accent); color:var(--accent-ink); font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums;}
.pp-count[hidden], .nav-count[hidden]{display:none;}
.header-nav-btn[hidden]{display:none;}
.header-nav-btn .nav-count{min-width:18px; height:18px; margin-left:6px; padding:0 5px;}
.header-nav-btn.active .nav-count{background:var(--accent-ink); color:var(--accent);}
/* Avis */
#avisPage .page-header, #adminPage .page-header{margin-bottom:var(--sp-5);}
.av-form{max-width:640px; display:grid; gap:var(--sp-3); margin-bottom:var(--sp-7);}
.av-kinds{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.av-kind{height:36px; padding:0 var(--sp-4); border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2); transition:border-color var(--dur-fast), color var(--dur-fast), background-color var(--dur-fast);}
.av-kind:hover{color:var(--text-1); border-color:var(--text-3);}
.av-kind[aria-checked="true"]{background:var(--accent); border-color:var(--accent); color:var(--accent-ink);}
.av-message{width:100%; min-height:120px; padding:var(--sp-3) var(--sp-4); border:1px solid var(--line-strong); border-radius:var(--r-md); background:var(--surface); font-family:var(--font-text); font-size:var(--fs-md); color:var(--text-1); resize:vertical;}
.av-message:focus{outline:none; border-color:var(--accent);}
.av-ctx{border-top:1px solid var(--line);}
.av-form > .btn{justify-self:start;}
.av-mine{max-width:640px;}
.av-item{padding:var(--sp-3) 0; border-bottom:1px solid var(--line); display:grid; gap:var(--sp-1);}
.av-item-meta{font-size:var(--fs-xs); color:var(--text-2);}
.av-status{font-weight:600; color:var(--text-2);}
.av-status.is-done{color:var(--note-high);}
.av-item-msg{margin:0; font-size:var(--fs-sm); color:var(--text-1); white-space:pre-wrap;}
.av-reply{margin-top:var(--sp-1); padding:var(--sp-2) var(--sp-3); border-left:2px solid var(--accent); border-radius:0 var(--r-sm) var(--r-sm) 0; background:var(--surface); font-size:var(--fs-sm); color:var(--text-1); white-space:pre-wrap;}
.av-reply-label{display:block; margin-bottom:2px; font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--accent);}
/* Admin */
#adminPage .page-header{flex-wrap:wrap; row-gap:var(--sp-3);}
.adm-tabs{display:flex; flex-wrap:wrap; gap:var(--sp-2) var(--sp-5);}
.adm-tabs .top-tab{flex:none;}
.adm-content{max-width:860px;}
.adm-figs{display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:var(--sp-3); margin-bottom:var(--sp-6);}
.adm-figs-4{grid-template-columns:repeat(4, minmax(0, 1fr));}
.adm-fig{display:grid; gap:2px; padding:var(--sp-4); border:1px solid var(--line); border-radius:var(--r-md); background:none; text-align:left; cursor:pointer; color:var(--text-1); font-family:var(--font-text); transition:border-color var(--dur-fast);}
.adm-fig:disabled{cursor:default;}
.adm-fig:not(:disabled):hover{border-color:var(--line-strong);}
.adm-fig b{font-family:var(--font-display); font-size:var(--fs-xl); font-weight:800; line-height:1.1; font-variant-numeric:tabular-nums;}
.adm-fig.is-hot b{color:var(--accent);}
.adm-fig span{font-size:var(--fs-xs); color:var(--text-2);}
.adm-label{margin-top:var(--sp-6);}
.adm-fb{padding:var(--sp-3) 0; border-bottom:1px solid var(--line); display:grid; gap:var(--sp-2);}
.adm-fb.is-resolved .adm-fb-msg{color:var(--text-2);}
.adm-fb-top{display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--sp-1) var(--sp-3); font-size:var(--fs-xs); color:var(--text-2);}
.adm-fb-top b{font-size:var(--fs-sm); color:var(--text-1);}
.adm-kind{padding:1px 8px; border:1px solid var(--line-strong); border-radius:var(--r-full); font-weight:600; color:var(--text-1);}
.adm-fb-msg{margin:0; font-size:var(--fs-sm); white-space:pre-wrap;}
.adm-acts{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.adm-line-btn{height:32px; padding:0 var(--sp-3); border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-1); transition:border-color var(--dur-fast), color var(--dur-fast);}
.adm-line-btn:hover{border-color:var(--accent); color:var(--accent);}
.adm-reply{display:grid; gap:var(--sp-2);}
.adm-reply textarea{width:100%; min-height:84px; padding:var(--sp-2) var(--sp-3); border:1px solid var(--line-strong); border-radius:var(--r-sm); background:var(--surface); font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-1); resize:vertical;}
.adm-reply textarea:focus{outline:none; border-color:var(--accent);}
.adm-err{display:grid; grid-template-columns:minmax(0, 1fr) auto; gap:2px var(--sp-3); padding:var(--sp-2) 0; border-bottom:1px solid var(--line);}
.adm-err code{font-family:ui-monospace, Consolas, monospace; font-size:var(--fs-xs); color:var(--text-1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.adm-err-count{font-size:var(--fs-sm); font-weight:600; color:var(--note-low); font-variant-numeric:tabular-nums;}
.adm-err-sub{grid-column:1 / -1; font-size:var(--fs-xs); color:var(--text-2);}
@media (max-width:600px){
  #adminPage .adm-tabs{order:3; width:100%; gap:var(--sp-2) var(--sp-4);}
  .adm-figs, .adm-figs-4{grid-template-columns:repeat(2, minmax(0, 1fr));}
  .adm-fig{padding:var(--sp-3);}
  .adm-fig b{font-size:var(--fs-lg);}
}

/* ==========================================================
   PROFIL PARTAGÉ, piste A du 15/10 (« Partager et inviter »).
   Page Profil : « Modifier le profil » et « Partager » en pilules, le
   panneau de partage. Page publique (#/u/:id) : chiffres sans couleur,
   invitation « Rejoindre Kinet ». Voir js/profilePage.js, js/publicProfile.js.
   ========================================================== */
.pp-actions{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.pp-edit{
  height:34px; padding:0 var(--sp-4);
  border:1px solid var(--line-strong); border-radius:var(--r-full);
  color:var(--text-1);
  transition:border-color var(--dur-fast), color var(--dur-fast);
}
.pp-edit:hover, .pp-edit[aria-expanded="true"]{border-color:var(--accent); color:var(--accent);}
/* Téléphone (audit général du 16/10, B1) : le nom et ses chiffres gardent la
   ligne à côté de la photo, « Modifier le profil » et « Partager » passent
   dessous. Avant, ils écrasaient le nom à une lettre par ligne. */
@media (max-width:600px){
  .pp-id{flex:1 1 calc(100% - 68px - var(--sp-4));}
  .pp-actions{flex:1 1 100%;}
}
.tmdb-result.is-rated .tmdb-result-year{color:var(--text-2);}
.tmdb-result.is-rated .tmdb-result-action{margin-left:auto; font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
.pp-share{max-width:560px; margin:var(--sp-4) 0 0; padding:var(--sp-4); border:1px solid var(--line-strong); border-radius:var(--r-md); display:grid; gap:var(--sp-2);}
.pp-share[hidden]{display:none;}
.pp-share-title{font-size:var(--fs-md); font-weight:600;}
.pp-share-text{margin:0; font-size:var(--fs-sm); color:var(--text-2);}
.pp-share-acts{display:flex; flex-wrap:wrap; gap:var(--sp-2); margin-top:var(--sp-1);}
.pp-share-note{margin:0; font-size:var(--fs-xs); color:var(--text-3);}
/* Page publique */
.pub-hero{display:flex; align-items:center; gap:var(--sp-4); margin:var(--sp-2) 0 var(--sp-6);}
.pub-hero img, .pub-hero .avatar-fallback{width:64px; height:64px; flex:none; border-radius:50%; object-fit:cover; border:1px solid var(--line-strong); background:var(--surface-2); display:flex; align-items:center; justify-content:center; color:var(--text-3);}
.pub-hero .avatar-fallback svg{width:28px; height:28px;}
.pub-hero h3{margin:0; font-family:var(--font-display); font-size:var(--fs-xl); font-weight:800; line-height:1.1;}
.pub-sub{margin-top:2px; font-size:var(--fs-sm); color:var(--text-2); font-variant-numeric:tabular-nums;}
.pub-favs{max-width:520px; margin-bottom:var(--sp-6);}
.pub-invite{position:sticky; bottom:var(--sp-3); z-index:5; margin-top:var(--sp-6); padding:var(--sp-4); border:1px solid var(--line-strong); border-radius:var(--r-md); background:rgba(var(--glass-rgb),0.94); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px); display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-3) var(--sp-4);}
.pub-invite p{flex:1; min-width:200px; margin:0; font-size:var(--fs-sm); color:var(--text-2);}
.pub-invite p b{display:block; font-size:var(--fs-md); color:var(--text-1);}
.pub-self-note{margin-top:var(--sp-5); font-size:var(--fs-xs); color:var(--text-3);}
.pub-gone{min-height:40vh; display:grid; place-content:center; justify-items:center; gap:var(--sp-2); text-align:center;}
.pub-gone h3{margin:0; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:800;}
.pub-gone p{margin:0 0 var(--sp-2); color:var(--text-2); font-size:var(--fs-sm);}
@media (max-width:600px){
  .pub-invite{bottom:calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));}
}

/* ==========================================================
   JOURNAL DANS LE CATALOGUE (draft Profil du 15/10, piste A).
   « Grille · Liste · Journal » à côté de la recherche des films (l'icône
   grille/liste de la pilule est masquée ici), et le Journal : tes
   visionnages par mois. Voir js/journal.js.
   ========================================================== */
#appContainer .toolbar-search .view-toggle{display:none;}
.films-views{display:flex; flex:none; gap:var(--sp-4);}
.fj-month{display:flex; align-items:baseline; gap:var(--sp-2); margin:var(--sp-6) 0 var(--sp-1); font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.fj-month:first-child{margin-top:0;}
.fj-month span{font-weight:400; letter-spacing:0; text-transform:none; color:var(--text-3);}
.film-journal{max-width:860px;}
.fj-row{display:flex; align-items:center; gap:var(--sp-3); min-height:72px; padding:var(--sp-1) 0; border-bottom:1px solid var(--line); cursor:pointer;}
.fj-row:hover .fj-title, .fj-row:focus-visible .fj-title{color:var(--accent);}
.fj-poster{flex:none; width:40px; height:60px; border-radius:var(--r-sm); object-fit:cover; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.fj-poster svg{width:18px; height:18px;}
.fj-main{flex:1; min-width:0;}
.fj-title{font-size:var(--fs-sm); font-weight:600; color:var(--text-1); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
.fj-again{margin-left:var(--sp-2); font-size:var(--fs-xs); font-weight:600; color:var(--accent);}
.fj-sub{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.fj-note{flex:none; width:36px; text-align:right; font-size:var(--fs-md); font-weight:600; font-variant-numeric:tabular-nums;}
.fj-note.rate-low{color:var(--note-low);} .fj-note.rate-mid{color:var(--note-mid);} .fj-note.rate-high{color:var(--note-high);}
.fj-date{flex:none; width:64px; text-align:right; font-size:var(--fs-xs); color:var(--text-2); font-variant-numeric:tabular-nums;}
@media (max-width:600px){
  /* Sur sa propre ligne, sous la recherche */
  #appContainer .toolbar{flex-wrap:wrap; row-gap:var(--sp-3);}
  #appContainer .toolbar-actions{width:100%;}
  .films-views{gap:var(--sp-5);}
  .fj-row{gap:var(--sp-2);}
  .fj-poster{width:36px; height:54px;}
  .fj-date{width:52px;}
}

/* ==========================================================
   CARTES : l'album sans hasard (draft Profil du 15/10, piste A).
   Un seul écran : le total, la règle, « Bientôt dans ta collection », puis
   la grille en trois onglets. Bord de la carte selon son rang (couleurs de
   rareté de Kinet). Voir js/album.js.
   ========================================================== */
.tcg-legacy[hidden]{display:none;}
#tcgOverlay .page-header{flex-wrap:wrap; row-gap:var(--sp-3);}
.al-album{max-width:900px;}
.al-sum{display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--sp-1) var(--sp-4);}
.al-sum b{font-family:var(--font-display); font-size:var(--fs-xl); font-weight:800; font-variant-numeric:tabular-nums;}
.al-sum span{font-size:var(--fs-sm); color:var(--text-2);}
/* .al-rule retirée le 28/10 : les règles de l'album sont derrière le « ? » du titre (#helpCartes). */
.al-status{min-height:1em; margin:var(--sp-2) 0 var(--sp-4); font-size:var(--fs-xs); color:var(--accent);}
.al-status:empty{margin-bottom:var(--sp-2);}
.al-next{border-top:1px solid var(--line); margin-bottom:var(--sp-6); max-width:560px;}
.al-next-row{display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-3) 0; border-bottom:1px solid var(--line);}
.al-next-row img, .al-next-empty{width:44px; height:44px; flex:none; border-radius:50%; object-fit:cover; background:var(--surface-2); color:var(--text-3); display:grid; place-items:center;}
.al-next-empty svg{width:20px; height:20px;}
.al-next-main{flex:1; min-width:0;}
.al-next-title{font-size:var(--fs-sm); font-weight:600;}
.al-next-sub{font-size:var(--fs-xs); color:var(--text-2);}
.al-prog{height:6px; max-width:240px; margin-top:6px; border-radius:var(--r-full); background:var(--line); overflow:hidden;}
.al-prog i{display:block; height:100%; border-radius:inherit; background:var(--accent);}
.al-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(118px, 1fr)); gap:var(--sp-5) var(--sp-3);}
.al-card{display:grid; gap:5px; min-width:0; padding:0; border:0; background:none; color:inherit; text-align:left; cursor:pointer; font-family:var(--font-text);}
.al-face{position:relative; display:grid; place-items:center; aspect-ratio:2/3; border-radius:var(--r-sm); overflow:hidden; border:2px solid var(--line-strong); background:var(--surface-2); color:var(--text-3);}
.al-face img{width:100%; height:100%; object-fit:cover; display:block;}
.al-face > svg{width:32px; height:32px;}
.al-r2 .al-face{border-color:var(--rarity-rare);}
.al-r3 .al-face{border-color:var(--accent);}
.al-r4 .al-face{border-color:var(--rarity-legendary); box-shadow:0 0 16px rgba(232,163,61,0.35);}
.al-card:hover .al-name, .al-card:focus-visible .al-name{color:var(--accent);}
.al-card:focus-visible{outline:none;}
.al-card:focus-visible .al-face{outline:2px solid var(--accent); outline-offset:2px;}
.al-name{font-size:var(--fs-sm); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
.al-rank{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.al-rank b{font-weight:600;}
.al-r2 .al-rank b{color:var(--rarity-rare);}
.al-r3 .al-rank b{color:var(--accent);}
.al-r4 .al-rank b{color:var(--rarity-legendary);}
.al-more{margin-top:var(--sp-5); background:none; border:0; padding:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
.al-detail{display:grid; justify-items:center; gap:var(--sp-2); text-align:center;}
.al-detail .al-face{width:160px;}
.al-detail-name{font-family:var(--font-display); font-size:var(--fs-lg); font-weight:800;}
.al-detail-films{margin:var(--sp-2) 0 0; font-size:var(--fs-sm); color:var(--text-2);}
#tcgCardCopyLink[hidden]{display:none;}
@media (max-width:600px){
  #tcgOverlay .al-tabs{order:3; width:100%;}
  .al-grid{grid-template-columns:repeat(3, minmax(0, 1fr)); gap:var(--sp-4) var(--sp-2);}
  .al-name{font-size:var(--fs-xs);}
}

/* Profil réorganisé (draft Profil du 15/10) : la rangée Admin en tête, sur
   téléphone et pour le compte admin seulement (sur PC, bouton de l'entête). */
.pp-admin-rows{display:none;}
@media (max-width:600px){
  .pp-admin-rows:has(> .pp-row:not([hidden])){display:block; margin-bottom:var(--sp-4);}
}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, CATALOGUE (piste A choisie le 16/10)
   Pastilles de filtre, filtres actifs, vraie vue Liste en rangées,
   titre sur deux lignes sous l'affiche, tri par critère expliqué.
   ========================================================== */
/* Panneau « Trier et filtrer » : pastilles */
.toolbar-filters-sheet{width:min(460px, calc(100vw - 32px)); gap:var(--sp-2);}
.toolbar-filters-head{margin-bottom:var(--sp-1);}
.filter-chips{display:grid; gap:var(--sp-2);}
.filter-label{
  font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase;
  color:var(--text-2); margin-top:var(--sp-2);
}
.filter-label:first-child{margin-top:0;}
.filter-row{display:flex; flex-wrap:wrap; gap:var(--sp-2);}
.filter-chip{
  height:32px; padding:0 12px;
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line-strong); border-radius:var(--r-full);
  background:none; color:var(--text-2);
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; white-space:nowrap;
  cursor:pointer; transition:color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.filter-chip:hover{color:var(--text-1);}
.filter-chip small{font-size:var(--fs-xs); font-weight:500; color:var(--text-3);}
.filter-chip.is-on{background:var(--accent-wash); border-color:var(--accent); color:var(--accent);}
.filter-chip.is-on small{color:var(--accent);}
.filter-chip span{font-size:15px; line-height:1;}
.filter-foot{display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3); margin-top:var(--sp-3);}
.filter-clear{
  background:none; border:0; padding:8px 0; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2);
}
.filter-clear:hover{color:var(--text-1);}
.filter-apply{display:none;}
@media (max-width:600px){
  .toolbar-filters-sheet{width:100%; max-width:none; max-height:86vh; overflow-y:auto;}
  .filter-apply{display:inline-flex; border-radius:var(--r-full); height:44px; padding:0 20px;}
}
/* Filtres actifs et explication sous la recherche */
.active-filters{display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-2); margin-top:var(--sp-3);}
.active-filters[hidden], .catalog-notice[hidden]{display:none;}
.catalog-hint{margin:var(--sp-3) 0 0; max-width:62ch; font-size:var(--fs-xs); color:var(--text-2);}
.catalog-strip{
  display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:var(--sp-3);
  margin-top:var(--sp-4); padding:var(--sp-3) 0;
  border-top:1px solid var(--line); border-bottom:1px solid var(--line);
  font-size:var(--fs-sm); color:var(--text-2);
}
.catalog-strip b{color:var(--text-1); font-weight:600;}
.catalog-strip-btn{background:none; border:0; padding:6px 0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
/* Séparateur du tri par critère */
.film-divider{
  grid-column:1/-1; padding-top:var(--sp-3); border-top:1px solid var(--line);
  font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);
}
body[data-view-mode="list"] #filmList .film-divider{padding:var(--sp-5) 0 var(--sp-2); border-top:0;}
/* Affiche et pictos de happening sur l'affiche, plus dans le titre (C7) */
.film-poster-box{position:relative; flex:none;}
#filmList .film-poster-box .happening-badge{
  position:absolute; left:6px; bottom:10px; z-index:1; margin:0;
  width:24px; height:24px; border-radius:var(--r-full);
  display:flex; align-items:center; justify-content:center;
  background:rgba(var(--glass-rgb),0.78); font-size:13px;
}
/* Grille : titre sur deux lignes au plus, puis l'année */
body[data-view-mode="grid"] #filmList .film-title{
  white-space:normal; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
body[data-view-mode="grid"] #filmList :is(.film-sub-list, .row-rate-link){display:none;}
body[data-view-mode="grid"] #filmList .film-row:hover .film-title{color:var(--accent);}
/* Téléphone : l'étoile seulement sur les favoris (C4) */
@media (hover:none){
  body[data-view-mode="grid"] #filmList .star-btn:not(.active){display:none;}
}
/* Liste : vraies rangées de 64 px, sans boîte, note à droite (C1) */
body[data-view-mode="list"] #filmList{display:flex; flex-direction:column; gap:0;}
body[data-view-mode="list"] #filmList .film-row{
  display:flex; flex-direction:row; align-items:center; gap:var(--sp-3);
  min-height:64px; padding:2px 0; margin:0;
  background:none; border:0; border-bottom:1px solid var(--line); border-radius:0;
  box-shadow:none; transform:none;
}
body[data-view-mode="list"] #filmList .film-row:hover{background:none; border-color:var(--line); box-shadow:none; transform:none;}
body[data-view-mode="list"] #filmList .film-row:hover .film-title{color:var(--accent);}
body[data-view-mode="list"] #filmList :is(.film-poster, .film-poster-placeholder){width:40px; height:60px; margin:0; border:0; border-radius:var(--r-sm);}
body[data-view-mode="list"] #filmList .film-poster-box .happening-badge{left:-4px; bottom:-4px; width:20px; height:20px; font-size:11px;}
body[data-view-mode="list"] #filmList .film-main{flex:1; min-width:0; display:grid; gap:2px;}
body[data-view-mode="list"] #filmList .film-title{font-size:var(--fs-sm); font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:color var(--dur-fast);}
body[data-view-mode="list"] #filmList .film-sub-grid{display:none;}
body[data-view-mode="list"] #filmList .film-sub-list{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin:0;}
body[data-view-mode="list"] #filmList .star-btn{width:28px; height:28px; padding:0; background:none; border:0; color:var(--accent); flex:none;}
body[data-view-mode="list"] #filmList .star-btn:not(.active){display:none;}
body[data-view-mode="list"] #filmList .counter{
  width:auto; height:auto; min-width:32px; padding:0; flex:none;
  border:0; border-radius:0; background:none; box-shadow:none;
  font-size:var(--fs-sm); font-weight:600; font-variant-numeric:tabular-nums; text-align:right;
}
body[data-view-mode="list"] #filmList .counter:not(.rate-low):not(.rate-mid):not(.rate-high){display:none;}
.row-rate-link{
  flex:none; background:none; border:0; padding:8px 0; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);
}
body[data-view-mode="list"] #filmList .bulk-select-checkbox{flex:none;}
#filmList .film-poster-box > .film-poster{display:block;}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, FICHE FILM (piste A choisie le 16/10)
   Bandeau fondu, affiche posée dessus, actions sur une ligne,
   « Ton avis », où regarder selon tes abonnements.
   ========================================================== */
.detail-topbar{display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3); margin-bottom:var(--sp-3);}
.back-link{background:none; border:0; padding:6px 0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); color:var(--text-2);}
.back-link:hover{color:var(--text-1);}
.film-page .detail-backdrop.is-fade{height:280px; border:0; margin-bottom:0;}
.film-page .detail-backdrop.is-fade::after{background:linear-gradient(to bottom, rgba(var(--glass-rgb),0) 45%, var(--ink-surface) 100%);}
.ff-top{display:grid; grid-template-columns:180px minmax(0,1fr) auto; gap:var(--sp-5); align-items:end; margin-top:-110px; position:relative; z-index:2;}
.ff-poster{width:180px; aspect-ratio:2/3; object-fit:cover; border-radius:var(--r-sm); box-shadow:var(--shadow-float); display:block;}
.ff-poster[hidden]{display:none;}
.ff-top:has(.ff-poster[hidden]){grid-template-columns:minmax(0,1fr) auto; margin-top:var(--sp-4);}
.ff-id{min-width:0;}
.ff-top #filmDetailRatingRing{grid-area:auto; align-self:end; margin-bottom:6px;}
.film-page .detail-title{font-size:var(--fs-2xl); line-height:1.02; letter-spacing:-0.02em;}
.film-page .detail-meta{text-transform:none; letter-spacing:0; color:var(--text-2); font-size:var(--fs-sm); margin-top:var(--sp-2);}
.ff-under{display:grid; grid-template-columns:180px minmax(0,1fr); gap:var(--sp-5);}
.ff-under > .ff-col{grid-column:2; min-width:0;}
.ff-actions{display:flex; align-items:center; flex-wrap:wrap; gap:6px 8px; margin-top:var(--sp-5);}
.ff-btn-main{
  height:40px; padding:0 20px; border:0; border-radius:var(--r-full);
  background:var(--accent); color:var(--accent-ink);
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; cursor:pointer;
  transition:transform var(--dur-press) var(--ease-out);
}
.ff-btn-main:active{transform:scale(0.97);}
.ff-btn-line{
  height:36px; padding:0 14px; display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; color:var(--text-1);
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; cursor:pointer;
}
.ff-btn-line:hover, .ff-btn-line.is-on{border-color:var(--accent); color:var(--accent);}
.ff-btn-text{
  height:40px; padding:0 8px; display:inline-flex; align-items:center; gap:6px;
  background:none; border:0; color:var(--text-2); text-decoration:none;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; cursor:pointer;
}
.ff-btn-text:hover{color:var(--text-1);}
.ff-btn-text.active{color:var(--accent);}
.ff-btn-icon{width:40px; height:40px; border-radius:50%; display:grid; place-items:center; background:none; border:0; color:var(--text-2); cursor:pointer;}
.ff-btn-icon:hover{background:var(--surface-2); color:var(--text-1);}
.ff-actions .ui-icon{width:18px; height:18px;}
.ff-more{position:relative;}
.ff-menu{
  position:absolute; top:calc(100% + 6px); left:0; z-index:30; min-width:250px; padding:6px;
  display:grid; background:rgba(var(--glass-rgb),0.92);
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  border:1px solid var(--line); border-radius:var(--r-md); box-shadow:var(--shadow-float);
}
.ff-menu[hidden]{display:none;}
.ff-menu button{
  display:flex; align-items:center; gap:10px; padding:10px 12px; border:0; border-radius:var(--r-sm);
  background:none; color:var(--text-1); text-align:left; cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:500;
}
.ff-menu button[hidden]{display:none;}
.ff-menu button:hover{background:var(--surface-2);}
.ff-menu button small{margin-left:auto; font-size:var(--fs-xs); color:var(--text-2);}
.ff-menu #filmDetailLikeBtn.active{border:0; color:var(--accent);}
.ff-menu .ui-icon{width:16px; height:16px;}
.film-page .synopsis{margin:var(--sp-5) 0 0;}
.film-page .fiche{margin-top:var(--sp-5);}
.ff-sec-head{display:flex; align-items:baseline; gap:10px;}
.ff-sec-head .ff-link{margin-left:auto;}
.ff-link{background:none; border:0; padding:4px 0; cursor:pointer; color:var(--accent); font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600;}
.ff-avis .tmdb-empty{margin-bottom:var(--sp-3);}
.ff-avis-grid{display:grid; grid-template-columns:auto minmax(0,1fr); gap:20px 28px; align-items:start;}
.ff-avis-grid.no-crit{grid-template-columns:1fr;}
.ff-avis-note{display:flex; align-items:center; gap:12px;}
.ff-avis-note .counter{width:72px; height:72px; font-size:20px;}
.ff-avis-note b{display:block; font-size:var(--fs-sm); font-weight:600;}
.ff-avis-note small{display:block; font-size:var(--fs-xs); color:var(--text-2);}
.ff-crits{display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px 28px;}
.ff-crit{display:grid; grid-template-columns:96px minmax(0,1fr) 36px; gap:10px; align-items:center; font-size:var(--fs-xs); color:var(--text-2);}
.ff-crit i{display:block; height:4px; border-radius:var(--r-full); background:var(--surface-2); overflow:hidden;}
.ff-crit i b{display:block; height:100%; background:var(--accent);}
.ff-crit .num{color:var(--text-1); font-weight:600; text-align:right;}
.ff-quote{margin:var(--sp-5) 0 0; padding-left:14px; border-left:2px solid var(--line-strong); max-width:62ch; font-size:var(--fs-md); line-height:1.55; color:var(--text-1); white-space:pre-line;}
.ff-seen{display:flex; flex-wrap:wrap; align-items:center; gap:6px 14px; margin-top:var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);}
.ff-history{margin-top:var(--sp-2);}
.ff-provs{display:grid; gap:var(--sp-3);}
.ff-prov{display:flex; align-items:center; flex-wrap:wrap; gap:10px; font-size:var(--fs-sm); color:var(--text-2);}
.ff-prov img{width:32px; height:32px; border-radius:var(--r-sm);}
.ff-prov b{color:var(--text-1); font-weight:600;}
.ff-rent-toggle{justify-self:start; background:none; border:0; padding:4px 0; cursor:pointer; color:var(--text-2); font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600;}
.ff-rent-toggle:hover{color:var(--text-1);}
.ff-rent-list{margin:0; max-width:62ch; font-size:var(--fs-sm); color:var(--text-2);}
/* Commentaires : datés, réactions sans compteur au survol ou après « Réagir » */
.comment-when{margin-left:8px; font-size:var(--fs-xs); font-weight:400; color:var(--text-2);}
.comment-item:not(:hover):not(.show-reactions):not(:focus-within) .comment-reactions .reaction-btn:not(.active):not(:has(.reaction-count)){display:none;}
.comment-react-toggle{background:none; border:0; padding:4px 0; cursor:pointer; color:var(--text-2); font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600;}
@media (hover:hover){ .comment-react-toggle{display:none;} }
@media (max-width:600px){
  .film-page .detail-backdrop.is-fade{height:190px;}
  .ff-top{grid-template-columns:100px minmax(0,1fr); gap:var(--sp-4); margin-top:-56px;}
  .ff-poster{width:100px;}
  .ff-top #filmDetailRatingRing{display:none;}
  .ff-under{display:block;}
  .film-page .detail-title{font-size:var(--fs-xl);}
  .ff-label{display:none;}
  .ff-btn-text{width:40px; padding:0; justify-content:center;}
  .ff-btn-main{height:44px;}
  .ff-avis-grid, .ff-crits{grid-template-columns:1fr;}
}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, NOTATION EN PAS À PAS (piste B, 16/10)
   Un critère à la fois, puis un résumé. Feuille plein écran au téléphone,
   pied toujours visible.
   ========================================================== */
.modal.film-modal.nt-modal{max-width:620px; padding:24px 28px 0; display:flex; flex-direction:column; max-height:calc(100vh - 80px);}
.nt-head{display:flex; align-items:center; gap:14px; flex:none;}
.nt-thumb{width:44px; height:66px; border-radius:var(--r-sm); object-fit:cover; flex:none;}
.nt-thumb.film-poster-placeholder{display:flex; align-items:center; justify-content:center;}
.nt-ht{flex:1; min-width:0;}
.nt-ht b{display:block; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; letter-spacing:-0.01em; line-height:1.15; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.nt-ht span{font-size:var(--fs-xs); color:var(--text-2);}
.nt-scroll{flex:1; min-height:0; overflow-y:auto; margin:0 -28px; padding:0 28px 8px;}
.nt-pick{padding-top:var(--sp-5);}
.nt-pick[hidden], .nt-body[hidden], .nt-extras[hidden]{display:none;}
.nt-steps{display:flex; gap:4px; margin:var(--sp-5) 0 var(--sp-2);}
.nt-steps i{flex:1; height:4px; border-radius:var(--r-full); background:var(--surface-2);}
.nt-steps i.on{background:var(--accent);}
.nt-step{display:grid; gap:10px; padding:var(--sp-4) 0 var(--sp-2);}
.nt-step-count{font-size:var(--fs-xs); color:var(--text-2);}
.nt-step-label{font-family:var(--font-display); font-size:var(--fs-xl); font-weight:800; letter-spacing:-0.02em; margin:0;}
.nt-step-def{margin:0; font-size:var(--fs-sm); color:var(--text-2); max-width:56ch;}
.nt-step-val, .nt-direct-val{font-family:var(--font-display); font-size:var(--fs-2xl); font-weight:800; letter-spacing:-0.02em; line-height:1; color:var(--text-3);}
.nt-step-val.rate-high, .nt-direct-val.rate-high{color:var(--note-high);}
.nt-step-val.rate-mid, .nt-direct-val.rate-mid{color:var(--note-mid);}
.nt-step-val.rate-low, .nt-direct-val.rate-low{color:var(--note-low);}
.nt-range{-webkit-appearance:none; appearance:none; width:100%; height:4px; margin:10px 0; border-radius:var(--r-full); background:var(--surface-2); outline:none;}
.nt-range::-webkit-slider-thumb{-webkit-appearance:none; width:26px; height:26px; border-radius:50%; background:var(--accent); border:0; cursor:pointer;}
.nt-range::-moz-range-thumb{width:26px; height:26px; border-radius:50%; background:var(--accent); border:0; cursor:pointer;}
.nt-range.is-unset::-webkit-slider-thumb{background:var(--text-3);}
.nt-range.is-unset::-moz-range-thumb{background:var(--text-3);}
.nt-range:focus-visible{outline:2px solid var(--accent); outline-offset:6px;}
.nt-anchors{display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; font-size:var(--fs-xs); color:var(--text-2);}
.nt-anchors div{display:grid; gap:2px;}
.nt-anchors div:nth-child(2){text-align:center;}
.nt-anchors div:nth-child(3){text-align:right;}
.nt-anchors b{color:var(--text-1); font-weight:600; font-variant-numeric:tabular-nums;}
.nt-score{display:flex; align-items:center; gap:14px; margin-top:var(--sp-5);}
.nt-score .nt-ring{width:72px; height:72px; font-size:20px;}
.nt-score b{display:block; font-size:var(--fs-md); font-weight:600;}
.nt-score small{display:block; font-size:var(--fs-xs); color:var(--text-2);}
.nt-sum{margin-top:var(--sp-4); border-top:1px solid var(--line);}
.nt-sumrow{
  width:100%; display:grid; grid-template-columns:120px minmax(0,1fr) 40px; gap:10px; align-items:center;
  min-height:44px; padding:0; background:none; border:0; border-bottom:1px solid var(--line);
  color:var(--text-1); font-family:var(--font-text); font-size:var(--fs-sm); text-align:left; cursor:pointer;
}
.nt-sumrow:hover span:first-child{color:var(--accent);}
.nt-sumrow i{display:block; height:4px; border-radius:var(--r-full); background:var(--surface-2); overflow:hidden;}
.nt-sumrow i b{display:block; height:100%; background:var(--accent);}
.nt-sumrow .num{text-align:right; font-weight:600;}
.nt-hint{margin:var(--sp-3) 0 0; font-size:var(--fs-xs); color:var(--text-2);}
.nt-direct-title{margin:var(--sp-5) 0 0; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700;}
.nt-direct{display:grid; gap:8px; padding:var(--sp-3) 0;}
.nt-scale{display:flex; justify-content:space-between; font-size:11px; color:var(--text-3); font-variant-numeric:tabular-nums;}
.nt-extras{margin-top:var(--sp-5);}
.nt-field{display:grid; gap:8px; margin-top:var(--sp-5);}
.nt-field-label{font-size:var(--fs-sm); font-weight:600; color:var(--text-1); text-transform:none; letter-spacing:0;}
.nt-extras textarea{width:100%; min-height:84px; border-radius:var(--r-md);}
.nt-switch{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:var(--sp-5); padding:12px 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); font-size:var(--fs-sm); cursor:pointer;}
.nt-switch small{display:block; font-size:var(--fs-xs); color:var(--text-2);}
.nt-switch-input{-webkit-appearance:none; appearance:none; width:40px; height:24px; flex:none; margin:0; position:relative; border-radius:var(--r-full); background:var(--surface-2); border:1px solid var(--line-strong); cursor:pointer; transition:background var(--dur-fast);}
.nt-switch-input::after{content:""; position:absolute; top:2px; left:2px; width:18px; height:18px; border-radius:50%; background:var(--text-2); transition:transform var(--dur-fast);}
.nt-switch-input:checked{background:var(--accent); border-color:var(--accent);}
.nt-switch-input:checked::after{transform:translateX(16px); background:var(--accent-ink);}
.nt-seen{display:flex; flex-wrap:wrap; align-items:center; gap:6px 14px; font-size:var(--fs-sm); color:var(--text-2);}
.nt-link-quiet{color:var(--text-2);}
.nt-link-quiet:hover{color:var(--text-1);}
.nt-seen-date{max-width:200px;}
.nt-seen-date[hidden], #viewingsList[hidden], .viewings-add-row[hidden]{display:none;}
.nt-extras #viewingsSection{margin:var(--sp-2) 0 0;}
.nt-foot{display:flex; align-items:center; justify-content:space-between; gap:12px; flex:none; padding:14px 0 18px; border-top:1px solid var(--line);}
.nt-foot-r{display:flex; align-items:center; gap:6px 12px; flex-wrap:wrap; justify-content:flex-end;}
.nt-foot-r [hidden]{display:none;}
.nt-more{position:relative;}
.nt-menu{top:auto; bottom:calc(100% + 6px);}
.nt-menu .nt-danger{color:var(--note-low);}
@media (max-width:600px){
  #overlay:has(.nt-modal){padding:44px 0 0; align-items:stretch;}
  .modal.film-modal.nt-modal{max-width:none; max-height:none; height:100%; border-radius:var(--r-md) var(--r-md) 0 0; padding:16px 16px 0;}
  .nt-scroll{margin:0 -16px; padding:0 16px 8px;}
  .nt-foot{padding-bottom:calc(16px + env(safe-area-inset-bottom, 0px));}
  .nt-foot .ff-btn-main{height:44px;}
}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, FICHE SÉRIE (piste D, A avec les carrés de B, 16/10)
   Même en-tête que la fiche film, « À suivre » avec « Vu », progression sur
   les épisodes sortis, saisons en carrés ou en rangées, menus ⋯ par saison.
   ========================================================== */
.sx-ring-wrap{position:relative; align-self:end; margin-bottom:6px;}
.sx-ring-wrap .counter{cursor:pointer;}
.sx-note-pop{top:calc(100% + 8px); left:auto; right:0; width:min(290px, calc(100vw - 32px)); padding:16px; gap:10px; z-index:40;}
.sx-note-title{font-family:var(--font-display); font-size:var(--fs-md); font-weight:700;}
.sx-note-row{display:flex; align-items:center; gap:14px;}
.sx-note-row .num{min-width:72px; text-align:right; font-size:var(--fs-md); font-weight:600;}
.sx-note-pop .nt-range{margin:6px 0;}
.sx-where{display:flex; align-items:center; flex-wrap:wrap; gap:8px 16px; margin-top:var(--sp-4);}
.sx-where #seriesDetailWatchProviders{min-width:0;}
.sx-next{display:flex; align-items:center; gap:16px; margin-top:var(--sp-5); padding:var(--sp-4) 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line);}
.sx-next[hidden]{display:none;}
.sx-nx{flex:1; min-width:0; display:grid; gap:2px;}
.sx-nx small{font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.sx-nx b{font-size:var(--fs-md); font-weight:600;}
.sx-nx span{font-size:var(--fs-xs); color:var(--text-2);}
.sx-next .ff-btn-main{display:inline-flex; align-items:center; gap:6px; flex:none;}
.sx-next .ui-icon{width:16px; height:16px;}
.sx-prog{display:grid; gap:8px; margin-top:var(--sp-4); font-size:var(--fs-xs); color:var(--text-2);}
.sx-prog:empty{display:none;}
.sx-prog i{display:block; height:4px; border-radius:var(--r-full); background:var(--surface-2); overflow:hidden;}
.sx-prog i b{display:block; height:100%; background:var(--accent);}
.sx-sec-head{display:flex; align-items:baseline; gap:12px;}
.sx-views{display:flex; gap:18px; margin-left:auto;}
.sx-undo{margin:0 0 var(--sp-2);}
.series-page .sx-season{border:0; border-bottom:1px solid var(--line); border-radius:0; background:none; padding:0; margin:0;}
.sx-season-head{display:flex; align-items:center; gap:4px; min-height:56px; position:relative;}
.sx-season-head .season-header{flex:1; min-width:0; display:flex; align-items:center; gap:12px; padding:0; background:none; border:0; color:var(--text-1); text-align:left; cursor:pointer; font-family:var(--font-text);}
.sx-season-head .season-header[tabindex="-1"]{cursor:default;}
.sx-season-head .season-name{font-size:var(--fs-md); font-weight:600; min-width:84px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.sx-bar{flex:1; max-width:240px; height:4px; border-radius:var(--r-full); background:var(--surface-2); overflow:hidden;}
.sx-bar b{display:block; height:100%; background:var(--accent);}
.sx-season-head .season-progress{margin-left:auto; font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap;}
.sx-avg{min-width:26px; text-align:right; font-size:var(--fs-xs); font-weight:600;}
.sx-avg:empty{min-width:0;}
.sx-season-more{position:relative;}
.sx-season-menu{left:auto; right:0; top:calc(100% + 4px); min-width:270px; z-index:35;}
.sx-season-menu button[disabled]{color:var(--text-3); cursor:default;}
.series-page .season-caret{width:8px; height:8px; margin:0 10px 4px 6px; border-right:1.5px solid var(--text-2); border-bottom:1.5px solid var(--text-2); transform:rotate(45deg); transition:transform var(--dur-fast);}
.series-page .sx-season.open .season-caret{transform:rotate(-135deg); margin-bottom:-2px;}
/* La flèche est un vrai bouton (elle ne réagissait pas au toucher). */
.sx-caret-btn{flex:none; width:40px; height:40px; display:grid; place-items:center; padding:0; margin:0; border:0; border-radius:var(--r-full); background:none; cursor:pointer;}
.series-page .sx-caret-btn .season-caret{margin:0 0 4px;}
.series-page .sx-season.open .sx-caret-btn .season-caret{margin:4px 0 0;}
.sx-caret-btn:hover .season-caret{border-color:var(--text-1);}
.sx-caret-btn:focus-visible{outline:2px solid var(--accent); outline-offset:-2px;}
.series-page .season-episodes{display:none; padding:0 0 var(--sp-3);}
.series-page .sx-season.open .season-episodes{display:block;}
/* Grille d'épisodes */
.sx-sq{display:grid; grid-template-columns:repeat(auto-fill, minmax(38px, 1fr)); gap:6px; padding:4px 0 var(--sp-4);}
.sx-sq-btn{
  aspect-ratio:1; padding:0; border:1.5px solid var(--line-strong); border-radius:var(--r-sm);
  background:none; color:var(--text-2); cursor:pointer;
  font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; font-variant-numeric:tabular-nums;
  transition:background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-press) var(--ease-out);
}
.sx-sq-btn:hover{border-color:var(--accent); color:var(--text-1);}
.sx-sq-btn:active{transform:scale(0.94);}
.sx-sq-btn.on{background:var(--accent); border-color:var(--accent); color:var(--accent-ink);}
.sx-sq-btn.future{border-style:dashed; color:var(--text-3);}
.sx-sq-btn.sel{outline:2px solid var(--text-1); outline-offset:2px;}
.sx-sel{display:flex; flex-wrap:wrap; align-items:center; gap:6px 16px; padding:0 0 var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);}
.sx-sel b{color:var(--text-1); font-weight:600;}
.sx-extra{display:flex; flex-wrap:wrap; align-items:center; gap:8px 18px; font-size:var(--fs-xs); color:var(--text-2);}
.sx-step{display:inline-flex; align-items:center; gap:6px;}
.sx-step button{width:28px; height:28px; padding:0; border-radius:50%; border:1px solid var(--line-strong); background:none; color:var(--text-1); font-size:var(--fs-sm); line-height:1; cursor:pointer;}
.sx-step button:hover{border-color:var(--accent); color:var(--accent);}
.sx-step b{min-width:42px; text-align:center; color:var(--text-1); font-weight:600;}
.sx-legend{display:flex; flex-wrap:wrap; gap:16px; margin-top:var(--sp-3); font-size:var(--fs-xs); color:var(--text-2);}
.sx-legend[hidden]{display:none;}
.sx-legend span{display:inline-flex; align-items:center; gap:6px;}
.sx-legend i{width:14px; height:14px; border-radius:var(--r-sm); border:1.5px solid var(--line-strong); display:inline-block;}
.sx-legend i.on{background:var(--accent); border-color:var(--accent);}
.sx-legend i.future{border-style:dashed;}
/* Liste d'épisodes */
.series-page .sx-ep{display:grid; grid-template-columns:28px minmax(0,1fr) auto 28px; gap:10px; align-items:center; min-height:48px; padding:0; margin:0; border:0; border-top:1px solid var(--line); border-radius:0; background:none; font-size:var(--fs-sm);}
.sx-ep .episode-num{color:var(--text-2);}
.sx-ep .episode-title{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.sx-ep [data-ep-sel]{cursor:pointer;}
.sx-ep [data-ep-sel]:hover{color:var(--accent);}
.sx-ep .episode-air{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap;}
.sx-ep.future :is(.episode-title, .episode-num, .episode-air){color:var(--text-3);}
.sx-ep .sx-check{width:20px; height:20px; margin:0; accent-color:var(--accent); cursor:pointer;}
.sx-ep .sx-extra{grid-column:2/-1; padding:2px 0 var(--sp-3);}
.sx-end{margin-top:var(--sp-6);}
@media (max-width:600px){
  .series-page .ff-top{grid-template-columns:100px minmax(0,1fr) auto;}
  .sx-ep .episode-air{display:none;}
  .sx-ep.future .episode-air{display:block;}
  .sx-bar{max-width:90px;}
  .sx-next{flex-wrap:wrap;}
}
/* Page Séries (S3) : « À rattraper » en neutre, « Nouvel épisode » garde l'accent. */
.new-episode-badge.is-catchup{background:none; color:var(--text-2); border:1px solid var(--line-strong);}
.sx-undo[hidden]{display:none;}
.series-page .sx-prog + .stats-section, .series-page .sx-next + .sx-prog:empty + .stats-section{margin-top:var(--sp-6);}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, GROUPES ET PROPOSITIONS (piste A, 16/10)
   Rangées sans boîte, prochaine séance en tête, « Partant », menus ⋯.
   ========================================================== */
.gp-list-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; flex-wrap:wrap;}
.gp-create{display:flex; flex-wrap:wrap; gap:10px; margin-top:var(--sp-4);}
.gp-create[hidden]{display:none;}
.gp-create input{flex:1; min-width:200px; height:40px; border-radius:var(--r-full); padding:0 16px;}
.gp-list{margin-top:var(--sp-4);}
.gp-row{display:flex; align-items:center; gap:14px; min-height:72px; padding:10px 0; border-bottom:1px solid var(--line); cursor:pointer;}
.gp-row-main{flex:1; min-width:0; display:grid; gap:3px;}
.gp-row-main b{font-size:var(--fs-md); font-weight:600; transition:color var(--dur-fast);}
.gp-row-main span{font-size:var(--fs-xs); color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.gp-row:hover .gp-row-main b{color:var(--accent);}
.gp-chev{color:var(--text-2); font-size:20px; line-height:1;}
.gp-avs{display:flex; flex:none; align-items:center;}
.gp-avs > .friend-avatar, .gp-av-more{width:28px; height:28px; margin-left:-8px; border:2px solid var(--ink-surface); border-radius:50%; flex:none;}
.gp-avs > :first-child{margin-left:0;}
.gp-avs .friend-avatar-placeholder{display:grid; place-items:center; background:var(--surface-2); color:var(--text-2);}
.gp-avs .friend-avatar-placeholder .ui-icon{width:14px; height:14px;}
.gp-av-more{display:grid; place-items:center; background:var(--surface-2); color:var(--text-2); font-size:var(--fs-xs); font-weight:600;}
.gp-head{display:flex; align-items:flex-end; justify-content:space-between; flex-wrap:wrap; gap:12px 20px;}
.gp-head h1{font-family:var(--font-display); font-size:var(--fs-xl); font-weight:800; letter-spacing:-0.02em; margin:0;}
.gp-meta{display:flex; align-items:center; gap:10px; margin-top:var(--sp-2); font-size:var(--fs-xs); color:var(--text-2);}
.gp-actions{display:flex; align-items:center; gap:4px; position:relative;}
.gp-menu{left:auto; right:0;}
.gp-desc{margin:var(--sp-3) 0 0; color:var(--text-2); font-size:var(--fs-sm); max-width:65ch;}
.gp-manage{margin-top:var(--sp-5); padding:var(--sp-4) 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line);}
.gp-manage[hidden]{display:none;}
.gp-manage-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--sp-2);}
.gp-manage-head b{font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700;}
.gp-manage .wl-list{display:block;}
.gp-manage .wl-row{display:flex; align-items:center; gap:12px; min-height:56px; padding:6px 0; margin:0; background:none; border:0; border-bottom:1px solid var(--line); border-radius:0; box-shadow:none;}
.gp-manage .wl-row:last-child{border-bottom:0;}
.gp-manage .wl-main{flex:1; min-width:0;}
/* Prochaine séance */
.gp-next-wrap{margin-top:var(--sp-5);}
.gp-next{display:grid; grid-template-columns:96px minmax(0,1fr); gap:18px; align-items:center; padding:var(--sp-4) 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line);}
.gp-next > img, .gp-next > .film-poster-placeholder{width:96px; aspect-ratio:2/3; border-radius:var(--r-sm); object-fit:cover;}
.gp-next-main{min-width:0;}
.gp-next small, .gp-prop-head small{font-size:var(--fs-xs); font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-2);}
.gp-nt{font-family:var(--font-display); font-size:var(--fs-lg); font-weight:800; letter-spacing:-0.02em; line-height:1.15; margin:4px 0;}
.gp-nd{font-size:var(--fs-sm); color:var(--text-2);}
.gp-next .ff-actions{margin-top:var(--sp-3);}
.gp-syn{margin:var(--sp-3) 0 0; font-size:var(--fs-sm); color:var(--text-2); max-width:62ch; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden;}
.gp-empty{display:grid; gap:4px; padding:var(--sp-4) 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); font-size:var(--fs-sm); color:var(--text-2);}
.gp-empty b{display:block; color:var(--text-1); font-size:var(--fs-md); font-weight:600;}
.gp-empty-row{display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;}
.gp-when{font-size:var(--fs-sm); color:var(--text-2);}
.gp-dates{margin-top:var(--sp-3); align-items:center;}
.gp-date-input{height:32px; border-radius:var(--r-full); padding:0 12px;}
/* Propositions */
.gp-sec-head{display:flex; align-items:baseline; flex-wrap:wrap; gap:6px 10px;}
.gp-sec-head small{font-size:var(--fs-xs); color:var(--text-2);}
.gp-sec-head .ff-link{margin-left:auto;}
.gp-propose{margin-top:var(--sp-3);}
.gp-propose[hidden]{display:none;}
.gp-propose .tmdb-result{cursor:pointer;}
.gp-propose .tmdb-result-action{margin-left:auto; font-size:var(--fs-xs); font-weight:600; color:var(--accent);}
.gp-props{margin-top:var(--sp-2);}
.gp-prop{display:flex; align-items:center; gap:14px; min-height:84px; padding:10px 0; border-bottom:1px solid var(--line);}
.gp-prop-poster{flex:none; padding:0; border:0; background:none; cursor:pointer;}
.gp-prop-poster img, .gp-prop-poster .film-poster-placeholder{width:48px; height:72px; border-radius:var(--r-sm); object-fit:cover; display:block;}
.gp-prop-main{flex:1; min-width:0; display:grid; gap:4px; justify-items:start;}
.gp-prop-title{padding:0; border:0; background:none; cursor:pointer; text-align:left; color:var(--text-1); font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600;}
.gp-prop-title:hover{color:var(--accent);}
.gp-prop-title span{color:var(--text-2); font-weight:500;}
.gp-prop-sub{font-size:var(--fs-xs); color:var(--text-2);}
.gp-voters .gp-avs > .friend-avatar, .gp-voters .gp-av-more{width:22px; height:22px;}
.gp-vote{
  flex:none; height:36px; padding:0 14px; display:inline-flex; align-items:center; gap:8px;
  border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; color:var(--text-1);
  font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; cursor:pointer;
  transition:border-color var(--dur-fast), background var(--dur-fast), transform var(--dur-press) var(--ease-out);
}
.gp-vote:active{transform:scale(0.97);}
.gp-vote b{font-weight:600; color:var(--text-2); font-variant-numeric:tabular-nums;}
.gp-vote .ui-icon{width:15px; height:15px;}
.gp-vote.is-on{background:var(--accent-wash); border-color:var(--accent); color:var(--accent);}
.gp-vote.is-on b{color:var(--accent);}
/* Goûts et déjà vus */
.gp-taste{display:flex; align-items:center; gap:14px; min-height:64px; padding:6px 0; border-bottom:1px solid var(--line); cursor:pointer;}
.gp-taste > img, .gp-taste > .film-poster-placeholder{width:40px; height:60px; border-radius:var(--r-sm); object-fit:cover; flex:none;}
.gp-taste-main{flex:1; min-width:0; display:grid; gap:4px;}
.gp-taste-main b{font-size:var(--fs-sm); font-weight:600;}
.gp-notes{display:flex; flex-wrap:wrap; gap:4px 12px; font-size:var(--fs-xs); color:var(--text-2);}
.gp-notes .taste-note-pill{background:none; border:0; padding:0; font-size:var(--fs-xs); font-weight:600;}
.gp-avg{min-width:32px; text-align:right; font-size:var(--fs-sm); font-weight:600;}
/* Une proposition */
.gp-prop-head{grid-template-columns:140px minmax(0,1fr); border-top:0;}
.gp-prop-head > img, .gp-prop-head > .film-poster-placeholder{width:140px;}
.gp-voters-list{display:flex; flex-wrap:wrap; gap:12px 18px;}
.gp-voter{display:inline-flex; align-items:center; gap:8px; font-size:var(--fs-sm);}
.gp-voter .friend-avatar{width:28px; height:28px;}
#proposalDetailFooter:empty{display:none;}
@media (max-width:600px){
  .gp-next{grid-template-columns:72px minmax(0,1fr); gap:14px;}
  .gp-next > img, .gp-next > .film-poster-placeholder{width:72px;}
  .gp-prop-head{grid-template-columns:96px minmax(0,1fr);}
  .gp-prop-head > img, .gp-prop-head > .film-poster-placeholder{width:96px;}
  .gp-prop{gap:10px;}
  .gp-vote{padding:0 12px;}
}

/* ==========================================================
   AUDIT DES SIX ÉCRANS, LISTES (piste A, 16/10)
   Même couverture de 4 affiches pour tes listes et celles de tes amis,
   une liste en grille d'affiches, « Modifier », « Ajouter à une liste ».
   ========================================================== */
.ls-page .stats-section{margin-top:var(--sp-6);}
.ls-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(180px, 1fr)); gap:24px 18px; margin-top:var(--sp-4);}
.ls-card{display:grid; gap:8px; align-content:start; min-width:0; padding:0; margin:0; background:none; border:0; border-radius:0; overflow:visible; color:var(--text-1); text-align:left; cursor:pointer; font-family:var(--font-text);}
.ls-card:hover{transform:none; border-color:transparent;}
.ls-card b{font-size:var(--fs-sm); font-weight:600; line-height:1.25; transition:color var(--dur-fast); overflow-wrap:anywhere;}
.ls-card span{font-size:var(--fs-xs); color:var(--text-2); margin-top:-4px;}
.ls-card:hover b{color:var(--accent);}
.ls-card:focus-visible{outline:2px solid var(--accent); outline-offset:4px; border-radius:var(--r-sm);}
.ls-cover.list-cover-posters, .ls-cover.list-cover-posters-empty{
  display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); grid-template-rows:repeat(2, minmax(0, 1fr)); gap:3px; aspect-ratio:1;
  border-radius:var(--r-sm); overflow:hidden; background:var(--surface-2);
}
.ls-cover.is-one{grid-template-columns:1fr; grid-template-rows:minmax(0, 1fr);}
.ls-cover img{width:100%; height:100%; object-fit:cover; display:block;}
.ls-cover.list-cover-posters-empty{grid-template-columns:1fr; grid-template-rows:minmax(0, 1fr); place-items:center; color:var(--text-3);}
.ls-cover.list-cover-posters-empty .ui-icon{width:28px; height:28px;}
.ls-new-card .ls-new{display:grid; place-items:center; aspect-ratio:1; border:1.5px dashed var(--line-strong); border-radius:var(--r-sm); color:var(--text-2); font-size:13px; font-weight:600; margin:0; transition:border-color var(--dur-fast), color var(--dur-fast);}
.ls-new-card:hover .ls-new, .ls-new-card[aria-expanded="true"] .ls-new{border-color:var(--accent); color:var(--accent);}
.ls-empty{grid-column:1 / -1; margin:0;}
.ls-page .gp-create{margin-top:var(--sp-4);}
/* Une liste */
.ls-page .gp-id{min-width:0;}
.ls-page .gp-id[hidden]{display:none;}
.ls-edit-fields{display:grid; gap:8px; flex:1; min-width:220px; max-width:520px;}
.ls-edit-fields input{height:40px; border-radius:var(--r-md); padding:0 14px;}
.ls-edit-fields #listEditTitle{font-size:var(--fs-md); font-weight:600;}
.ls-page .gp-meta .friend-avatar{width:22px; height:22px; border-radius:50%; flex:none;}
.ls-page .gp-meta .friend-avatar-placeholder{display:grid; place-items:center; background:var(--surface-2); color:var(--text-2);}
.ls-page .gp-meta .friend-avatar-placeholder .ui-icon{width:12px; height:12px;}
.ls-page .gp-actions > [hidden]{display:none;}
#listLikeBtn.active{color:var(--accent);}
#listLikeBtn .ui-icon{width:16px; height:16px; vertical-align:-3px;}
.ff-menu .nt-danger{color:var(--note-low);}
#listLikesSummary:empty{display:none;}
.ls-progress{display:flex; align-items:center; gap:12px; margin-top:var(--sp-4); font-size:var(--fs-xs); color:var(--text-2);}
.ls-progress:empty{display:none;}
.ls-progress i{flex:1; max-width:240px; height:4px; border-radius:var(--r-full); background:var(--surface-2); overflow:hidden;}
.ls-progress i b{display:block; height:100%; background:var(--accent);}
.ls-edit-bar[hidden], .ls-add #listAddSearch[hidden], #listAddToggle[hidden]{display:none;}
.ls-edit-bar .nt-switch{margin-top:var(--sp-4);}
.ls-add{margin-top:var(--sp-3);}
.ls-drag-hint{margin-top:var(--sp-2);}
.ls-items{display:grid; grid-template-columns:repeat(auto-fill, minmax(140px, 1fr)); gap:22px 16px; margin-top:var(--sp-5);}
.ls-items > .tmdb-empty{grid-column:1 / -1;}
.ls-tile{display:grid; gap:6px; align-content:start; min-width:0; cursor:pointer;}
.ls-tile-img{position:relative; aspect-ratio:2/3; border-radius:var(--r-sm); overflow:hidden; background:var(--surface-2);}
.ls-tile-img img, .ls-tile-img .film-poster-placeholder{width:100%; height:100%; object-fit:cover; display:block;}
.ls-tile-img .film-poster-placeholder{display:grid; place-items:center;}
.ls-tile-t{font-size:var(--fs-sm); font-weight:600; line-height:1.25; transition:color var(--dur-fast); overflow-wrap:anywhere;}
.ls-tile-s{font-size:var(--fs-xs); color:var(--text-2); margin-top:-2px;}
.ls-tile:not(.is-editing):hover .ls-tile-t{color:var(--accent);}
.ls-rank{position:absolute; top:6px; left:6px; z-index:1; min-width:24px; height:24px; padding:0 6px; border-radius:var(--r-full); display:grid; place-items:center; background:rgba(var(--glass-rgb),0.78); color:var(--text-1); font-size:11px; font-weight:700; font-variant-numeric:tabular-nums;}
.ls-tile.is-editing{cursor:default;}
.ls-tile.is-editing .ls-tile-img::after{content:""; position:absolute; inset:0; background:rgba(10,10,10,0.35); pointer-events:none;}
.ls-tile.is-dragging{opacity:0.6;}
.ls-drag, .ls-x{position:absolute; top:6px; z-index:2; width:32px; height:32px; padding:0; border:0; border-radius:50%; display:grid; place-items:center; background:rgba(var(--glass-rgb),0.85); color:var(--text-1); cursor:pointer;}
.ls-drag{left:6px; cursor:grab; touch-action:none;}
.ls-drag:active{cursor:grabbing;}
.ls-drag .ui-icon{width:16px; height:16px;}
.ls-x{right:6px; font-size:var(--fs-lg); line-height:1;}
.ls-drag:focus-visible, .ls-x:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
/* Ajouter à une liste */
.modal.ls-sheet{max-width:440px; padding:20px 24px 0; display:flex; flex-direction:column; max-height:calc(100vh - 80px);}
.ls-sheet .nt-ht b{white-space:normal;}
.ls-check-list{margin-top:var(--sp-3); overflow-y:auto; min-height:0;}
.ls-check{display:flex; align-items:center; gap:12px; min-height:52px; padding:6px 0; border-bottom:1px solid var(--line); font-size:var(--fs-sm); cursor:pointer;}
.ls-check input{width:22px; height:22px; margin:0; flex:none; accent-color:var(--accent); cursor:pointer;}
.ls-check-main{flex:1; min-width:0; display:grid; gap:2px;}
.ls-check-main small{font-size:var(--fs-xs); color:var(--text-2);}
.ls-new-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:var(--sp-3) 0;}
.ls-new-row input{flex:1; min-width:180px; height:40px; border-radius:var(--r-full); padding:0 16px;}
.ls-new-row input[hidden]{display:none;}
.ls-sheet .nt-foot .nt-hint{margin:0;}
@media (max-width:600px){
  .ls-grid{grid-template-columns:repeat(2, minmax(0, 1fr)); gap:18px 12px;}
  .ls-items{grid-template-columns:repeat(3, minmax(0, 1fr)); gap:16px 12px;}
  .ls-edit-fields{flex-basis:100%; max-width:none;}
  .ls-drag, .ls-x{width:30px; height:30px; top:4px;}
  .ls-drag{left:4px;}
  .ls-x{right:4px;}
  #addToListOverlay{padding:44px 0 0; align-items:flex-end;}
  .modal.ls-sheet{max-width:none; width:100%; border-radius:var(--r-md) var(--r-md) 0 0; padding:16px 16px 0;}
  .ls-sheet .nt-foot{padding-bottom:calc(16px + env(safe-area-inset-bottom, 0px));}
}
@media (prefers-reduced-motion:reduce){
  .ls-card b, .ls-tile-t, .ls-new-card .ls-new{transition:none;}
}

/* ==========================================================
   AUDIT GÉNÉRAL, LOT 3 (16/10) : confort au doigt, rappel, bande-annonce.
   ========================================================== */
/* B6 : rappel d'inactivité sur le catalogue seulement, dans les marges au téléphone. */
#inactivityReminderBanner.off-route{display:none !important;}
@media (max-width:600px){
  .digest-banner{margin:var(--sp-3) var(--sp-4); padding:var(--sp-4);}
}
/* B5 : toute la ligne d'une saison réagit, pas seulement la bande du texte. */
.sx-season-head .season-header{align-self:stretch; min-height:44px;}
/* B5 : sur écran tactile, les onglets et petits liens gardent leur dessin
   mais leur zone de toucher s'étend à 44 px de haut environ. */
@media (pointer:coarse){
  .top-tab, .catalog-switch-btn, .gp-prop-title, .app-footer-link, .watch-provider-justwatch{position:relative;}
  .top-tab::after, .catalog-switch-btn::after, .gp-prop-title::after, .app-footer-link::after, .watch-provider-justwatch::after{
    content:""; position:absolute; left:-4px; right:-4px; top:-12px; bottom:-12px;
  }
}
/* B8 : « Bande-annonce » ne se coupe plus à côté de la plateforme. */
.sx-where #seriesTrailerBtn{flex:none; white-space:nowrap;}
@media (max-width:600px){
  .sx-where{display:grid; justify-items:start; gap:var(--sp-2);}
}

/* ==========================================================
   AUDIT GÉNÉRAL, CHOIX DE FLIX DU 23/10 (I3 piste A) : du texte et une
   fine ligne au lieu des boîtes.
   ========================================================== */
/* « N films à voir disponibles » */
.wl-reminder{background:none; border:0; border-left:2px solid var(--accent); border-radius:0; padding:var(--sp-1) 0 var(--sp-1) var(--sp-3); margin-bottom:var(--sp-4); font-size:var(--fs-sm); color:var(--text-2);}
/* Rappel d'inactivité (maybeShowInactivityReminder(), js/activityState.js) */
.inactivity-reminder{margin-top:var(--sp-4); margin-bottom:var(--sp-2);}
.ir-note{border-left:2px solid var(--accent); padding:var(--sp-1) 0 var(--sp-1) var(--sp-3);}
.ir-title{font-size:var(--fs-sm); font-weight:600; color:var(--text-1);}
.ir-sub{margin-top:2px; font-size:var(--fs-sm); color:var(--text-2);}
.ir-actions{display:flex; flex-wrap:wrap; gap:var(--sp-4); margin-top:var(--sp-2);}
.ir-link{min-height:32px; background:none; border:0; padding:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--text-2); transition:color var(--dur-fast);}
.ir-link:hover{color:var(--text-1);}
.ir-link.is-accent{color:var(--accent);}
/* Cette année vs l'année dernière : deux colonnes séparées d'un filet */
.year-compare-col{background:none; border:0; border-radius:0; padding:var(--sp-2) var(--sp-3);}
.year-compare-col + .year-compare-col{border-left:1px solid var(--line);}
.year-compare-genre{background:none; padding:0; color:var(--text-2);}
/* Suggestions d'À voir : une rangée d'affiches qui défile de côté (js/suggestions.js) */
.wls-strip{display:grid; grid-auto-flow:column; grid-auto-columns:128px; gap:var(--sp-3); overflow-x:auto; scrollbar-width:none; scroll-snap-type:x proximity; padding-bottom:var(--sp-1);}
.wls-strip::-webkit-scrollbar{display:none;}
.wls-item{display:flex; flex-direction:column; align-items:flex-start; gap:var(--sp-1); min-width:0; scroll-snap-align:start;}
.wls-poster{display:block; width:100%; aspect-ratio:2/3; padding:0; border:0; border-radius:var(--r-sm); overflow:hidden; background:var(--surface-2); cursor:pointer;}
.wls-poster img{width:100%; height:100%; object-fit:cover; display:block; transition:transform var(--dur-fast);}
.wls-poster:hover img{transform:scale(1.03);}
.wls-poster .film-poster-placeholder{width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:var(--text-3);}
.wls-add{min-height:32px; white-space:nowrap; background:none; border:0; padding:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent); transition:color var(--dur-fast);}
.wls-add:hover{color:var(--text-1);}
.wls-add:disabled{opacity:0.5; cursor:default;}
@media (max-width:600px){
  .wls-strip{grid-auto-columns:100px;}
}
@media (prefers-reduced-motion:reduce){
  .wls-poster img{transition:none;}
}
/* « Ton année » (I9 piste A) : un lien texte dans les Statistiques, et
   l'image à partager au dernier écran du récap (openYearRecap(), js/stats.js). */
.year-link{display:inline-block; min-height:32px; margin:0 0 var(--sp-5); padding:0; background:none; border:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-md); font-weight:600; color:var(--accent); transition:color var(--dur-fast);}
.year-link:hover{color:var(--text-1);}
.recap-slide.active{pointer-events:auto;}
.recap-share{margin-top:var(--sp-3);}
/* Format des Nouveautés, rappelé dans l'onglet de l'admin (js/admin.js). */
.cl-admin-hint{margin:0 0 var(--sp-2); font-size:var(--fs-xs); color:var(--text-2);}
/* Les mieux notés sur tes abonnements (js/streamingTop.js) : la rangée des
   suggestions, avec la note TMDB à gauche de « + À voir ». */
.wls-hint{margin-left:var(--sp-2); font-family:var(--font-text); font-size:var(--fs-xs); font-weight:400; color:var(--text-2);}
.wls-meta{display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2); width:100%;}
.wls-note{font-size:var(--fs-xs); font-weight:600; color:var(--text-2); font-variant-numeric:tabular-nums;}
#streamingTopSection{margin-top:var(--sp-6);}
/* Surprends-moi, plus de critères et « pourquoi ce film » (28/10) */
.surprise-criteria-toggle{display:block; margin:calc(-1 * var(--sp-2)) 0 var(--sp-3); padding:var(--sp-1) 0; background:none; border:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; color:var(--text-2);}
.surprise-criteria-toggle::after{content:' ›'; display:inline-block; transition:transform var(--dur-fast);}
.surprise-criteria-toggle[aria-expanded="true"]::after{transform:rotate(90deg);}
.surprise-criteria-toggle:hover{color:var(--text-1);}
.surprise-criteria{margin:0 0 var(--sp-4); display:flex; flex-direction:column; gap:var(--sp-2);}
.surprise-criteria[hidden]{display:none;}
.surprise-crit-row{display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-2);}
.surprise-crit-label{width:56px; font-size:var(--fs-xs); color:var(--text-2);}
.surprise-crit-row .wl-sub-chip{padding:0 var(--sp-3);}
.surprise-crit-row .wl-sub-chip[aria-checked="true"]{color:var(--text-1); border-color:var(--accent); background:var(--accent-wash);}
.surprise-crit-row .wl-sub-chip:disabled{opacity:0.45; cursor:default;}
.surprise-crit-row .wl-sub-chip[aria-checked="true"]:disabled{opacity:1;}
.surprise-crit-wait{font-size:var(--fs-xs); color:var(--text-2);}
.surprise-why{display:flex; flex-wrap:wrap; justify-content:center; gap:var(--sp-1) var(--sp-3); margin-top:var(--sp-1); font-size:var(--fs-sm); color:var(--text-2);}
.surprise-why:empty{display:none;}
.surprise-why-friends{color:var(--accent);}
@media (max-width:600px){
  .surprise-crit-row{flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none; padding-right:var(--sp-5); -webkit-mask-image:linear-gradient(90deg, black calc(100% - var(--sp-5)), transparent); mask-image:linear-gradient(90deg, black calc(100% - var(--sp-5)), transparent);}
  .surprise-crit-row::-webkit-scrollbar{display:none;}
  .surprise-crit-row .wl-sub-chip, .surprise-crit-label{flex:none;}
}
/* Bientôt dans ton agenda (downloadUpcomingIcs(), js/upcoming.js) */
.up-ics{margin-left:var(--sp-1); padding:0; background:none; border:0; cursor:pointer; font-family:var(--font-text); font-size:var(--fs-sm); font-weight:600; color:var(--accent);}
.up-ics:hover{color:var(--text-1);}
/* Arrivé sur tes abonnements (renderWatchlistArrivals(), js/watchlist.js) */
.wl-arrivals{margin-top:calc(-1 * var(--sp-2));}
.wl-arrivals[hidden]{display:none;}
.wl-arrival{padding:0; background:none; border:0; cursor:pointer; font:inherit; font-weight:600; color:var(--accent);}
.wl-arrival:hover{color:var(--text-1);}
/* Bulles d'aide « ? » (js/ui.js) : un petit rond à côté du titre, l'aide en
   texte et fine ligne dessous, comme les rappels. */
.help-q{position:relative; display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px; margin-left:var(--sp-2); padding:0; border:1px solid var(--line-strong); border-radius:var(--r-full); background:none; cursor:pointer; vertical-align:middle; font-family:var(--font-text); font-size:var(--fs-xs); font-weight:600; line-height:1; color:var(--text-2); transition:color var(--dur-fast), border-color var(--dur-fast);}
.help-q:hover, .help-q[aria-expanded="true"]{color:var(--accent); border-color:var(--accent);}
.help-q:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
@media (pointer:coarse){ .help-q::after{content:''; position:absolute; inset:-12px;} }
.nt-head .help-q{flex:none;}
.help-bubble{max-width:62ch; margin:0 0 var(--sp-4); padding:var(--sp-1) 0 var(--sp-1) var(--sp-3); border-left:2px solid var(--accent); font-size:var(--fs-sm); line-height:1.5; color:var(--text-2);}
.help-bubble[hidden]{display:none;}
#helpNotation{margin:var(--sp-2) var(--sp-4) var(--sp-3);}
/* Ta note prévue (fillFilmPrediction(), js/prediction.js) */
.ff-predict{margin:var(--sp-2) 0 var(--sp-3); font-size:var(--fs-sm); color:var(--text-2);}
.ff-predict[hidden]{display:none;}
/* Ton ADN ciné (js/dna.js) : des lignes libellé / valeur, une fine ligne entre. */
.dna-head{display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-3); flex-wrap:wrap; margin-bottom:var(--sp-2);}
.dna-head .year-link{margin:0; font-size:var(--fs-sm);}
.dna-row{display:grid; grid-template-columns:150px 1fr; gap:var(--sp-3); padding:var(--sp-2) 0; border-bottom:1px solid var(--line-soft); font-size:var(--fs-sm);}
.dna-row:last-child{border-bottom:0;}
.dna-label{color:var(--text-2);}
.dna-val{color:var(--text-1);}
.dna-wait{color:var(--text-2);}
#dnaSection[hidden]{display:none;}
@media (max-width:600px){
  .dna-row{grid-template-columns:1fr; gap:2px;}
}
/* À voir en commun (renderGroupWatchlistOverlap(), js/groups.js) : l'affiche ouvre la fiche. */
.gp-overlap-open{flex:none; padding:0; border:0; background:none; cursor:pointer; border-radius:var(--r-sm); overflow:hidden; line-height:0;}
.gp-overlap-open img, .gp-overlap-open .film-poster-placeholder{display:block; width:40px; height:60px; object-fit:cover;}
/* Séries, notes et commentaires d'épisode (28/10, js/series.js) :
   un petit point sous un épisode noté ou commenté, le commentaire en une
   ligne discrète, et l'anneau de la série en pointillés quand il suit la
   moyenne des épisodes plutôt qu'une note posée à la main. */
.sx-sq-btn{position:relative;}
.sx-sq-btn.has-note::after{content:''; position:absolute; left:50%; bottom:3px; width:4px; height:4px; margin-left:-2px; border-radius:var(--r-full); background:currentColor; opacity:0.7;}
.sx-comment{flex-basis:100%; margin:0; font-size:var(--fs-sm); color:var(--text-2);}
.sx-comment span{font-style:italic;}
.sx-comment-add{flex-basis:100%; text-align:left; font-size:var(--fs-sm);}
.sx-comment-form{flex-basis:100%; display:flex; flex-direction:column; gap:var(--sp-2);}
.sx-comment-input{width:100%; resize:vertical; min-height:56px;}
.sx-comment-actions{display:flex; justify-content:flex-end; align-items:center; gap:var(--sp-3);}
.counter.is-derived{border-style:dashed;}
.sx-note-avg{margin-top:var(--sp-2);}
.sx-note-avg[hidden]{display:none;}
.sx-legend-hint{color:var(--text-3);}
/* Fiches film et série : Retour, flèches et ⋯ posés sur le bandeau (retour
   de Flix, 28/10 : « le bouton retour… pas beau »). Ronds en verre fumé, la
   même teinte dans les deux thèmes : ils sont sur une photo. */
.film-page .detail-topbar{position:relative; z-index:4; height:0; margin:0;}
.film-page .detail-topbar > .detail-back{position:absolute; top:var(--sp-3); left:var(--sp-3);}
.film-page .detail-topbar > .detail-nav{position:absolute; top:var(--sp-3); right:var(--sp-3); margin:0; gap:var(--sp-2); align-items:center;}
.detail-back, .film-page .detail-nav-arrow{
  width:40px; height:40px; padding:0; border-radius:var(--r-full);
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  border:1px solid rgba(255,255,255,0.18); background:rgba(15,14,22,0.45);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
  color:#fff; font-size:var(--fs-lg); line-height:1;
  transition:background-color var(--dur-fast), transform var(--dur-press) var(--ease-out);
}
.detail-back svg{width:20px; height:20px;}
.detail-more-btn svg{width:18px; height:18px;}
.detail-back:hover, .film-page .detail-nav-arrow:hover{background:rgba(15,14,22,0.65); color:#fff;}
.detail-back:active, .film-page .detail-nav-arrow:active{transform:scale(0.94);}
.detail-back:focus-visible, .film-page .detail-nav-arrow:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
.film-page .detail-nav .ff-menu{left:auto; right:0; min-width:200px;}
@media (prefers-reduced-motion:reduce){ .detail-back, .film-page .detail-nav-arrow{transition:none;} }
