/* ==========================================================================
   LE GUIDE DES ITEMS — feuille de style unique

   ORGANISATION (et règle de travail) :
     1. JETONS       → toutes les valeurs réutilisables, au même endroit
     2. THÈME NUIT   → le bleu marine de la charte (thème par défaut)
     3. THÈME JOUR   → la déclinaison claire
     4. COMPOSANTS   → n'utilisent QUE des jetons, jamais une valeur en dur

   ⚠️ Pour changer une couleur, une police, un arrondi : modifier le JETON,
   jamais le composant. Aucune valeur littérale ne doit réapparaître plus bas,
   sinon changer la charte redevient une chasse au trésor.
   ========================================================================== */

/* ==========================================================================
   0. LES POLICES — hébergées avec le site
   Pourquoi pas Google Fonts : ce guide doit s'ouvrir en brocante, sans réseau.
   Une police appelée sur un serveur tiers ne se charge pas au premier
   affichage hors connexion, et l'appareil retombe sur une police de secours
   qui n'a rien à voir avec la charte. Ces trois fichiers pèsent 78 Ko au
   total, sont mis en cache par le service worker, et ne sortent jamais du
   site. (Sous-ensemble latin uniquement.)
   ========================================================================== */

@font-face {
  font-family: 'Oswald';
  src: url('../assets/fonts/Oswald.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Kaushan Script';
  src: url('../assets/fonts/KaushanScript.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Archivo';
  src: url('../assets/fonts/Archivo.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   1. JETONS INDÉPENDANTS DU THÈME
   ========================================================================== */

:root {
  /* --- Les trois polices, et leur rôle ------------------------------------
     RÈGLE TYPOGRAPHIQUE DU PROJET.

     Le choix n'est pas une impression : les proportions des maquettes de
     Mathieu ont été MESURÉES (largeur du mot rapportée à la hauteur des
     capitales, sur BOOSTERS / DISPLAYS / ETB / DECKS / COFFRETS / POKEBOX),
     puis comparées à six candidates. Écarts obtenus :
       Oswald 700    4,1 %   ← retenue
       Fjalla One   17,6 %
       Bebas Neue   18,9 %
       Anton        21,9 %
       Archivo Black 62 %    (deux tiers trop large — écartée)
     Même méthode pour la manuscrite (« Items vides ») : Kaushan Script est
     la plus proche des cinq testées.

     --police-affichage  NOMME les choses. Logo, titre d'écran, catégorie,
                         liste, série, item. TOUJOURS en capitales, TOUJOURS
                         en graisse 700 (Oswald en compte plusieurs).

     --police-manuscrite ACCOMPAGNE un titre, et rien d'autre. Jamais pour une
                         information utile : c'est un ornement, il doit rester
                         facultatif à la lecture.

     --police-texte      DIT les choses. Phrases, compteurs, boutons, aide.
                         Archivo : grotesque neutre de largeur normale, qui
                         laisse toute la personnalité au condensé des titres.
     ---------------------------------------------------------------------- */
  --police-affichage:  'Oswald', 'Haettenschweiler', 'Arial Narrow', sans-serif;
  --police-manuscrite: 'Kaushan Script', 'Brush Script MT', cursive;
  --police-texte:      'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* Oswald existe en plusieurs graisses : sans cette valeur, les titres
     s'affichent en maigre et perdent toute leur présence. */
  --graisse-affichage: 700;

  /* --- Tailles ------------------------------------------------------- */
  --taille-logo:        clamp(38px, 11vw, 68px);
  --taille-logo-script: clamp(26px, 7.5vw, 46px);
  --taille-titre-haut:  clamp(22px, 5.8vw, 34px);
  --taille-script-haut: clamp(18px, 4.8vw, 27px);
  --taille-titre:       clamp(18px, 4.4vw, 24px);
  --taille-categorie:   clamp(24px, 6.4vw, 32px);
  --taille-liste:       clamp(18px, 4.8vw, 22px);
  --taille-serie:       18px;
  --taille-item:        16.5px;
  --taille-texte:       15px;
  --taille-petit:       13px;
  --taille-minuscule:   11.5px;

  /* --- Graisses ------------------------------------------------------ */
  --gras:      600;
  --tres-gras: 800;

  /* --- Interlettrage --------------------------------------------------
     Une police très grasse a besoin d'air en capitales ; le texte courant, non. */
  --lettres-affichage: 0.01em;
  --lettres-capitales: 0.06em;

  /* --- Espacements ---------------------------------------------------- */
  --esp-xs: 4px;
  --esp-sm: 8px;
  --esp-md: 14px;
  --esp-lg: 22px;
  --esp-xl: 30px;

  /* --- Arrondis ------------------------------------------------------- */
  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 22px;
  --r-plein: 99px;

  /* --- Gabarit -------------------------------------------------------- */
  --largeur-max: 1080px;
  /* CONSTANTE PURE — ne dépend d'aucune mesure JS et ne varie JAMAIS avec le
     défilement. 44 px est la hauteur d'une barre de navigation iOS (HIG).
     C'est cette invariance qui rend tout le reste imperturbable : la place
     réservée par `body` est juste, à la première frame comme à la millième,
     et les trois éléments collants s'empilent sans se resynchroniser.
     AVANT : valeur mesurée à chaque redimensionnement, avec deux variables à
     tenir d'accord. Un bandeau qui rétrécit oblige à ça, et ça faisait sauter
     la page de 17 px en plein défilement (tenté puis abandonné, 2026-08-14). */
  /* DEUX CONSTANTES PURES, aucune mesure JS.
     La place réservée par `body` vaut TOUJOURS la hauteur DÉPLOYÉE : elle ne
     bouge donc jamais, et la page ne peut pas sauter quand le bandeau maigrit.
     C'est ce point précis que ma première tentative avait raté — elle mesurait
     la hauteur courante et la réécrivait pendant la transition. */
  --entete-plein:   calc(96px + env(safe-area-inset-top, 0px));
  /* 60 px et non 48 : le filtre est une cible tactile de 44 px, il lui faut
     8 px d'air en haut ET en bas pour ne pas se lire comme un débordement.
     Trouvé par le contrôle « marge de sécurité », pas à l'œil. */
  --entete-compact: calc(64px + env(safe-area-inset-top, 0px));
  /* Ce que les éléments COLLANTS suivent : la hauteur courante du bandeau. */
  --hauteur-entete: var(--entete-plein);
  --cible-tactile: 44px;   /* minimum absolu : usage debout, une seule main */

  /* --- Superpositions -------------------------------------------------- */
  --z-serie:  20;
  --z-entete: 30;
  --z-flottant:   40;
  --z-fiche:  50;
  --z-toast:  60;

  /* --- Animations ------------------------------------------------------ */
  --duree-courte: 0.14s;
  /* Le glissement de la barre basse : assez lent pour qu'on comprenne qu'elle
     part se ranger (et donc qu'elle peut revenir), assez court pour ne jamais
     se faire attendre en remontant. */
  --duree-moyenne: 0.22s;
  --duree-longue: 0.35s;
  --courbe: cubic-bezier(0.22, 1, 0.36, 1);
  /* L'arrivée d'un écran. Plus long que les transitions ci-dessus, parce qu'un
     mouvement qui doit se REMARQUER a besoin de durer ; et une décélération
     très marquée (l'élément parcourt 80 % du chemin dans le premier tiers du
     temps), parce que c'est ce qui donne l'impression que la page se pose au
     lieu de glisser. */
  --duree-entree: 0.42s;
  --courbe-entree: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ==========================================================================
   2. THÈME NUIT — le bleu marine de la charte (thème par défaut)
   ========================================================================== */

:root,
:root[data-theme='nuit'] {
  --c-fond:          #191C2F;
  --c-fond-haut:     #1D2137;
  --c-fond-bas:      #15182A;
  /* L'affiche d'accueil se fond dans la page : la teinte est celle du fond du
     thème courant, en composantes, pour pouvoir la doser en transparence. */
  --affiche-teinte:  25, 28, 47;
  --affiche-titre:   #FFFFFF;
  --affiche-ombre:   0 2px 14px rgba(10, 12, 24, 0.55);
  --affiche-densite: 0.72;

  --c-surface:       #21243B;
  --c-surface-haute: #262A44;
  --c-bordure:       #2E3250;

  --c-texte:         #FFFFFF;
  --c-texte-doux:    rgba(255, 255, 255, 0.58);  /* 6,4:1 — conforme AA */
  --c-texte-faible:  rgba(255, 255, 255, 0.38);  /* décoratif UNIQUEMENT */

  --c-trait:         rgba(255, 255, 255, 0.12);
  --c-trait-fin:     rgba(255, 255, 255, 0.06);
  --c-survol:        rgba(255, 255, 255, 0.06);
  --c-appui:         rgba(255, 255, 255, 0.12);

  /* Blocs pleins (cartes de catégorie, menu des listes)
     -------------------------------------------------------------------------
     LA NUIT, CES BLOCS SONT SOMBRES — ils étaient blancs jusqu'au 2026-08-13.

     Un aplat blanc plein écran sur fond bleu nuit, c'est le contresens
     classique du mode sombre : il éblouit, il ruine l'adaptation de l'œil
     (le guide sert le soir, en brocante), il fait halo sur les écrans OLED,
     et il inverse la hiérarchie — le contenant crie plus fort que le contenu.

     C'est aussi contraire à la règle du projet, « deux thèmes, chacun entier » :
     un bloc blanc au milieu d'une page bleu nuit est le thème hybride en
     miroir, celui qu'Adrien avait explicitement rejeté dans l'autre sens.

     La méthode retenue est celle des systèmes de design sombres : l'élévation
     se marque par une surface PLUS CLAIRE que le fond, pas par du blanc, et
     accompagnée d'un filet — la nuit, une ombre portée ne se voit pas.
     Le blanc ne subsiste plus qu'en petites touches : le texte, et le toast. */
  --c-bloc-fond:     #21243B;                     /* surface surélevée */
  --c-bloc-texte:    #FFFFFF;                     /* 12,1:1 — largement AA */
  --c-bloc-doux:     rgba(255, 255, 255, 0.62);   /* 6,4:1 — conforme AA */
  --c-bloc-trait:    rgba(255, 255, 255, 0.10);   /* séparateurs de lignes */
  --c-bloc-survol:   rgba(255, 255, 255, 0.06);
  --c-bloc-piste:    rgba(255, 255, 255, 0.24);   /* piste de barre / d'anneau */
  --c-bloc-bordure:  rgba(255, 255, 255, 0.08);   /* le filet qui détache le bloc */

  /* Le jaune de cochage — identique dans les deux thèmes, c'est la marque */
  --c-coche:         #FEF43D;
  --c-coche-sombre:  #E2D400;
  --c-coche-marque:  #191C2F;

  /* Le jaune employé comme TEXTE */
  --c-jauge:         #FEF43D;   /* la nuit, le jaune porte parfaitement */
  /* ⚠️ CE JETON ÉTAIT #FEF43D JUSQU'AU 2026-08-17, c'est-à-dire LE JAUNE DU
     COCHAGE — sur l'anneau de focus clavier de TOUT le site, et sur le contour
     de chaque champ actif. La règle « le jaune ne veut dire qu'une chose : je
     possède cet item » était donc enfreinte partout, depuis l'origine, sans
     qu'aucun contrôle ne s'en aperçoive : ils cherchaient le jaune sur des
     états d'OPTION, jamais sur le focus.
     Trouvé par `apple-design-master`. Le blanc fait mieux, mesuré : 16,8:1
     contre 14,6:1 sur le fond nuit (seuil non-texte : 3:1). */
  --c-focus:         #FFFFFF;   /* anneau de focus clavier + champ actif */
  --c-coche-trait:   #FEF43D;   /* contour d'une case cochée */

  --c-accent-texte:  #FEF43D;
  --c-accent-voile:  rgba(254, 244, 61, 0.12);
  --c-accent-trait:  rgba(254, 244, 61, 0.35);

  --c-danger:        #FF8A8A;
  --c-danger-trait:  rgba(255, 138, 138, 0.50);

  --c-entete-fond:   rgba(25, 28, 47, 0.97);
  --c-flottant-fond:     rgba(33, 36, 59, 0.94);
  --c-flottant-trait:    rgba(255, 255, 255, 0.14);

  --c-toast-fond:    #FFFFFF;
  --c-toast-texte:   #191C2F;

  --c-case-trait:    rgba(255, 255, 255, 0.62);   /* contour de case : 3:1 minimum */
  --c-champ-trait:   rgba(255, 255, 255, 0.42);   /* contour de champ de saisie */
  --c-bouton-trait:  rgba(255, 255, 255, 0.35);
  --c-rideau:        rgba(10, 11, 20, 0.62);

  /* Décor : faisceaux clairs sur fond sombre */
  --faisceau-fort:  rgba(255, 255, 255, 0.075);
  --faisceau-moyen: rgba(255, 255, 255, 0.055);
  --faisceau-fin:   rgba(255, 255, 255, 0.022);
  --voile-bords:    rgba(0, 0, 0, 0.34);

  --ombre:       0 8px 24px rgba(0, 0, 0, 0.28);
  --ombre-forte: 0 14px 34px rgba(0, 0, 0, 0.38);

  color-scheme: dark;
}

/* ==========================================================================
   3. THÈME JOUR
   Même construction, inversée : fond clair et faisceaux gris pâle — le
   traitement exact des pages blanches du dossier de présentation de Mathieu.
   ========================================================================== */

:root[data-theme='jour'] {
  --c-fond:          #F2F3F7;
  --c-fond-haut:     #FAFBFD;
  --c-fond-bas:      #E9EBF1;
  --affiche-teinte:  242, 243, 247;
  /* En thème jour le titre est SOMBRE : blanc sur une image éclaircie, il
     tombait à 2,3:1 — mesuré à l'écran, pas supposé. La règle du projet est
     que chaque thème est entier ; c'est aussi vrai pour du texte sur image. */
  --affiche-titre:   #191C2F;
  --affiche-ombre:   0 1px 10px rgba(255, 255, 255, 0.85);
  --affiche-densite: 0.88;

  --c-surface:       #FFFFFF;
  --c-surface-haute: #FFFFFF;
  --c-bordure:       rgba(25, 28, 47, 0.14);

  --c-texte:         #191C2F;
  --c-texte-doux:    rgba(25, 28, 47, 0.68);   /* tient encore 4,5:1 sur une ligne survolée */
  --c-texte-faible:  rgba(25, 28, 47, 0.42);

  --c-trait:         rgba(25, 28, 47, 0.14);
  --c-trait-fin:     rgba(25, 28, 47, 0.08);
  --c-survol:        rgba(25, 28, 47, 0.05);
  --c-appui:         rgba(25, 28, 47, 0.10);

  /* En thème clair, les blocs pleins restent CLAIRS. Un bloc bleu nuit au
     milieu d'une page claire donne un thème hybride, ni jour ni nuit — c'est
     précisément le défaut relevé par Adrien. Un thème s'engage : clair partout,
     ou nuit partout. La hiérarchie passe alors par l'ombre et le trait, pas
     par un renversement de couleur. */
  --c-bloc-fond:     #FFFFFF;
  --c-bloc-texte:    #191C2F;
  --c-bloc-doux:     rgba(25, 28, 47, 0.62);
  --c-bloc-trait:    rgba(25, 28, 47, 0.10);
  --c-bloc-survol:   rgba(25, 28, 47, 0.05);
  --c-bloc-piste:    rgba(25, 28, 47, 0.20);   /* piste de barre / d'anneau */
  /* Le jour, blanc sur blanc cassé : il faut un filet pour détacher le bloc. */
  --c-bloc-bordure:  rgba(25, 28, 47, 0.10);

  --c-coche:         #FEF43D;
  --c-coche-sombre:  #E2D400;
  --c-coche-marque:  #191C2F;

  /* Le jaune ne se lit pas sur du blanc : en plein jour, le texte d'accent
     passe en bleu marine. La couleur de COCHAGE, elle, ne bouge pas. */
  --c-jauge:         #191C2F;   /* le jour, le jaune sur clair fait 1,15:1 */
  --c-focus:         #191C2F;   /* idem : un focus jaune sur clair ne se voit pas */
  /* Le contour, et LUI SEUL, détache la case cochée du blanc : le jaune de la
     marque plafonne à 1,15:1 sur blanc — vingt fois sous le seuil. On ne touche
     donc pas au remplissage (c'est la marque, identique dans les deux thèmes),
     on lui donne un contour bleu marine, qui porte le contraste (15:1).
     #E2D400 était utilisé ici : 1,54:1, il échouait aussi. */
  --c-coche-trait:   #191C2F;

  --c-accent-texte:  #191C2F;
  --c-accent-voile:  rgba(254, 244, 61, 0.30);
  --c-accent-trait:  rgba(25, 28, 47, 0.25);

  --c-danger:        #C0392B;
  --c-danger-trait:  rgba(192, 57, 43, 0.45);

  --c-entete-fond:   rgba(242, 243, 247, 0.97);
  --c-flottant-fond:     rgba(255, 255, 255, 0.94);
  --c-flottant-trait:    rgba(25, 28, 47, 0.14);

  --c-toast-fond:    #191C2F;
  --c-toast-texte:   #FFFFFF;

  --c-case-trait:    rgba(25, 28, 47, 0.58);   /* contour de case : 3:1 minimum */
  --c-champ-trait:   rgba(25, 28, 47, 0.52);   /* contour de champ de saisie */
  --c-bouton-trait:  rgba(25, 28, 47, 0.30);
  --c-rideau:        rgba(25, 28, 47, 0.42);

  --faisceau-fort:  rgba(25, 28, 47, 0.055);
  --faisceau-moyen: rgba(25, 28, 47, 0.038);
  --faisceau-fin:   rgba(25, 28, 47, 0.018);
  --voile-bords:    rgba(25, 28, 47, 0.10);

  --ombre:       0 8px 24px rgba(25, 28, 47, 0.12);
  --ombre-forte: 0 14px 34px rgba(25, 28, 47, 0.18);

  color-scheme: light;
}

/* ==========================================================================
   4. BASE
   ========================================================================== */

* { box-sizing: border-box; }

/* La montée du décor. Elle ne dépend PAS du thème — c'est une forme, pas une
   couleur — donc elle vit ici et pas dans les deux blocs de jetons.
   Les repères : rien au-dessus de 30 %, puis une rampe longue et continue
   jusqu'à 90 %. L'affiche de l'accueil descend jusqu'à 44 % de la hauteur sur
   un iPhone courant (mesuré : 374 px sur 844) — le décor n'y vaut donc que
   ~11 % de sa force, et la couleur des deux côtés de son bord est la même à
   moins d'un point de luminance près. Une rampe COURTE remettrait la marche
   ailleurs au lieu de la supprimer. */
:root {
  --decor-monte: linear-gradient(to bottom, transparent 30%, #000 90%);
  /* La hauteur de l'affiche de l'accueil. Elle décide aussi de l'ÉCHELLE des
     items du tapis (`.home-tapis`) : l'affiche est en `object-fit: cover` sur
     une boîte de cette hauteur, donc l'image y est dessinée à `hauteur / 455`.
     Le tapis reprend la même règle, et les items ont la même taille des deux
     côtés — « même taille etc, juste rajouter des items » (Adrien).
     Le plancher, pas un rapport de forme figé : voir `.home-affiche`. */
  --affiche-h: min(58vh, 96vw);
}

html {
  background: var(--c-fond);
}

/* ⚠️ RÈGLE DE SÛRETÉ — À NE JAMAIS RETIRER
   ------------------------------------------------------------------------
   L'attribut `hidden` du HTML masque un élément… tant qu'aucune règle CSS ne
   lui impose un `display`. Une simple règle de classe le neutralise en
   silence : l'élément reste à l'écran alors que tout le code le croit masqué.

   Ça n'est pas théorique, c'est le bug le plus grave qu'ait connu ce projet.
   `.item { display: flex }` a rendu INOPÉRANTS le filtre ET la recherche des
   listes : `applyFilters()` posait bien `hidden` sur 250 items, l'écran en
   affichait 151, et la ligne de résultat annonçait « 1 item ». Adrien l'a
   signalé mot pour mot — « les filtres je vois PAS DU TOUT ce que ça fait,
   rien ne change » — et la première correction a traité sa phrase comme un
   problème de formulation au lieu d'un bug. Il décrivait exactement ce qui se
   passait.

   Cette règle unique règle le cas pour TOUS les éléments, présents et à venir.
   Les quelques règles `[hidden]` ciblées qui subsistent plus bas sont
   redondantes ; on les garde, elles ne coûtent rien et documentent l'intention.
   ------------------------------------------------------------------------ */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  color: var(--c-texte);
  font-family: var(--police-texte);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}

body {
  min-height: 100svh;
  padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px));

  /* Le fond est posé sur `html` ET sur `body`.
     Pourquoi les deux : un hôte qui embarque cette page (aperçu, iframe,
     extension) peint parfois SON propre fond sur `body`. Il recouvre alors le
     nôtre, et l'on se retrouve avec les composants du thème nuit posés sur un
     fond clair : tout le texte blanc devient illisible. Vécu.
     Conséquence : les couches de décor ne peuvent plus vivre en z-index
     négatif (elles passeraient DERRIÈRE ce fond) — voir ci-dessous. */
  background: var(--c-fond);
}

img { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--c-texte);
  color: var(--c-fond);
  padding: 10px 16px;
  border-radius: 0 0 var(--r-sm) 0;
  font-weight: var(--tres-gras);
}
.skip-link:focus { left: 0; }

:focus-visible {
  /* L'indicateur de focus doit se voir dans les DEUX thèmes : en plein jour,
     un anneau jaune sur fond clair tombe à 1,04:1 — autant dire rien. */
  outline: 3px solid var(--c-focus);
  outline-offset: 3px;
  border-radius: var(--esp-xs);
}

/* --------------------------------------------------------------------------
   LE DÉCOR
   Deux couches fixes derrière tout le contenu : immobiles au défilement,
   sans coût de repeinture.

   Les faisceaux sont relevés sur les maquettes : des bandes de lumière
   orientées « \ », d'épaisseurs et d'espacements IRRÉGULIERS, aux bords très
   adoucis. Chaque faisceau est un dégradé distinct — c'est ce qui permet des
   largeurs différentes. Une trame répétitive donnerait des rayures régulières,
   ce qui a été explicitement rejeté.

   ⚠️ LE DÉCOR MONTE DU BAS — ET C'EST CE QUI SUPPRIME LA COUTURE SOUS L'AFFICHE.
   *« C'est moche ici, la transition de fond, il y a une cassure — je pense à
   cause du truc qui fait les rayons, qui ne continue pas là-haut. Les rayons
   peuvent venir du bas avec le texte »* (Adrien, 2026-08-18, capture à l'appui).

   Le diagnostic est le sien et il était juste. L'affiche de l'accueil est une
   boîte OPAQUE qui termine sur `--c-fond`, une couleur PLATE — alors que la
   page, juste dessous, peint tout autre chose : un dégradé à 168° qui commence
   sur `--c-fond-haut` (plus clair), plus les faisceaux, plus la vignette. Deux
   peintures différentes bord à bord : une ligne horizontale nette. **Mesuré sur
   les vrais pixels, à 390 px de large : marche de 8,9 points de luminance en
   thème nuit (28,7 → 37,7) et de 6,1 en thème jour.** L'affiche IMITAIT le fond
   au lieu de le laisser voir, et l'imitation était fausse.

   Le masque règle les deux choses d'un coup : le décor n'existe plus en haut,
   où la page vaut exactement `--c-fond` — c'est-à-dire précisément ce sur quoi
   l'affiche se termine —, et il apparaît en descendant. Les faisceaux viennent
   donc du bas, comme demandé, et il n'y a plus aucun bord à raccorder puisque
   les deux côtés peignent la même couleur.

   ⚠️ NE PAS « RÉTABLIR LE DÉGRADÉ EN HAUT » sans refaire la mesure : le
   `--c-fond-haut` du sommet est précisément ce qui créait la marche.
   -------------------------------------------------------------------------- */

body::before {
  content: '';
  position: fixed;
  inset: 0;
  /* z-index 0 et non -2 : en négatif, cette couche passerait derrière le fond
     de `body` et disparaîtrait. Le contenu, lui, est remonté à 1. */
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(45deg, transparent 4%,  var(--faisceau-moyen) 11%, var(--faisceau-moyen) 20%, transparent 28%),
    linear-gradient(45deg, transparent 31%, var(--faisceau-fin) 36%, transparent 41%),
    linear-gradient(45deg, transparent 45%, var(--faisceau-fort) 54%, var(--faisceau-fort) 62%, transparent 70%),
    linear-gradient(45deg, transparent 73%, var(--faisceau-fin) 77%, transparent 81%),
    linear-gradient(45deg, transparent 84%, var(--faisceau-moyen) 91%, transparent 98%),
    linear-gradient(168deg, var(--c-fond-haut) 0%, var(--c-fond) 46%, var(--c-fond-bas) 100%);
  -webkit-mask-image: var(--decor-monte);
  mask-image: var(--decor-monte);
}

/* Vignette : les bords s'assombrissent légèrement, l'œil reste au centre.
   Elle suit la même montée que les faisceaux : c'est elle qui pesait le plus
   dans la marche du thème JOUR (elle assombrit les bords de la rangée juste
   sous l'affiche, alors que l'affiche, elle, la recouvre). */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(125% 95% at 50% 42%, transparent 42%, var(--voile-bords) 100%);
  -webkit-mask-image: var(--decor-monte);
  mask-image: var(--decor-monte);
}

/* ==========================================================================
   RÈGLE iOS — LE DOUBLE-APPUI NE DOIT JAMAIS ZOOMER
   --------------------------------------------------------------------------
   Demande d'Adrien (2026-08-14) : « attention, les double-clics font des zooms
   involontaires sur iOS dans certains cas ».

   Pourquoi ça arrive ICI plus qu'ailleurs : le geste central du guide est
   l'appui répété. On coche 261 items d'affilée, au pouce, vite. Deux appuis
   rapprochés à moins de ~350 ms sur la même zone, et Safari les interprète
   comme un double-tap : il zoome. On perd sa place dans la liste, et il faut
   dézoomer à deux doigts — debout, une main, en brocante.

   `touch-action: manipulation` supprime la détection du double-tap SANS
   désactiver le défilement ni le pincer-zoomer volontaire. C'est la seule
   solution correcte : `user-scalable=no` dans le viewport, lui, casse
   l'accessibilité (impossible d'agrandir pour lire) et Safari l'ignore de
   toute façon depuis iOS 10.

   PORTÉE VOLONTAIREMENT LARGE : tout ce qui se touche. Une exception oubliée
   sur un futur bouton, c'est le zoom qui revient sur ce bouton-là seulement —
   donc un défaut qu'on ne reproduit pas en testant les autres.
   Un contrôle le vérifie sur tous les éléments interactifs de l'écran liste.
   ========================================================================== */
button,
a,
input,
select,
textarea,
label,
summary,
[role="button"],
[role="checkbox"],
.item,
.menu-row {
  touch-action: manipulation;
}

/* ==========================================================================
   5. EN-TÊTE
   Position imposée par la charte :
   téléchargement à gauche · titre au centre · retour à droite.
   ========================================================================== */

/* LA BARRE FIXE — hauteur figée pour toujours. Rien ne la fait varier : ni le
   défilement, ni une police qui charge, ni un titre sur deux lignes. */
.gdi-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--entete-plein);
  transition: height var(--duree-moyenne) ease;
  z-index: var(--z-entete);
  display: flex;
  /* Les DEUX dernières colonnes sont en `auto`, pas en 48px fixes : le bouton
     de retour du bandeau est masqué sur cet écran (le retour vit en bas, sous
     le pouce), et une colonne de largeur fixe lui gardait sa place — d'où un
     couloir vide de 48 px à droite du filtre, qui ne s'alignait donc sur rien.
     En `auto`, une colonne vide se referme. */
  /* FLEX, plus grille — et le titre est CENTRÉ SUR L'ÉCRAN, pas sur ce qui
     l'entoure. « Le titre doit bien être au centre du header, là ça bouge un peu
     selon la taille des trucs autour » (Adrien, 2026-08-14). Dans une grille, il
     occupait une colonne élastique : il se décalait dès qu'une pastille changeait
     de largeur (« Tout » vs « Il me manque »), et il imposait sa hauteur à la
     rangée — ce qui faisait déborder le filtre du bandeau compact.
     Sorti du flux, il ne dépend plus de rien. */
  /* ⚠️ `flex-start`, PAS `space-between` — et c'est un piège qui s'est refermé
     le 2026-08-17. Avec `space-between`, la répartition dépend du NOMBRE
     d'éléments visibles : à deux, l'un va à gauche et l'autre à droite ; à
     trois, celui du milieu se retrouve CENTRÉ — c'est-à-dire pile sous le titre,
     qui est centré sur l'écran. En ajoutant l'icône d'accueil, la pastille
     d'affichage est passée de deuxième à troisième et a atterri sous le titre,
     aux quatre largeurs testées. Une répartition qui change de sens quand un
     bouton apparaît n'est pas une mise en page, c'est un hasard. Ici, les
     éléments se rangent à gauche et seule la zone d'action est poussée à
     droite (`margin-left: auto`) : le sens ne dépend plus de leur nombre. */
  justify-content: flex-start;
  align-items: center;
  gap: var(--esp-sm);
  /* Marge de sécurité en BAS : « le filtre est collé en bas, c'est moche »
     (Adrien). Un bandeau sans air sous son contenu se lit comme un débordement. */
  /* De l'air EN HAUT ET EN BAS, dans les deux états : « le filtre est collé en
     bas, c'est moche » (Adrien). Sans marge, un bandeau se lit comme un
     débordement, pas comme un cadre. Vérifié par le contrôle « marge de
     sécurité », qui a d'ailleurs attrapé un second cas que je n'avais pas vu. */
  padding: calc(env(safe-area-inset-top, 0px) + 10px) var(--esp-md) 10px;
  background: var(--c-entete-fond);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--c-trait-fin);
}

/* Le titre DANS la barre. Il n'apparaît qu'une fois le grand titre sorti de
   l'écran, en fondu — et UNIQUEMENT en opacité : jamais une propriété qui
   touche à la mise en page, sinon on réintroduit le risque de saut. */
.gdi-header__vue,
.gdi-header__action { display: flex; align-items: center; flex: none; }
/* Chaque bord garde une largeur minimale : sans elle, un bord vide laisserait le
   titre centré chevaucher la pastille de l'autre bord sur un écran étroit. */
.gdi-header__vue { min-width: 44px; }
.gdi-header__action { min-width: 44px; justify-content: flex-end; margin-left: auto; }

/* LE TITRE EST DANS LA BARRE, EN PERMANENCE.
   -------------------------------------------------------------------------
   J'avais d'abord suivi le motif iOS : un grand titre qui défile sous la barre
   et n'y apparaît qu'une fois sorti de l'écran. Adrien, capture à l'appui :
   « pourquoi le titre n'est pas dans le header ??? » — à mi-défilement le grand
   titre est coupé en deux par la barre, ce qui se lit comme un bug, pas comme
   une transition. Et il coûtait ~50 px de contenu à chaque ouverture.
   Le nom vit donc dans la barre, toujours lisible, à toute hauteur. Le grand
   titre disparaît de l'écran (il reste le <h1> de la page pour la structure). */
.gdi-title-inline {
  /* Centré sur la LARGEUR DE L'ÉCRAN, hors du flux : sa position ne dépend
     donc plus de la taille des pastilles qui l'encadrent, et sa hauteur
     n'impose plus celle de la rangée. */
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  /* ⚠️ CE PLAFOND SE CALCULE, IL NE S'ESTIME PAS — et il se recalcule dès
     qu'on touche au bandeau.
     Le titre étant centré sur l'ÉCRAN, il grandit des deux côtés à la fois :
     sa demi-largeur ne doit donc jamais atteindre le bord le plus PROCHE. Or
     les deux bords sont inégaux. Mesuré à 390 px, filtre au pire cas :
        bord gauche  14 + 48 (accueil) + 8 + 47 (vue) = finit à 117
        bord droit   14 + 139 (« Il me manque »)      = commence à 237
        centre de l'écran = 195  →  demi-largeur max = 237 − 195 = 42
        donc largeur max = 84 px, soit 100% − 306 px
     Il valait `100% − 200px`, ce qui autorisait 190 px : le titre passait donc
     SOUS le filtre dès qu'une liste avait un nom long — « POKÉMON SOUS
     CLOCHE », « BÂTON DE SUCETTE 1999 ». Ce défaut est ANTÉRIEUR à l'icône
     d'accueil, qui n'a fait que l'aggraver. Sous-évaluer ce plafond ne produit
     aucune erreur : ça se voit, ou ça ne se voit pas. Un contrôle le mesure
     désormais sur 5 listes × 3 filtres × 5 largeurs. */
  /* Écran ordinaire : les deux bords ne portent que l'icône d'accueil. */
  max-width: calc(100% - 130px);
  pointer-events: none;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  line-height: 1.05;
}
.gdi-title-inline__nom {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  /* GRAND en haut de page — « il doit être de sa grosse taille normale, pas en
     petit ». Il se réduit en défilant (voir `body.entete-compact`). */
  font-size: var(--taille-titre-haut);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  transition: font-size var(--duree-moyenne) ease;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* La catégorie en manuscrite : elle ACCOMPAGNE le nom, elle ne le remplace pas.
   BOOSTERS existe en « vide » ET en « scellé » — sans elle, on ne sait pas
   laquelle des deux on remplit. */
.gdi-title-inline__cat {
  font-family: var(--police-manuscrite);
  font-size: var(--taille-script-haut);
  opacity: 0.9;
  transition: font-size var(--duree-moyenne) ease, opacity var(--duree-courte) ease;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.gdi-title-inline__cat:empty { display: none; }

/* SUR L'ÉCRAN LISTE, le bandeau porte en plus le filtre et la pastille
   d'affichage : le titre doit se serrer bien davantage. Un plafond unique,
   calé sur ce cas-là, tronquerait « RÉGLAGES » et « MON COMPTE » pour rien —
   vu à l'écran le 2026-08-17, après avoir cru bien faire en le durcissant. */
body.bandeau-charge .gdi-title-inline { max-width: calc(100% - 306px); }

/* SOUS 380 px, le titre du bandeau s'efface — il ne reste PAS tronqué à vingt
   pixels. À cette largeur, quatre commandes et un titre centré ne tiennent
   simplement pas ensemble : le plafond calculé plus haut tomberait à 14 px, et
   « POKÉMON SOUS CLOCHE » deviendrait « P… », ce qui n'informe personne tout en
   occupant la place. C'est le titre qui cède, et c'est le bon choix : on vient
   d'ouvrir la liste, on sait laquelle c'est — alors que l'état du filtre, lui,
   explique pourquoi elle paraît incomplète, et le retour à l'accueil est le
   raccourci qu'on est venu chercher. Le grand titre, en haut de la page, porte
   toujours le nom en entier. */
@media (max-width: 380px) { body.bandeau-charge .gdi-title-inline { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .gdi-header, .gdi-title-inline__nom, .gdi-title-inline__cat { transition: none; }
}

/* Retiré de l'écran, gardé dans la page : c'est le <h1>, un lecteur d'écran
   doit pouvoir l'annoncer. Le nom visible, lui, est dans la barre. */
.gdi-title-large {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  padding: 0;
  border: 0;
}

/* ⚠️ L'EN-TÊTE QUI SE COMPRIME AU DÉFILEMENT reste À FAIRE.
   Demandé deux fois par Adrien (« le header est toujours aussi énorme et
   inutile quand scroll »), tenté le 2026-08-14, et RETIRÉ le jour même : ma
   version passait l'en-tête en `position: fixed` avec la hauteur réservée par
   `body`, et les tests l'ont mise en pièces — l'en-tête ne maigrissait pas,
   du contenu se cachait dessous, et la page sautait de 17 px en plein
   défilement. Trois éléments collants doivent rester d'accord ici (en-tête,
   barre chercher/filtrer, en-têtes de série) : ce n'est pas un endroit où
   improviser. À reprendre sur la base de l'avis HIG demandé le même jour. */

/* Sur l'accueil, le grand logo central porte déjà l'identité :
   l'en-tête est masqué pour ne pas afficher deux fois le même titre. */
/* L'accueil masque l'en-tête au regard, mais son titre reste le <h1> de la
   page : on le sort de l'écran plutôt que de le supprimer, sinon un lecteur
   d'écran n'a plus aucun titre de niveau 1 à annoncer. */
body.is-home .gdi-header { display: none; }

/* `.list-pied` (« Tout cocher » répété en bas de liste) retiré le 2026-08-14 :
   l'action a rejoint la rangée d'en-tête, où elle est à côté de la progression
   qu'elle fait bouger. La règle restait sans personne pour la porter. */
.btn--discret {
  background: transparent;
  border: 1px solid var(--c-bloc-piste);
  color: var(--c-texte-doux);
}
.btn--discret:hover { background: var(--c-survol); color: var(--c-texte); }

/* ⚠️ CE SÉLECTEUR AVAIT PERDU SON PRÉFIXE `body.is-home`, et la règle rognait
   donc le titre à 1 px SUR TOUS LES ÉCRANS — « pas de titre, il y a un trou »
   (Adrien, 2026-08-14, deux fois). Cause : une substitution de texte en aveugle
   qui a inséré un bloc AU MILIEU du sélecteur `body.is-home .gdi-title-large`,
   laissant le préfixe orphelin sur la règle voisine. Leçon : ne jamais insérer
   du CSS par `replace` sur un nom de classe — un même nom apparaît dans
   plusieurs sélecteurs, et couper un sélecteur en deux ne produit AUCUNE erreur,
   juste une règle qui s'applique là où elle ne devait pas. */
body.is-home .gdi-title-large {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  padding: 0;
  border: 0;
}

/* Le lien d'évitement doit lui aussi respecter la cible tactile minimale. */
.skip-link { min-height: var(--cible-tactile); display: inline-flex; align-items: center; }

.gdi-title__main {
  display: block;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-titre-haut);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gdi-title__script {
  display: block;
  font-family: var(--police-manuscrite);
  font-size: var(--taille-script-haut);
  /* La manuscrite déborde sous la ligne de base : sans cette marge négative
     elle s'éloigne trop du titre qu'elle accompagne. */
  margin-top: -2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.icon-btn {
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: background var(--duree-courte) ease, transform 0.1s ease;
}
.icon-btn svg { width: 26px; height: 26px; }
.icon-btn:hover { background: var(--c-survol); }
.icon-btn:active { transform: scale(0.92); }
.icon-btn[hidden] { display: none; }

/* ==========================================================================
   6. STRUCTURE
   ========================================================================== */

/* La barre étant FIXE, c'est ici qu'on réserve sa place. Une CONSTANTE : jamais
   mesurée, jamais réécrite, jamais en retard d'une frame. C'est la garantie
   « rien de caché à l'arrivée, jamais de saut ». */
body:not(.is-home) { padding-top: var(--entete-plein); }

/* Le bandeau maigrit au défilement — et LUI SEUL bouge : la place réservée
   ci-dessus ne change pas, donc rien ne se déplace sous le doigt. */
body.entete-compact { --hauteur-entete: var(--entete-compact); }
body.entete-compact .gdi-header { height: var(--entete-compact); padding-bottom: 10px; }
body.entete-compact .gdi-title-inline__nom { font-size: 14px; letter-spacing: 0.12em; }
body.entete-compact .gdi-title-inline__cat { font-size: 0; opacity: 0; }

.gdi-main {
  /* Au-dessus des couches de décor (z-index 0), sinon le contenu passerait
     dessous et la vignette assombrirait le texte. */
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--largeur-max);
  margin: 0 auto;
  padding: var(--esp-lg) 16px 12px;
}
.gdi-main:focus { outline: none; }

.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 var(--esp-md);
}
.section-head h2 {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-titre);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  margin: 0;
}
.section-head .count {
  color: var(--c-texte-doux);
  font-size: 14px;
  font-weight: var(--gras);
  white-space: nowrap;
}

.section-head + .list-progress { margin: -6px 0 18px; }

/* Fil d'Ariane : rappelle la catégorie. Sans lui, « BOOSTERS » en Vide et
   « BOOSTERS » en Scellé donnent deux écrans identiques, et on coche dans la
   mauvaise collection sans s'en apercevoir. */

/* ==========================================================================
   7. ACCUEIL
   ========================================================================== */

/* Barre d'actions de l'accueil. L'accueil est le seul écran sans bandeau
   supérieur : c'est donc le seul endroit où ce bouton ne heurte pas la charte. */
.home-topbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--esp-sm);
}

.theme-toggle {
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  gap: var(--esp-sm);
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
  border-radius: var(--r-plein);
  color: var(--c-texte);
  padding: var(--esp-sm) 16px;
  font-size: 12px;
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: border-color var(--duree-courte) ease, background var(--duree-courte) ease;
}
.theme-toggle:hover { border-color: var(--c-texte); background: var(--c-surface-haute); }
.theme-toggle svg { width: 18px; height: 18px; flex: none; }

/* ---------------------------------------------------------------------------
   L'AFFICHE — la page de garde du guide

   L'accueil s'ouvrait sur du texte seul : correct, et parfaitement muet. Rien
   ne disait ce qu'il y a DEDANS. L'affiche le dit en une image.

   Trois décisions valent d'être écrites, parce qu'elles se défont facilement :

   1. ELLE VA D'UN BORD À L'AUTRE. Les marges négatives annulent le rembourrage
      de `.gdi-main`. Une image d'accroche qui respecte la gouttière se lit
      comme une carte posée sur la page, pas comme une couverture.

   2. SA HAUTEUR EST LA PLUS PETITE DE DEUX MESURES : son rapport de forme, et
      46 % de l'écran. Le rapport seul donnerait 720 px de haut sur un grand
      écran ; les 46 % seuls écraseraient l'image sur un téléphone posé en
      paysage. `object-fit: cover` recadre, il ne déforme jamais.
      C'est aussi ce qui rend le FICHIER indifférent à son propre rapport : on
      peut remplacer la mosaïque par une illustration 3:2 sans toucher au CSS.

   3. LE BAS SE DISSOUT DANS LA PAGE. Sans ce dégradé, l'image s'arrête net sur
      une ligne horizontale — l'effet « bannière collée », qui date une page
      plus sûrement que n'importe quel choix de couleur. Le dégradé finit
      EXACTEMENT sur `--c-fond`, donc la couture est invisible dans les deux
      thèmes.
   --------------------------------------------------------------------------- */

.home-affiche {
  position: relative;
  isolation: isolate;
  margin: calc(var(--esp-lg) * -1) -16px var(--esp-md);
  overflow: hidden;

  /* La hauteur n'est PAS un rapport de forme figé, et c'est délibéré.
     Un `aspect-ratio` impose une boîte : dès que la phrase d'accroche tient sur
     quatre lignes au lieu de trois — écran étroit, texte traduit, police
     agrandie par le système — elle déborde de l'image ou se fait rogner. Ici la
     boîte a un PLANCHER et le contenu peut la pousser :
       · 96vw = ~374 px sur un iPhone courant, soit 55 % de l'écran. MESURÉ :
         en portrait c'est TOUJOURS ce terme qui gagne, de 320 à 430 px de
         large ;
       · 58vh est un garde-fou, pas un réglage. Le baisser « pour le téléphone
         couché » a été essayé : ça rabotait aussi l'iPhone debout. Le téléphone
         couché est donc traité par une requête de HAUTEUR, plus bas, et pas en
         abaissant cette valeur-ci.
     ⚠️ C'est le PLANCHER qui commande, pas le rembourrage : augmenter la marge
     sous la phrase sans toucher à cette ligne ne fait rien grandir du tout —
     le vide est simplement absorbé. Les deux vont ensemble.
     `object-fit: cover` recadre l'image dans ce que la boîte fait réellement,
     sans jamais la déformer — donc le FICHIER est indifférent à tout ça. */
  /* ⚠️ LA HAUTEUR VIT DANS UN JETON, parce qu'un SECOND élément en dépend :
     le tapis d'items (`.home-tapis`) répète cette même image à cette même
     échelle sur toute la page. Deux valeurs à tenir d'accord = deux valeurs
     qui divergent ; ici, la divergence se verrait tout de suite (des items de
     deux tailles bord à bord). */
  min-height: var(--affiche-h);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Le rembourrage bas est GÉNÉREUX, et c'est le réglage qui fait respirer
     toute la page de garde : « pas mal de marge en dessous du sous-titre pour
     que l'image prenne encore plus de place et de manière harmonieuse »
     (Adrien, 2026-08-15). Ce vide n'est pas perdu — c'est là que le dégradé
     finit de dissoudre l'image dans la page, donc plus il est haut, plus la
     couture est douce. */
  padding: var(--esp-xl) 16px 46px;
}

/**
 * LE TAPIS D'ITEMS — l'affiche continue sur TOUTE la page, le temps de l'arrivée.
 *
 * *« Il y a en haut des items en fond, et ils pourraient être sur toute la page
 * le temps que le reste charge ? Même taille etc, juste rajouter des items »*
 * (Adrien, 2026-08-18).
 *
 * C'est la même image que l'affiche, à la MÊME ÉCHELLE (d'où `--affiche-h`,
 * partagé), répétée jusqu'en bas. Pendant l'arrivée, la page n'est donc plus
 * « une affiche en haut, du vide en dessous » : c'est un mur d'items d'un bord
 * à l'autre. Puis il s'efface et la page redevient elle-même.
 *
 * ⚠️ `position: fixed` et NON `absolute` : il doit couvrir l'écran, pas le
 * document. Sur l'accueil, le document fait plusieurs milliers de pixels de
 * haut — en `absolute`, on répéterait l'image des dizaines de fois pour un
 * décor qui ne vit qu'une seconde, et qui n'est jamais vu ailleurs qu'en haut.
 *
 * ⚠️ IL EST DERRIÈRE L'AFFICHE, ET C'EST CE QUI SUPPRIME TOUTE COUTURE. L'affiche
 * se dissout vers le bas (son voile finit transparent) ; ce qu'elle découvre
 * n'est plus la page nue mais la SUITE des items, à la même taille. Le raccord
 * n'existe plus : c'est la même image qui continue.
 */
.home-tapis {
  position: fixed;
  inset: 0;
  /* ⚠️ AU-DESSUS DE `main` (z-index 1), ET C'EST INDISPENSABLE — pas un détail
     d'empilement. Placé DESSOUS, son dégradé n'atteignait pas l'affiche : elle
     est opaque sur les 44 % du haut, donc la pente ne commençait qu'une fois
     l'affiche finie. On retombait exactement sur le défaut d'Adrien, « le
     dégradé au milieu qui ne va pas jusqu'en bas », par l'autre bout.
     Le rendre visible par-dessus l'affiche est sans risque parce que le tapis
     EST l'affiche : même image, même échelle (`--affiche-h`), même cadrage
     (`center top`). Sa première tuile se superpose au pixel à ce que l'affiche
     dessine — on ne recouvre rien, on prolonge.
     Pendant l'arrivée, le reste de l'accueil est de toute façon masqué ; et le
     tapis s'efface pendant que la page monte, donc il ne reste jamais devant du
     texte lisible. */
  z-index: 2;
  pointer-events: none;
  /* ⚠️ LE DÉGRADÉ DESCEND JUSQU'EN BAS DE L'ÉCRAN, ET C'EST TOUT LE POINT.
     *« Ta première image avec tous les items sur toute la page, avec le dégradé
     au milieu qui ne va pas jusqu'en bas, c'est moche »* (Adrien, 2026-08-18) —
     et c'était vrai : le voile de l'affiche s'arrêtait au bas de SA boîte, à
     44 % de l'écran. On lisait donc « des items clairs, une bande sombre en
     travers, des items clairs » — une ombre qui flotte au milieu de rien.

     Le voile ne peut pas vivre sur l'affiche tant que le tapis est là : sa
     boîte s'arrête, l'écran non. Il vit donc ICI, sur toute la hauteur, et
     l'affiche rend le sien le temps de l'arrivée (voir plus bas). Une seule
     peinture, une seule pente, du haut de l'écran jusqu'en bas.

     Les repères reprennent le profil de l'affiche, réexprimés en % d'écran
     (sa boîte fait 44 % de la hauteur) : 0,08 en haut, puis la montée, puis la
     poursuite jusqu'à 0,92 en bas. Il s'arrête à 0,92 et non à 1 : à 1 les
     items du bas disparaîtraient, et « intégralement recouverte d'items » ne
     voudrait plus rien dire. ⚠️ Il a d'abord fini à 0,92 : mesuré, il ne restait
     alors que **12 % du bas de l'écran portant des items** — la page se lisait
     « sombre avec un motif », plus « couverte d'items ». Un contrôle sur les
     vrais pixels tient cette limite. */
  background-image:
    linear-gradient(to bottom,
      rgba(var(--affiche-teinte), 0.08) 0%,
      rgba(var(--affiche-teinte), 0.16) 9%,
      rgba(var(--affiche-teinte), 0.46) 17%,
      rgba(var(--affiche-teinte), 0.66) 26%,
      rgba(var(--affiche-teinte), 0.74) 45%,
      rgba(var(--affiche-teinte), 0.82) 100%),
    url('../assets/affiche-accueil.webp');
  /* Le voile ne se répète pas — il couvre l'écran une fois. Les items, si. */
  background-repeat: no-repeat, repeat;
  /* `auto` sur la largeur des items : c'est le rapport de l'image qui décide,
     exactement comme le `cover` de l'affiche. Poser une largeur ici déformerait
     les items d'un côté et pas de l'autre. */
  background-size: 100% 100%, auto var(--affiche-h);
  background-position: center top, center top;
}


/* Il s'efface dès que la page se construit — « puis le reste arrive et la page
   devient normale » (Adrien). Il part AVANT que le texte ne monte : les blocs
   de l'accueil ont 160 ms de retard au minimum, le tapis est alors déjà à un
   cinquième de sa force, et aucun texte ne se pose sur une photo en pleine
   lumière. */
.accueil-se-revele .home-tapis {
  /* ⚠️ IL PART VITE, ET C'EST STRUCTUREL : le tapis est AU-DESSUS de `main`.
     Tant qu'il est opaque, le contenu qui monte derrière lui est invisible —
     un fondu lent ici recréerait exactement le « mou » qu'Adrien a signalé,
     mais par en haut. 300 ms : à mi-parcours le contenu en est à son premier
     tiers, et la page se construit à travers un voile qui s'évapore. */
  animation: tapis-part 300ms both cubic-bezier(0.4, 0, 0.6, 1);
}

@keyframes tapis-part {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Une fois l'arrivée passée, il n'a plus lieu d'être : la page a son affiche.
   Sans cette règle il resterait à l'écran, en `fixed`, sous toutes les listes. */
html:not(.intro-en-cours):not(.accueil-se-revele) .home-tapis { display: none; }

.home-affiche__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  z-index: -1;
  /* ⚠️ L'IMAGE DOIT AVOIR DISPARU AVANT QUE LE VOILE NE S'EFFACE. Le voile
     descend maintenant jusqu'à la transparence (voir ci-dessous) : sans ce
     masque, la photo REPARAÎTRAIT dans les derniers pourcents, à l'endroit
     précis où l'on veut ne plus rien voir. Les deux bornes sont accordées à la
     main : à 88 % le voile vaut encore 0,97, donc ce qui reste de photo est
     sous les 3 % — le même ordre de grandeur qu'avant ce changement. Rien ne
     bouge au-dessus de 80 % : l'image est exactement celle d'avant. */
  -webkit-mask-image: linear-gradient(to bottom, #000 80%, transparent 92%);
  mask-image: linear-gradient(to bottom, #000 80%, transparent 92%);
}

/* Le voile fait DEUX choses à la fois, et c'est voulu : il assombrit assez le
   bas pour que le titre ET la phrase tiennent le contraste, et il DISSOUT
   l'image dans la page.
   ⚠️ IL FINIT SUR DU TRANSPARENT, PLUS SUR `--c-fond`. Terminer sur une
   couleur, c'était IMITER le fond de la page ; et l'imitation était fausse
   (le fond y peint un dégradé, des faisceaux et une vignette, pas un aplat) —
   d'où la ligne horizontale visible qu'Adrien a photographiée. En finissant
   sur du transparent, l'affiche ne raccorde plus rien : elle laisse voir. Ce
   qui est dessous devient juste visible, quel qu'il soit. */
.home-affiche__voile {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to bottom,
    rgba(var(--affiche-teinte), 0.08) 0%,
    rgba(var(--affiche-teinte), 0.18) 26%,
    rgba(var(--affiche-teinte), var(--affiche-densite)) 56%,
    rgba(var(--affiche-teinte), 0.97) 88%,
    rgba(var(--affiche-teinte), 0) 100%
  );
}

/* Le titre suit son thème, et le voile se densifie sous lui pour lui donner de
   quoi tenir. La première version le laissait blanc dans les deux thèmes, « parce
   que c'est une image » : mesuré à l'écran, il tombait à 2,3:1 en thème jour. */
.home-affiche__titre {
  text-align: center;
  color: var(--affiche-titre);
  text-shadow: var(--affiche-ombre);
}

/* ÉCRAN COURT — le téléphone couché. Ce n'est pas un point de rupture de
   LARGEUR (le projet n'en admet que quatre) : c'est la hauteur qui manque, et
   c'est le titre, dimensionné en `vw`, qui devient énorme dès que l'écran
   s'allonge. Sans ça l'affiche mangeait 65 % d'un écran de 390 px de haut,
   mesuré. On réduit le texte, on ne le retire pas. */
@media (max-height: 520px) {
  /* ⚠️ LE JETON SE REDÉFINIT SUR `:root`, PAS SUR `.home-affiche`. Le tapis
     d'items est son FRÈRE, pas son enfant : posé sur l'affiche, le jeton ne
     l'atteindrait jamais, et on se retrouverait avec des items de deux tailles
     bord à bord sur téléphone couché. */
  :root { --affiche-h: min(42vh, 86vw); }
  .home-affiche { padding: var(--esp-md) 16px 22px; }
  .home-affiche .home-hero__logo { font-size: clamp(26px, 6.5vh, 40px); }
  .home-affiche .home-hero__script { font-size: clamp(19px, 4.6vh, 28px); }
  .home-affiche .home-hero__tag { font-size: 12px; margin-top: 10px; max-width: 56ch; }
}

/* Le bouton de thème passe SUR l'affiche : il gagne sa place d'origine, et
   l'accueil récupère la rangée entière qu'il occupait. */
.home-affiche .home-topbar {
  position: absolute;
  top: 10px;
  right: 10px;
  margin: 0;
  z-index: 1;
}

.home-hero { text-align: center; }
.home-hero__logo {
  margin: 0;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-logo);
  line-height: 0.95;
  letter-spacing: var(--lettres-affichage);
  margin: 0;
}
.home-hero__script {
  font-family: var(--police-manuscrite);
  font-size: var(--taille-logo-script);
  line-height: 1;
  margin: -4px 0 0;
}
/* La phrase d'accroche est DANS l'image depuis 2026-08-15 : « le sous-titre
   pourrait être inclus dans la partie imagée pour que la partie imagée soit
   plus grande encore en hauteur » (Adrien). Elle y gagne deux fois — l'image
   grandit de moitié sans rien pousser vers le bas, et la page de garde devient
   un bloc entier au lieu d'une bannière suivie d'un paragraphe.
   Sa couleur n'est donc plus celle du texte de la page : c'est celle du titre,
   légèrement adoucie, parce que son fond est la photo et non la page. */
.home-affiche .home-hero__tag {
  color: inherit;
  opacity: 0.92;
  font-size: var(--taille-texte);
  /* Le titre et la phrase sont deux voix, pas un bloc : 22 px les séparent, là
     où « LE GUIDE » et « des items » n'en ont que 4 parce qu'ils sont, eux, un
     seul mot dessiné en deux lignes. */
  margin: 22px auto 0;
  max-width: 34ch;
  line-height: 1.45;
}

.home-search { margin-bottom: var(--esp-lg); }

/* ---------------------------------------------------------------------------
   LES ZONES DE L'ACCUEIL — refonte du 2026-08-19
   ---------------------------------------------------------------------------
   « La page ici est trop fouillis, on n'a pas assez de netteté de séparation.
   Tout est dans le même ton serré alors que ce sont des éléments bien
   différents » (Adrien, capture à l'appui). Diagnostic juste, et la cause
   n'était pas le nombre de blocs — c'était que RIEN ne les séparait :

   1. l'espace entre deux zones (22 px) valait à peine plus que l'espace entre
      un titre et son propre contenu (10 px). Un rythme sans contraste ne
      groupe rien : tout se lit comme une seule coulée ;
   2. les titres de zone étaient plus DISCRETS que ce qu'ils coiffaient —
      12 px gris pâle au-dessus de cartes en Oswald blanc 18 px. L'œil trouvait
      donc les cartes, jamais la structure ;
   3. tout portait la même surface, le même filet, le même arrondi — alors
      qu'une liste à cocher, un raccourci et un article à lire ne sont pas la
      même chose.

   Les trois corrections tiennent en deux principes, et n'ajoutent AUCUNE
   couleur (la charte tient en deux thèmes entiers et un jaune réservé au
   cochage) : un rythme franchement contrasté, et une surface par NATURE.
   --------------------------------------------------------------------------- */

/* GÉNÉREUX entre deux zones, SERRÉ entre un titre et son contenu.
   30 px contre 8 : c'est le rapport qui fait qu'on voit des groupes. */
.home-section { margin: var(--esp-xl) 0 0; }

/* LE TITRE DE ZONE EST UN SÉPARATEUR, pas une étiquette.
   Oswald capitales, pleine couleur, et un filet qui court jusqu'au bord :
   c'est la façon la moins coûteuse de dire « nouvelle zone » — pas de boîte
   de plus, pas de couleur de plus, et ça marche dans les deux thèmes. */
.home-section__title {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte);
  margin: 0 0 var(--esp-sm);
}

.home-section__title::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--c-trait);
}

/* ---------------------------------------------------------------------------
   LES DEUX DÉPLIANTS DE L'ACCUEIL — « vide » et « scellé »
   ---------------------------------------------------------------------------
   « Le site principal de Mathieu, c'est les objets autres que le TCG » (via
   Adrien, 2026-08-15). Ces deux catégories pèsent 36 listes et 8 010 items,
   contre 6 listes et 1 966 items pour « divers » — le vrai sujet du guide.
   À plat, le sujet passait après six fois son volume.
   Repliés, ils tiennent DEUX LIGNES à eux deux, et restent à un geste.
   La tête ressemble volontairement à une ligne de liste (même hauteur, même
   fond) : c'est le même geste, ouvrir quelque chose — la seule différence est
   le chevron, qui pivote au lieu de pointer vers un autre écran.
   --------------------------------------------------------------------------- */
.home-groupe { margin-top: var(--esp-md); }

.home-groupe__tete {
  width: 100%;
  min-height: var(--cible-tactile);
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  padding: 12px var(--esp-md);
  background: var(--c-bloc-fond);
  color: var(--c-bloc-texte);
  border: 1px solid var(--c-bloc-bordure);
  border-radius: var(--r-md);
  cursor: pointer;
  text-align: left;
}
.home-groupe__tete:hover { background: var(--c-bloc-survol); }

/* Le chevron pointe à droite (fermé) et vers le bas (ouvert) : la rotation dit
   « ça se déplie ici », là où une flèche fixe dirait « ça mène ailleurs ». */
.home-groupe__chevron {
  flex: none;
  font-size: 20px;
  line-height: 1;
  color: var(--c-texte-doux);
  transition: transform var(--duree-courte) ease;
}
.home-groupe.est-ouvert .home-groupe__chevron { transform: rotate(90deg); }

.home-groupe__nom {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  text-transform: uppercase;
  letter-spacing: var(--lettres-capitales);
}
.home-groupe__compte {
  flex: none;
  font-size: var(--taille-petit);
  color: var(--c-bloc-doux);
  font-variant-numeric: tabular-nums;
}

/* La carte dépliée se cale SOUS la tête, sans espace : les deux se lisent
   comme un seul bloc, et on voit tout de suite à quoi ces listes appartiennent. */
.home-groupe__corps { margin-top: 6px; }

@media (prefers-reduced-motion: reduce) {
  .home-groupe__chevron { transition: none; }
}

/* Les cartes de catégorie (`.cat-grid` / `.cat-card`) ont été retirées le
   2026-08-14 : l'accueil montre désormais TOUTES les listes au même niveau.
   Elles n'étaient plus émises par aucun écran — du CSS qui ne vise rien laisse
   croire qu'un composant existe encore, et c'est ce qui fait perdre du temps
   au relecteur suivant. L'écran d'une catégorie (`#/c/vide`) existe toujours et
   utilise les lignes ordinaires (`.menu-row`), pas ces cartes. */

/* ==========================================================================
   9. MENU D'UNE CATÉGORIE — le bloc plein des maquettes
   ========================================================================== */

.menu-card {
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  border-radius: var(--r-md);
  padding: 6px;
  box-shadow: var(--ombre);
}

.menu-row {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  width: 100%;
  min-height: var(--cible-tactile);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--c-bloc-trait);
  padding: 16px var(--esp-md);
  cursor: pointer;
  text-align: left;
  color: var(--c-bloc-texte);
  transition: background 0.12s ease;
}
.menu-row:last-child { border-bottom: 0; }
.menu-row:hover { background: var(--c-bloc-survol); }
.menu-row:active { background: var(--c-bloc-piste); }

/* Nom + étiquette de catégorie sur la même ligne. L'étiquette n'existe que sur
   l'accueil, où toutes les listes sont à plat : « BOOSTERS » y apparaît deux
   fois (vide et scellé) et rien d'autre ne les distingue. */
.menu-row__titre { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.menu-row__cat {
  flex: none;
  font-family: var(--police-affichage);
  font-size: var(--taille-minuscule);
  font-weight: var(--tres-gras);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-bloc-doux);
  padding: 2px 7px;
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-trait);
}

.menu-row__name {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-liste);
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
  flex: 1;
  min-width: 0;
  line-height: 1.1;
}
.menu-row__meta {
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-bloc-doux);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.menu-row__ring {
  --p: 0;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  /* --c-jauge et non --c-coche-sombre : le jour, le jaune sur gris clair
     tombe à 1,02:1 — l'anneau rempli devenait invisible. --c-jauge est le
     jeton qui porte déjà la progression de la BARRE et qui bascule en bleu
     marine en thème clair. Les deux indicateurs suivent donc la même règle. */
  background: conic-gradient(var(--c-jauge) calc(var(--p) * 1%), var(--c-bloc-piste) 0);
  display: grid;
  place-items: center;
}
.menu-row__ring::after {
  content: '';
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--c-bloc-fond);
}
.menu-row__ring[data-done="1"] { background: var(--c-coche-sombre); }
.menu-row__ring[data-done="1"]::after {
  content: '✓';
  background: transparent;
  color: var(--c-coche-marque);
  font-weight: 900;
  font-size: 18px;
  display: grid;
  place-items: center;
  width: auto;
  height: auto;
}

/* ==========================================================================
   10. RECHERCHE
   ========================================================================== */

.list-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 18px; flex-wrap: wrap; }

/* --- Choix d'affichage : liste ou photos ---------------------------------- */
/* Reprend TEL QUEL le sélecteur de tri de « Ma collection » (`.collec-tri`) :
   le guide a déjà une façon d'exprimer « choisis une option parmi plusieurs »,
   en inventer une seconde ferait deux langages pour une même idée.

   Deux boutons plutôt qu'une bascule unique : un bouton seul étiqueté
   « Photos » ne dit pas s'il décrit ce qu'on voit ou ce qu'on obtiendra. Ici,
   les deux options sont visibles et l'active est marquée — plus rien à
   deviner, et un seul appui pour aller à l'autre. */
/* « Où j'en suis » et « comment je regarde » sur UNE rangée : ni l'une ni
   l'autre ne remplit la largeur, les empiler gaspillait une ligne entière
   au-dessus du contenu. */
/* --- Barre de progression -------------------------------------------------
   Elle n'avait AUCUNE règle CSS. `progressBar()` produit .progress,
   .progress__bar, .progress__fill et .progress__val depuis le premier jour :
   les quatre classes étaient absentes de cette feuille. Ce qu'on prenait pour
   une barre n'était donc qu'un « 2/722 » en texte nu — l'ancre visuelle de
   l'écran n'existait pas, ce qui explique une bonne part du « on comprend pas
   l'organisation ». Trouvé par l'audit design du 2026-08-14, pas à l'œil. */
.progress { display: flex; align-items: center; gap: var(--esp-sm); }
.progress__bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 6px;
  border-radius: var(--r-plein);
  background: var(--c-bloc-piste);
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  width: 100%;
  /* `scaleX` posé en style en ligne par progressBar() : on transforme, on ne
     ré-agence pas — c'est ce qui rend l'animation gratuite pour le navigateur. */
  transform-origin: left center;
  border-radius: inherit;
  /* Le JAUNE est ici à sa place, et à une seule condition : cette barre mesure
     ce qu'on POSSÈDE. C'est le même sens que la case cochée. */
  background: var(--c-coche);
  transition: transform var(--duree-moyenne) var(--courbe);
}
.progress__val {
  flex: none;
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
  .progress__fill { transition: none; }
}

.list-entete {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  margin-bottom: var(--esp-sm);
}
/* « Tout cocher » ne doit JAMAIS revenir à la ligne ni rétrécir : c'est
   l'action de la rangée, la barre de progression à côté d'elle est
   compressible, pas lui. Sans `flex: none`, un nom de liste long poussait le
   bouton sur deux lignes et faisait grandir toute la rangée. */
.list-entete__tout { flex: none; white-space: nowrap; }
.list-entete .list-progress { flex: 1 1 auto; min-width: 0; margin: 0; }

/* PAS DE JAUNE ICI, et c'est une règle, pas un goût.
   -------------------------------------------------------------------------
   Adrien, en entourant la zone : « la hiérarchie est très mauvaise pour l'œil
   humain, on comprend pas du tout l'organisation, y'en a dans tous les sens ».
   La cause était identifiable : QUATRE jaunes différents cohabitaient dans un
   demi-écran — l'affichage actif, le filtre actif, la phrase d'état, et les
   cases cochées. Or le jaune du guide veut dire UNE chose : « je l'ai ». À
   force de servir aussi à « option choisie », il ne voulait plus rien dire.
   Une option sélectionnée se marque donc en neutre — pleine, contrastée,
   parfaitement lisible — et le jaune reste au cochage, seul. */

.search { position: relative; flex: 1 1 220px; min-width: 0; }
.search input {
  width: 100%;
  min-height: var(--cible-tactile);
  background: var(--c-surface);
  /* Le contour d'un champ de saisie n'est pas décoratif : c'est lui qui dit
     « on écrit ici ». Il lui faut donc son propre jeton, plus contrasté que
     celui des simples conteneurs. */
  /* 2 px EN PERMANENCE, seule la couleur change au focus : à 1 px l'indicateur
     de focus clavier passe sous le seuil WCAG 2.4.11, et l'épaissir seulement
     au focus fait sauter la mise en page. Correctif déjà appliqué au champ de
     recherche, jamais reporté ici (audit 2026-08-14). */
  border: 2px solid var(--c-champ-trait);
  color: var(--c-texte);
  border-radius: var(--r-plein);
  padding: 13px 16px 13px 42px;
  /* Les champs de saisie n'héritent PAS de la police de la page : sans cette
     ligne, le seul texte que l'utilisateur écrit lui-même s'affichait en
     Arial, hors charte, sur les trois écrans qui portent une recherche. */
  font-family: inherit;
  font-size: 16px;   /* en dessous de 16px, iOS zoome tout seul à la saisie */
  outline: none;
}
.search input::placeholder { color: var(--c-texte-faible); }
/* Le contour passe à 2 px EN PERMANENCE, et seule la couleur change au focus :
   à 1 px, l'indicateur de focus clavier était sous le minimum exigé (WCAG
   2.4.11) ; en l'épaississant seulement au focus, la mise en page sautait. */
.search input { border-width: 2px; }
.search input:focus { border-color: var(--c-focus); }
.search svg {
  position: absolute;
  left: var(--esp-md);
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  color: var(--c-texte-faible);
  pointer-events: none;
}

.list-hint {
  color: var(--c-texte-doux);
  font-size: 12.5px;
  margin: -8px 0 18px;
}

.search-count {
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 10px;
}
.search-hint { color: var(--c-texte-doux); font-size: var(--taille-petit); line-height: 1.5; margin: 20px 0 12px; }

.chips { display: flex; flex-wrap: wrap; gap: var(--esp-sm); }
/* Les quatre listes toutes prêtes, sous le champ « nouvelle liste ». Elles
   vivent DANS la carte des listes : la marge intérieure les aligne sur les
   lignes au-dessus, sinon elles ont l'air posées à côté de la carte. */
.chips--propositions { padding: 0 var(--esp-md) var(--esp-md); }
/* LE RACCOURCI EST EN CONTOUR, jamais en surface pleine.
   « Reprendre » ne contient rien : ce sont des portes vers des listes déjà
   ouvertes. Avec le même fond que les cartes, ces portes pesaient autant que
   le contenu qu'elles désignent. Un contour dit « passage » ; une surface dit
   « objet ». */
.chip {
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  gap: var(--esp-sm);
  background: transparent;
  border: 1px solid var(--c-bordure);
  border-radius: var(--r-plein);
  color: var(--c-texte);
  padding: var(--esp-sm) 16px;
  font-size: var(--taille-petit);
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--duree-courte) ease, background var(--duree-courte) ease;
}
.chip:hover { border-color: var(--c-texte); background: var(--c-survol); }

/* ==========================================================================
   11. SÉRIES ET ITEMS
   ========================================================================== */

/* LE NAVIGATEUR NE CALCULE QUE CE QU'IL AFFICHE.
   Mesuré par l'audit du 2026-08-14 : ouvrir `vide-boosters` (1 330 items,
   154 séries) prend ~870 ms sur iPhone bridé, dont 632 ms de seul calcul de
   mise en page. Avec cette ligne, ~315 ms — soit −65 %, sans virtualisation ni
   JavaScript. `contain-intrinsic-size` réserve une hauteur plausible pour que
   la barre de défilement ne saute pas. Ignoré sans effet par les navigateurs
   qui ne le connaissent pas. */
.group { margin-bottom: var(--esp-xl); }

/* ⚠️ LA LIGNE EST SUR `.items`, PAS SUR `.group` — ET C'EST OBLIGATOIRE.
   `content-visibility: auto` implique `contain: paint`, qui INTERDIT à un
   enfant de peindre hors du cadre de son parent. Posée sur `.group`, elle
   rognait donc le fond de l'en-tête de série exactement à la marge du contenu,
   annulant les `margin-left/right: -16px` qui le font aller jusqu'aux bords :
   « la carte fixée SÉRIE 1 / tout décocher n'est pas pleine largeur, du coup sa
   couleur ne va pas jusqu'au bord de l'écran, c'est moche » (Adrien,
   2026-08-14, capture à l'appui). Défaut introduit le soir même par
   l'optimisation ci-dessus.
   Le contrôle « l'en-tête de série couvre toute la largeur » ne l'a PAS vu : il
   mesurait `getBoundingClientRect()`, que le rognage de peinture ne change pas.
   La boîte était bien pleine largeur — seule la COULEUR était coupée. Un
   second contrôle interroge maintenant le pixel lui-même.
   Sur `.items`, le gain reste entier : ce sont les centaines de lignes qui
   coûtent le calcul de mise en page, pas l'en-tête. Et rien, dans `.items`,
   n'a besoin de peindre en dehors. */
.items {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

.group__head {
  /* Reste visible pendant le défilement : sur une série de 300 jetons, on doit
     toujours savoir dans quelle série on se trouve. Dégradé plutôt qu'aplat,
     pour que le bord bas se fonde dans la page au lieu de la trancher. */
  position: sticky;
  /* `--hauteur-entete` est MESURÉE par js/ui/header.js — et `offsetHeight`
     comprend déjà la zone sûre de l'iPhone : ne pas la rajouter ici, on la
     compterait deux fois. `--hauteur-banniere` vaut 0 hors mode correction. */
  top: calc(var(--hauteur-entete) + var(--hauteur-banniere, 0px));
  z-index: var(--z-serie);
  background: linear-gradient(180deg,
    var(--c-entete-fond) 0%,
    var(--c-entete-fond) 72%,
    transparent 100%);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  /* JUSQU'AUX BORDS, comme la barre d'outils. Il s'arrêtait aux 16 px de marge
     du contenu : son fond laissait deux bandes vides à gauche et à droite, et
     les items défilaient dedans — « il y a du vide à gauche et à droite, c'est
     moche » (Adrien, 2026-08-14). On sort des marges, on les rend en marge
     intérieure pour que le texte reste aligné sur les items. */
  margin-left: -16px;
  margin-right: -16px;
  padding-left: 16px;
  padding-right: 16px;
  /* Marge de sécurité EN HAUT. Elle manquait : le bouton « Tout cocher » était
     collé au bord du bandeau collant, sans un pixel d'air — « c'est moche »
     (Adrien, 2026-08-14), et il avait raison. Le `padding-bottom` existait
     depuis toujours, le `padding-top` n'avait simplement jamais été écrit :
     personne ne le voyait tant que l'en-tête n'était pas collé sous un autre
     élément. Un contrôle vérifie désormais cet air, ici et ailleurs. */
  padding-top: 12px;
  padding-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: var(--esp-sm);
  border-bottom: 1px solid var(--c-trait);
}
.group__logo { width: 42px; height: 42px; flex: none; object-fit: contain; border-radius: var(--r-sm); }
.group__name {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
  margin: 0;
  flex: 1;
  min-width: 0;
}
.group__count {
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  font-variant-numeric: tabular-nums;
}
.group__all {
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: 1.5px solid var(--c-bouton-trait);
  color: var(--c-texte);
  border-radius: var(--r-plein);
  padding: var(--esp-sm) 16px;
  font-size: 12px;
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duree-courte) ease, border-color var(--duree-courte) ease;
}
.group__all:hover { background: var(--c-survol); border-color: var(--c-texte); }

/* --- Le verso de la série, en position 0 -------------------------------- */
/*
   Mathieu a photographié le DOS de chaque série de Tazo, « pour éviter que le
   collectionneur coche le mauvais » : à l'endroit, deux Tazo de deux séries
   se ressemblent ; à l'envers, le dos les sépare d'un coup d'œil.

   ⚠️ CE BLOC NE DOIT JAMAIS RESSEMBLER À UN ITEM. C'est un repère, pas un
   objet à posséder — et il est rendu HORS du conteneur `.items`, donc rien
   ici ne peut être coché (voir `js/core/versos.js`). Trois conséquences que
   l'on tient exprès :
     • aucun état de survol ni d'appui, et `cursor` reste celui du texte : ce
       qui réagit au doigt promet de faire quelque chose ;
     • aucune case à cocher, même dessinée — elle inviterait à taper dessus ;
     • pas une trace de JAUNE. Le jaune du guide ne veut dire qu'une chose,
       « je l'ai », et un verso n'est pas quelque chose qu'on a.
*/
.verso {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  margin: 0 0 var(--esp-md);
  padding: 10px 12px;
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  border-radius: var(--r-md);
}
.verso__img {
  width: 56px;
  height: 56px;
  flex: none;
  /* `contain` et non `cover` : c'est le motif ENTIER du dos qui identifie la
     série — recadré, il ne sert plus à rien. */
  object-fit: contain;
  border-radius: var(--r-sm);
  /* Le dos d'un Tazo est souvent clair : sans ce filet, l'image se confond
     avec le bloc en thème jour et flotte sans limite visible. */
  box-shadow: inset 0 0 0 1px var(--c-bloc-bordure);
}
.verso__legende {
  margin: 0;
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-bloc-doux);
}
.verso__legende strong {
  display: block;
  font-weight: var(--tres-gras);
  color: var(--c-bloc-texte);
}

/* --- Items en liste ---------------------------------------------------- */
.items { display: grid; gap: 2px; grid-template-columns: 1fr; }
@media (min-width: 640px) { .items { grid-template-columns: repeat(2, 1fr); gap: 2px var(--esp-lg); } }
@media (min-width: 980px) { .items { grid-template-columns: repeat(3, 1fr); } }

.item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: var(--cible-tactile);
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  padding: 11px 10px;
  cursor: pointer;
  text-align: left;
  color: var(--c-texte);
  transition: background 0.12s ease;
}
.item + .item { box-shadow: inset 0 1px 0 var(--c-trait-fin); }
.item:hover { background: var(--c-survol); }
.item:active { background: var(--c-appui); }

.item__box {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: var(--esp-xs);
  border: 2px solid var(--c-case-trait);
  background: transparent;
  display: grid;
  place-items: center;
  transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;
}
.item__box svg { width: 15px; height: 15px; color: var(--c-coche-marque); opacity: 0; transition: opacity 0.1s ease; }

.item[aria-checked="true"] .item__box {
  background: var(--c-coche);
  border-color: var(--c-coche-trait);
}
.item[aria-checked="true"] .item__box svg { opacity: 1; }
.item[aria-checked="true"] .item__name { color: var(--c-texte-doux); }

.item__num {
  /* --c-texte-doux et non --c-texte-faible : ce numéro se lit en plein soleil,
     en brocante. À 3,5:1, --c-texte-faible échoue au seuil WCAG AA. */
  font-size: 12px;
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  min-width: 22px;
  font-variant-numeric: tabular-nums;
  flex: none;
}
/* --- Vignette en vue liste ---------------------------------------------- */
/* 36 px et pas 22 (la hauteur du titre) : en dessous, une carte n'est plus
   identifiable, et une image qu'on ne peut pas lire occupe de la place sans
   rien apprendre. Le raisonnement complet est dans `js/ui/components.js`. */
.item__vignette {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: var(--r-sm);
  /* `contain` et non `cover` : une carte recadrée perdrait ses bords, or c'est
     souvent le bord qui distingue deux variantes d'un même visuel. */
  object-fit: contain;
  background: var(--c-survol);
  /* Le fond d'une photo détourée est blanc : sans ce trait, la vignette se
     confond avec la page en thème clair et flotte sans limite visible. */
  box-shadow: inset 0 0 0 1px var(--c-trait-fin);
}
/* Emplacement réservé quand l'item n'a pas encore de photo : il tient les noms
   alignés, et ne dessine RIEN — pas de cadre gris répété sur toute la liste. */
.item__vignette--vide { background: transparent; box-shadow: none; }

/* Un item coché recule, sa vignette avec lui. Décoratif : aucun texte ne perd
   de contraste ici. */
.item[aria-checked="true"] .item__vignette { opacity: 0.5; }

/* La ligne illustrée garde une hauteur raisonnable : 36 px de vignette + 6 px
   d'air en haut et en bas = 48 px, au-dessus de la cible tactile de 44 px. */
.item--illustre { padding-top: 6px; padding-bottom: 6px; }

.item__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.item__name {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-item);
  line-height: 1.35;
  flex: 1;
  min-width: 0;
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
}
.item__origin {
  font-family: var(--police-texte);
  font-size: var(--taille-minuscule);
  font-weight: var(--gras);
  color: var(--c-texte-doux);
  letter-spacing: 0.02em;
}
/* La catégorie en tête de provenance : c'est elle qui distingue deux items
   homonymes appartenant à des collections différentes. */
.item__origin-cat { color: var(--c-accent-texte); font-weight: var(--tres-gras); letter-spacing: 0.06em; }
.item__origin-cat::after { content: ' ·'; color: var(--c-texte-doux); }

/* --- Items en vignettes ------------------------------------------------ */
/* 3 colonnes dès le téléphone, comme sur les maquettes : sur une liste de
   300 jetons, cela divise la longueur de défilement par trois. */
.items--grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
@media (min-width: 560px) { .items--grid { grid-template-columns: repeat(4, 1fr); gap: var(--esp-md); } }
@media (min-width: 900px) { .items--grid { grid-template-columns: repeat(6, 1fr); gap: 16px; } }

.items--grid .item {
  flex-direction: column;
  gap: var(--esp-sm);
  padding: 10px var(--esp-sm) 12px;
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
  border-radius: var(--r-md);
  text-align: center;
}
.items--grid .item + .item { box-shadow: none; }
.items--grid .item__thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: var(--c-trait-fin);
}
.items--grid .item__thumb--empty {
  display: grid;
  place-items: center;
  color: var(--c-texte-doux);
  font-size: 11px;
  font-weight: var(--tres-gras);
  letter-spacing: 0.08em;
  font-family: var(--police-texte);
}
.items--grid .item__name { font-size: var(--taille-minuscule); text-align: center; flex: none; }
.items--grid .item__origin { text-align: center; }
.items--grid .item[aria-checked="true"] { border-color: var(--c-coche); }

/* Résultats de recherche : une colonne, la provenance a besoin de place. */
.items--results { grid-template-columns: 1fr; gap: 2px; }
@media (min-width: 720px) { .items--results { grid-template-columns: repeat(2, 1fr); gap: 2px var(--esp-lg); } }

/* Mode rapide : glisser le doigt pour cocher plusieurs cases.
   Pas de `touch-action: none` ici : il coupait le défilement dès qu'on posait
   le doigt sur un item, rendant les longues listes impossibles à parcourir.
   C'est js/ui/checking.js qui prend la main, et seulement une fois le geste de
   cochage confirmé (au-delà de 24 px de déplacement). */
body.fast-mode .item { user-select: none; }
body.fast-mode .items .item { background: var(--c-survol); }
body.fast-mode .item__box { transform: scale(1.1); }

/* ==========================================================================
   12. BARRE BASSE
   ========================================================================== */

/* --- BARRE D'OUTILS DE LA LISTE : chercher, filtrer ----------------------- */
/* Elle a REMPLACÉ la pastille flottante du bas, le 2026-08-14. Le raisonnement
   complet est dans `js/ui/barre-liste.js` ; l'essentiel tient en une phrase :
   collante SOUS le bandeau, elle ne recouvre plus rien — le contenu s'arrête à
   son bord — alors que la pastille passait par-dessus deux rangées d'items,
   en permanence, et emportait la recherche hors de portée dès le premier
   défilement.

   Le `top` s'empile sur les deux hauteurs MESURÉES qui la précèdent : le
   bandeau, et le bandeau de correction quand il est là (0 sinon). Une valeur
   figée la ferait passer derrière le bandeau dès qu'une police charge ou qu'un
   titre passe sur deux lignes — c'est exactement le défaut qui rendait le
   bouton « Tout cocher » d'une série inatteignable avant que ces hauteurs ne
   soient mesurées (voir js/ui/header.js). */
/* PLUS COLLANTE — et c'est ce qui libère le plus d'écran de toute la refonte.
   « Quand on scrolle, le champ de recherche n'a pas à être collant ; et laisser
   juste le filtre serait moche, donc le filtre peut passer dans le header »
   (Adrien, 2026-08-14). Le filtre est monté dans le bandeau fixe, où il est
   atteignable à toute hauteur ; la recherche redevient du contenu ordinaire —
   on la retrouve en remontant, ce qu'on fait de toute façon pour chercher.
   Un étage collant en moins : les en-têtes de série se calent maintenant
   directement sous les 44 px du bandeau, au lieu de 44 + 67. */
.barre-liste {
  display: flex;
  gap: var(--esp-sm);
  align-items: center;
  margin-bottom: var(--esp-sm);
}

.barre-liste .search { flex: 1 1 auto; min-width: 0; }

/* Trois segments VISIBLES, plus un bouton qui cycle en secret.
   -------------------------------------------------------------------------
   L'ancien bouton n'affichait que l'état courant : pour découvrir les deux
   autres, il fallait appuyer au hasard et lire ce qui changeait. C'est
   exactement pour compenser cette opacité qu'une PHRASE séparée existait sous
   la barre — la ligne qu'Adrien a entourée n'était pas le problème, elle en
   était le symptôme. Trois segments disent l'état ET les possibilités, donc la
   phrase disparaît dans le cas courant, et on va à n'importe quel filtre en un
   seul appui au lieu de deux.
   Même langage visuel que le choix d'affichage : le guide a déjà une façon de
   dire « choisis une option », en inventer une seconde ferait deux langages. */

.barre-liste__filtre,
.entete-vue {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--cible-tactile);
  padding: 0 var(--esp-md);
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-piste);
  background: transparent;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
  cursor: pointer;
  white-space: nowrap;
}
.barre-liste__filtre svg,
.entete-vue svg { width: 17px; height: 17px; flex: none; }
/* Le mot « Liste » / « Photos » a été RETIRÉ le 2026-08-17 (il n'existait plus
   que sous 360 px de toute façon). L'icône d'accueil est arrivée dans le
   bandeau : le groupe de gauche a pris 48 px, et le titre centré passait sous
   cette pastille — mesuré à 320, 390 ET 430 px. Le libellé était la seule
   information redondante du bandeau (l'icône dit déjà grille ou lignes) et la
   plus large ; le nom accessible complet, lui, est resté sur le bouton. */
/* Le libellé du filtre (« Tout », « J'ai », « Manque »). JS le réécrit à chaque
   changement de filtre : s'il revenait à la ligne, la pastille changerait de
   hauteur d'un mot à l'autre, et le bandeau sauterait. */
.barre-liste__filtre-label { white-space: nowrap; }
/* Un filtre ACTIF doit se voir sans lire : c'est lui qui explique pourquoi la
   liste paraît incomplète. La forme change en plus de la couleur (contour →
   plein), l'information ne repose donc pas sur la couleur seule. */
/* Neutre lui aussi — voir la règle du jaune ci-dessus. L'information « un
   filtre est actif » passe par la FORME (contour → plein) et par la phrase
   juste en dessous, pas par une couleur qui appartient au cochage. */
.barre-liste__filtre[aria-pressed="true"] {
  background: var(--c-texte);
  color: var(--c-fond);
  border-color: var(--c-texte);
  font-weight: var(--tres-gras);
}

.barre-liste__rapide {
  flex: none;
  min-height: var(--cible-tactile);
  padding: 0 var(--esp-md);
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-piste);
  background: transparent;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
  cursor: pointer;
  white-space: nowrap;
}
/* Neutre, comme les autres options : le jaune reste au cochage. C'était la
   dernière instance du défaut que cette zone a corrigé partout ailleurs —
   invisible au doigt (le mode rapide n'existe qu'à la souris), mais une règle
   à moitié appliquée n'est pas une règle. */
.barre-liste__rapide[aria-pressed="true"] {
  background: var(--c-texte);
  color: var(--c-fond);
  border-color: var(--c-texte);
}

/* ==========================================================================
   13. FICHE D'UN ITEM
   Panneau qui monte du bas : la photo en grand, la provenance complète, et
   le bouton pour cocher. Ouvert par un appui long (ou la touche Entrée), pour
   ne pas gêner le geste principal, qui reste le cochage d'un simple appui.
   ========================================================================== */

.sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-fiche);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: var(--c-rideau);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duree-courte) ease;
}
.sheet.is-open { opacity: 1; pointer-events: auto; }

.sheet__panel {
  width: 100%;
  max-width: 520px;
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
  border-bottom: 0;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--esp-sm) var(--esp-lg) calc(var(--esp-lg) + env(safe-area-inset-bottom, 0px));
  transform: translateY(16px);
  transition: transform var(--duree-courte) var(--courbe);
  max-height: 88svh;
  overflow-y: auto;
}
.sheet.is-open .sheet__panel { transform: translateY(0); }

.sheet__grip {
  width: 42px;
  height: 4px;
  border-radius: var(--r-plein);
  background: var(--c-trait);
  margin: var(--esp-sm) auto var(--esp-lg);
}

.sheet__photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  max-height: 42svh;
  object-fit: contain;
  border-radius: var(--r-md);
  background: var(--c-trait-fin);
  margin-bottom: var(--esp-lg);
}
.sheet__photo--empty {
  /* Sans photo, ce cadre n'apporte rien : il ne prend que la place d'un repère,
     pas la moitié de l'écran. */
  aspect-ratio: auto;
  min-height: 120px;
  display: grid;
  place-items: center;
  color: var(--c-texte-doux);
  font-size: 12px;
  font-weight: var(--tres-gras);
  letter-spacing: 0.08em;
}

.sheet__name {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: clamp(22px, 6vw, 30px);
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
  line-height: 1.1;
  margin: 0 0 var(--esp-sm);
}

.sheet__meta { display: flex; flex-wrap: wrap; gap: var(--esp-sm); margin-bottom: var(--esp-lg); }
.sheet__tag {
  font-size: var(--taille-minuscule);
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-texte-doux);
  border: 1px solid var(--c-bordure);
  border-radius: var(--r-plein);
  padding: 6px 12px;
}
.sheet__tag--cat { color: var(--c-accent-texte); border-color: var(--c-accent-trait); }

.sheet__state {
  font-size: 14px;
  color: var(--c-texte-doux);
  margin: 0 0 var(--esp-md);
}

/* LE NOM DE FICHIER À DONNER À LA PHOTO — n'apparaît que sur les items qui
   n'en ont pas encore. Il n'avait AUCUN style : les trois éléments se suivaient
   en ligne, le code se confondait avec la phrase qui le précède, et on ne
   voyait pas ce qu'il fallait recopier. Or c'est tout l'objet du bloc.
   Le `<code>` est donc posé sur son propre fond, en pleine largeur, et
   `word-break` le laisse revenir à la ligne : certains identifiants font plus
   de 40 caractères et débordaient de la fiche sur iPhone SE. */
.sheet__photo-id {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--esp-sm);
  padding: var(--esp-md);
  margin: 0 0 var(--esp-md);
  border: 1px dashed var(--c-bordure);
  border-radius: var(--r-md);
}
.sheet__photo-id__quoi {
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
  line-height: 1.5;
}
/* La règle de toute la série (« TZ001 à TZ376 »). C'est elle qui évite de
   rouvrir la fiche pour chacune des 376 photos : elle mérite d'être lisible,
   pas reléguée en note de bas de page. */
.sheet__photo-id__serie {
  font-size: var(--taille-minuscule);
  color: var(--c-texte-doux);
  line-height: 1.55;
}
.sheet__photo-id__serie strong {
  color: var(--c-texte);
  font-weight: var(--tres-gras);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sheet__photo-id__code {
  align-self: stretch;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  color: var(--c-texte);
  background: var(--c-survol);
  border-radius: var(--r-sm);
  padding: 8px 10px;
  word-break: break-all;
  /* Recopier à la main reste possible même si le presse-papiers est refusé. */
  user-select: all;
  -webkit-user-select: all;
}

.sheet__actions { display: flex; flex-wrap: wrap; gap: 10px; }
/* Même hauteur pour tous les boutons d'une même rangée : un libellé qui passe
   sur deux lignes ne doit pas rendre son bouton plus haut que ses voisins.
   Mesuré dans la fiche d'item : 54 / 54 / 44 / 44 px côte à côte. */
.sheet__actions .btn,
.perso-actions .btn { flex: 1 1 160px; justify-content: center; min-height: 54px; }

/* ==========================================================================
   14. BOUTONS, RÉGLAGES, ÉTATS
   ========================================================================== */

.btn {
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  /* ⚠️ SANS CETTE LIGNE, le libellé d'un bouton ÉTIRÉ se cale à gauche.
     Tant qu'un bouton fait sa largeur naturelle, ça ne se voit pas — le
     contenu remplit tout. Dès qu'on lui donne une largeur (pleine largeur
     dans une colonne, une cellule de grille), le texte reste collé au bord
     gauche pendant que le bouton principal, lui, est centré par sa propre
     règle. Vu sur l'écran de connexion : « SE CONNECTER » centré, « CRÉER UN
     COMPTE » à gauche, l'un sous l'autre. Corrigé ICI plutôt que sur le cas
     rencontré : le défaut serait revenu au prochain bouton étiré. */
  justify-content: center;
  gap: var(--esp-sm);
  background: var(--c-texte);
  color: var(--c-fond);
  border: 2px solid var(--c-texte);
  border-radius: var(--r-plein);
  padding: 11px 20px;
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  text-decoration: none;
  transition: transform 0.1s ease;
}
.btn:active { transform: scale(0.97); }
.btn--ghost { background: transparent; color: var(--c-texte); border-color: var(--c-bouton-trait); }
.btn--danger { background: transparent; color: var(--c-danger); border-color: var(--c-danger-trait); }
.btn--coche {
  background: var(--c-coche);
  /* Bord assombri : un aplat jaune sur du blanc n'a aucun contour lisible. */
  border-color: var(--c-coche-sombre);
  color: var(--c-coche-marque);
}
/* `.btn--primary` = l'aplat plein, c'est-à-dire le style de `.btn` tout court.
   La classe existe parce que js/views/home.js l'écrit sur « Terminé » (fin du
   rangement de l'accueil) ; sans règle ici, elle ne faisait rien — le bouton
   passait quand même, par héritage de `.btn`. On la déclare explicitement pour
   que le jour où `.btn` deviendrait discret par défaut, l'action principale
   reste l'action principale. */
.btn--primary { background: var(--c-texte); color: var(--c-fond); border-color: var(--c-texte); }
/* Bouton d'appoint, à l'intérieur d'un bloc de texte (« Copier ce nom »).
   La hauteur descend sous la cible tactile ordinaire : c'est délibéré et c'est
   la SEULE exception du guide. Ce bouton double une information déjà lisible à
   l'écran (le nom de fichier est écrit juste au-dessus, on peut le recopier) ;
   il ne commande rien qu'on ne puisse faire autrement. 40 px reste au-dessus
   du plancher de 36 px des cibles secondaires, et l'alternative — un bouton
   pleine hauteur — poussait la fiche d'item hors de l'écran sur iPhone SE. */
.btn--petit {
  min-height: 40px;
  padding: 8px 14px;
  font-size: 12px;
  letter-spacing: 0.03em;
}
/* L'air qui manquait sous le bouton principal.
   Ni `.compte-form` ni `.btn-row` ne déclaraient de marge : le CTA et le bouton
   secondaire étaient donc COLLÉS, 0 px d'écart. C'est la cause exacte des
   « boutons mal placés » signalés par Adrien — mesuré par `apple-design-master`,
   pas deviné. */
/* Mesuré sur l'écran de connexion : le bouton principal faisait 360 px pleine
   largeur, le secondaire 184 px calé à gauche, et le lien 128 px centré. Trois
   actions qui se suivent, trois largeurs, trois alignements — un escalier.
   Elles partagent maintenant le même bord et la même largeur ; seul le
   TRAITEMENT dit le rang (plein, cerclé, souligné). */
.btn-row--suite { margin-top: var(--esp-md); }
.btn-row--suite .btn { flex: 1 1 100%; }

/* Le troisième niveau : ni bouton plein, ni bouton cerclé. Un lien, avec une
   vraie cible tactile. « Mot de passe oublié » ne pèse pas autant que « Créer
   un compte » — leur donner le même contour disait le contraire. */
.compte-tertiaire {
  margin: var(--esp-md) 0 0;
  text-align: center;
}

/* ⚠️ Un lien de 44 px de haut POSÉ DANS UN PARAGRAPHE écarte les lignes de
   celui-ci de 44 px : le pied de l'écran de création se lisait avec un
   interligne énorme, pour une seule ligne de texte utile. On lui rend une
   hauteur de ligne normale et on porte la cible tactile par le padding. */
.compte-pied .compte-lien {
  min-height: 0;
  padding: 0;
  display: inline;
}

/* La zone de message est TOUJOURS dans le gabarit — c'est voulu : elle réserve
   sa place pour que l'apparition d'une erreur ne fasse pas sauter le bouton
   sous le doigt. Mais elle réservait la place d'un paragraphe ENTIER, d'où le
   grand vide au-dessus du bouton tant qu'il n'y a rien à dire. Une hauteur de
   ligne suffit : c'est exactement ce qu'occupera le message. */
.compte-message:empty {
  min-height: 1.4em;
  margin: 0;
}

.compte-intro {
  margin: 0 0 var(--esp-md);
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.55;
}
.compte-intro strong { color: var(--c-texte); font-weight: var(--tres-gras); }

/* Un bouton qui travaille — « Connexion… », « Création… ». Il n'avait aucune
   règle : rien ne distinguait un bouton en cours d'un bouton normal, alors que
   c'est précisément le moment où l'on se demande si son geste a été pris. */
.btn:disabled {
  opacity: 0.55;
  cursor: default;
  transition: opacity var(--duree-courte) ease;
}

.btn-row { display: flex; flex-wrap: wrap; gap: 10px; }

/* Bouton textuel discret (« Tout cocher » au niveau d'une liste entière) */
/* « Tout cocher » — la seule utilisation de ce style, et l'action la plus
   lourde de l'écran : elle coche jusqu'à 251 items d'un geste.
   Elle était en JAUNE, c'est-à-dire dans la couleur qui signifie « je le
   possède » partout ailleurs, et elle devenait l'élément le plus lumineux de la
   page en mode nuit — le plus effacé en mode jour. Deux thèmes, deux hiérarchies
   opposées pour le même bouton dangereux. Il est désormais lisible sans crier,
   à l'identique dans les deux thèmes. */

.settings-card {
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
  border-radius: var(--r-md);
  padding: 18px;
  margin-bottom: var(--esp-md);
}
.settings-card h2 {
  margin: 0 0 6px;
  font-size: 16px;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
}
/* Les actions destructrices sont séparées du reste de la carte : on ne veut
   pas « Tout décocher » collé au bouton de sauvegarde. */
.settings-card__danger {
  margin-top: var(--esp-md);
  padding-top: var(--esp-md);
  border-top: 1px solid var(--c-bordure);
}

.btn__icon { width: 17px; height: 17px; flex: none; }

.settings-card p { margin: 0 0 var(--esp-md); color: var(--c-texte-doux); font-size: 14px; line-height: 1.5; }

/* Choix du thème */
.theme-picker { display: flex; gap: var(--esp-sm); flex-wrap: wrap; }
.theme-btn {
  flex: 1 1 90px;
  min-height: var(--cible-tactile);
  background: transparent;
  border: 1.5px solid var(--c-bouton-trait);
  color: var(--c-texte);
  border-radius: var(--r-plein);
  padding: 10px var(--esp-md);
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background var(--duree-courte) ease, border-color var(--duree-courte) ease;
}
.theme-btn:hover { background: var(--c-survol); }
/* ⚠️ PAS `--c-coche` : c'est le jaune du cochage, et « Jour » est un ÉTAT
   D'OPTION, pas un item possédé. La règle a été posée le 2026-08-14 après
   qu'Adrien a relevé « quatre jaunes sans rapport dans un demi-écran » — et ce
   bouton-ci y avait échappé, parce que le contrôle écrit alors ne regardait que
   trois sélecteurs de l'écran de liste. Une option choisie se marque en neutre
   plein, comme partout ailleurs dans le guide. */
.theme-btn[aria-pressed="true"] { background: var(--c-texte); border-color: var(--c-texte); color: var(--c-fond); }

.empty { text-align: center; color: var(--c-texte-doux); padding: 46px 20px; font-size: var(--taille-texte); }
.empty__cta { margin-top: 18px; }

/* La rangée « Réglages » au bas de l'accueil : centrée, et nettement détachée
   de la dernière liste — sinon elle se lit comme une liste de plus. */
/* LE PIED DE PAGE RECULE. Réglages, note et signature ne sont pas une
   quatrième zone de même rang : ce qui clôt une page doit se lire comme tel.
   Un filet fin et beaucoup d'air au-dessus suffisent — sans quoi le bouton
   « Réglages » pèse autant qu'une liste de 722 items. */
.home-links {
  display: flex;
  justify-content: center;
  margin: var(--esp-xl) 0 var(--esp-md);
  padding-top: var(--esp-xl);
  border-top: 1px solid var(--c-trait-fin);
}

.footnote {
  color: var(--c-texte-doux);
  font-size: 12.5px;
  line-height: 1.6;
  text-align: center;
  margin: var(--esp-xl) auto 0;
  max-width: 60ch;
}

/* --- La signature ---------------------------------------------------------
   « Un projet de Pokemat, pour la communauté ♥ » — le dernier mot de la page
   d'accueil. C'est un remerciement, pas une mention légale : il est en petites
   capitales de la police d'affichage, comme les titres, pour qu'il se lise
   comme une signature et non comme une note de bas de page.

   Le cœur est un SVG, PAS le caractère « ♥ ». Trois raisons, dans cet ordre :
   les polices du site sont des sous-ensembles hébergés avec lui (78 Ko) et ne
   contiennent pas ce glyphe ; iOS le remplacerait par un cœur ROUGE en couleur,
   hors charte ; et un glyphe absent partout donnerait un carré vide. En SVG, il
   prend la couleur d'accent — le jaune de la marque la nuit, le bleu marine le
   jour — et suit la taille du texte, y compris à 200 % de zoom.               */
.signature {
  margin: var(--esp-lg) auto 0;
  text-align: center;
  font-family: var(--police-affichage);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte-doux);
}
.signature strong {
  color: var(--c-texte);
  font-weight: var(--tres-gras);
}
.signature__coeur {
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.18em;
  margin-left: 0.15em;
  color: var(--c-accent-texte);
}

/* ==========================================================================
   15. TOAST
   ========================================================================== */

.toast {
  position: fixed;
  left: 50%;
  /* Au-dessus de la barre basse, avec un vrai dégagement : à 84px, le message
     venait toucher la barre et les deux se lisaient comme un seul bloc. */
  bottom: calc(110px + env(safe-area-inset-bottom, 0px));
  transform: translate(-50%, 14px);
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--esp-md);
  background: var(--c-toast-fond);
  color: var(--c-toast-texte);
  font-weight: 700;
  font-size: 14px;
  padding: 12px 20px;
  border-radius: var(--r-plein);
  box-shadow: var(--ombre-forte);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  /* Assez large pour tenir sur une ou deux lignes : le message se cassait en
     quatre lignes étroites dès qu'il portait un bouton « Annuler ». */
  width: max-content;
  max-width: calc(100vw - 32px);
  text-align: left;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.toast__text { flex: 1; min-width: 0; }

/* Le bandeau laisse passer les touchers (il ne doit pas bloquer la liste en
   dessous), mais son bouton d'action doit évidemment rester cliquable. */
.toast__action {
  pointer-events: auto;
  flex: none;
  min-height: 36px;
  background: var(--c-toast-texte);
  color: var(--c-toast-fond);
  border: 0;
  border-radius: var(--r-plein);
  padding: var(--esp-sm) 16px;
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
}

/* ==========================================================================
   16. HORS CONNEXION
   Rassure plutôt qu'alerte : l'application est conçue pour fonctionner ainsi.
   ========================================================================== */

body.is-offline .gdi-main::before {
  content: 'Hors connexion — le guide fonctionne normalement';
  display: block;
  background: var(--c-accent-voile);
  border: 1px solid var(--c-accent-trait);
  color: var(--c-texte);
  font-size: 12.5px;
  font-weight: 700;
  text-align: center;
  padding: 9px var(--esp-md);
  border-radius: var(--r-sm);
  margin-bottom: 16px;
}

/* ==========================================================================
   17. ACCESSIBILITÉ
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   MA COLLECTION — la vue d'ensemble (écran #/collection) et sa porte d'entrée
   sur l'accueil.

   Aucune valeur littérale ici : uniquement des jetons. Pour changer une
   couleur ou un arrondi, modifier le jeton (§1), jamais cette section.
   ========================================================================== */

/* --- porte d'entrée sur l'accueil ---------------------------------------- */

/* --- en-tête de l'écran : le chiffre qu'on vient chercher ----------------- */
.collec-tete {
  text-align: center;
  padding: var(--esp-lg) 0 var(--esp-xl);
}
/* Le même anneau que dans les listes, agrandi : une seule mécanique à maintenir. */
.collec-tete .menu-row__ring { width: 108px; height: 108px; margin: 0 auto var(--esp-md); }
.collec-tete .menu-row__ring::after { width: 86px; height: 86px; }

.collec-tete__chiffre {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-categorie);
  line-height: 1.1;
}
.collec-tete__chiffre span {
  font-size: var(--taille-titre);
  color: var(--c-texte-doux);
}
.collec-tete__reste { margin-top: var(--esp-sm); color: var(--c-texte-doux); }
.collec-tete__reste strong { color: var(--c-texte); font-weight: var(--tres-gras); }
.collec-tete__listes {
  margin-top: var(--esp-xs);
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
}

/* Le bloc « par catégorie » a été supprimé le 2026-08-14 (voir la raison en
   toutes lettres dans js/views/collection.js). Ses règles sont parties avec
   lui — le contrôle « aucune classe CSS orpheline » de `npm test` ne laisse
   plus passer le contraire. */

/* --- tri ------------------------------------------------------------------ */
.collec-tris {
  display: flex;
  flex-wrap: wrap;
  gap: var(--esp-xs);
  margin-bottom: var(--esp-md);
}
.collec-tri {
  min-height: var(--cible-tactile);
  padding: 0 var(--esp-md);
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-piste);
  background: transparent;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  cursor: pointer;
}
/* ⚠️ PAS `--c-coche` : c'est le jaune du cochage, et un tri choisi est un ÉTAT
   D'OPTION, pas un item possédé. Le voir ici, sur l'écran qui affiche justement
   des compteurs de cases jaunes, était le pire endroit possible. Neutre plein :
   l'option choisie reste évidente, sans emprunter un sens qui n'est pas le sien. */
.collec-tri[aria-pressed="true"] {
  background: var(--c-texte);
  color: var(--c-fond);
  border-color: var(--c-texte);
  font-weight: var(--gras);
}

/* --- une liste dans le relevé --------------------------------------------- */
.collec-listes { display: grid; gap: var(--esp-sm); }
.collec-ligne {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--c-bloc-fond);
  color: var(--c-bloc-texte);
  border: 1px solid var(--c-bloc-bordure);
  border-radius: var(--r-sm);
  padding: var(--esp-md);
  cursor: pointer;
  min-height: var(--cible-tactile);
}
.collec-ligne:hover { background: var(--c-bloc-survol); }
.collec-ligne__haut,
.collec-ligne__bas {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--esp-sm);
}
.collec-ligne__haut { margin-bottom: var(--esp-xs); }
.collec-ligne__bas { margin-top: var(--esp-xs); font-size: var(--taille-minuscule); }
.collec-ligne__nom {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  text-transform: uppercase;
  letter-spacing: var(--lettres-capitales);
}
.collec-ligne__part { font-weight: var(--gras); white-space: nowrap; color: var(--c-bloc-doux); }
.collec-ligne__cat,
.collec-ligne__manque { color: var(--c-bloc-doux); }
.collec-ligne__fini { color: var(--c-bloc-texte); font-weight: var(--tres-gras); }

/* --- bouton d'apparence : légende transitoire -----------------------------
   Le libellé n'est plus affiché en permanence (il volait l'attention au logo).
   Il apparaît au moment du changement, puis s'efface.

   Il est positionné en ABSOLU, hors du flux : on n'anime ainsi qu'`opacity` et
   `transform`, jamais une largeur — animer une dimension forcerait un recalcul
   de mise en page à chaque image (règle du projet).
   -------------------------------------------------------------------------- */
.theme-toggle { position: relative; padding: var(--esp-sm) 12px; }
.theme-toggle__label {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  white-space: nowrap;
  padding: 5px 12px;
  border-radius: var(--r-plein);
  background: var(--c-surface-haute);
  border: 1px solid var(--c-bordure);
  color: var(--c-texte);
  font-size: var(--taille-minuscule);
  opacity: 0;
  transform: translateY(-5px);
  pointer-events: none;
  transition: opacity var(--duree-longue) var(--courbe),
              transform var(--duree-longue) var(--courbe);
}
.theme-toggle.is-showing .theme-toggle__label { opacity: 1; transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .theme-toggle__label { transition: none; }
}

/* ==========================================================================
   MES LISTES — les listes que l'utilisateur crée lui-même (#/liste/p3),
   le choix de liste dans la fiche d'item, et la liste reçue par lien
   (#/partage/…).

   Aucune couleur en dur : tout passe par les jetons, donc les deux thèmes
   suivent sans une seule règle en double. Les commandes de quantité font
   44 px de haut — c'est la cible minimale du projet, et on s'en sert debout.
   ========================================================================== */

.btn--large { width: 100%; justify-content: center; }

/* --- Commande de quantité (− n +), partagée par la fiche et la liste ------ */

.rang-listes { display: grid; gap: 2px; margin-bottom: var(--esp-lg); }

.rang-liste {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  padding: 6px 12px;
  border-radius: var(--r-md);
  background: var(--c-surface);
  border: 1px solid var(--c-trait-fin);
}
.rang-liste.is-dedans { border-color: var(--c-accent-trait); background: var(--c-accent-voile); }

.rang-liste__nom {
  flex: 1;
  min-width: 0;
  font-weight: var(--gras);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rang-liste__pas { display: flex; align-items: center; gap: 2px; flex: none; }

.pas__btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid var(--c-bouton-trait);
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--c-texte);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.pas__btn:hover { background: var(--c-survol); }
.pas__btn:active { transform: scale(0.94); }
.pas__btn[disabled] { opacity: 0.35; cursor: default; }
.pas__btn[disabled]:active { transform: none; }

.pas__n {
  min-width: 34px;
  text-align: center;
  font-weight: var(--tres-gras);
  font-variant-numeric: tabular-nums;
}

/* --- Fiche d'item : second volet ----------------------------------------- */

.sheet__name--petit { font-size: 20px; margin-bottom: 4px; }
.sheet__sous { color: var(--c-texte-doux); margin: 0 0 var(--esp-md); }
.sheet__vide { color: var(--c-texte-doux); font-size: var(--taille-petit); line-height: 1.55; margin: 0 0 var(--esp-md); }
.sheet__dans { color: var(--c-texte-doux); font-size: var(--taille-petit); margin: 0 0 var(--esp-md); }
.sheet__dans strong { color: var(--c-accent-texte); font-weight: var(--tres-gras); }

.sheet__saisie { display: flex; gap: var(--esp-sm); width: 100%; }
.sheet__saisie input,
.perso-renommage input,
/* `.champ` — le pseudo des Réglages. Il n'avait AUCUNE règle : un `<input>`
   brut, sans bordure ni fond, dans un thème sombre — donc blanc sur blanc
   selon le navigateur, et haut de 22 px, la moitié de la cible tactile.
   Trouvé le 2026-08-14 par le contrôle « toute classe de gabarit a une règle ».
   Il rejoint ici les deux autres champs de saisie du guide au lieu d'avoir sa
   propre définition : trois champs, une seule apparence. */
.champ {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 12px;
  border-radius: var(--r-md);
  /* 2 px EN PERMANENCE, seule la couleur change au focus : à 1 px l'indicateur
     de focus clavier passe sous le seuil WCAG 2.4.11, et l'épaissir seulement
     au focus fait sauter la mise en page. Correctif déjà appliqué au champ de
     recherche, jamais reporté ici (audit 2026-08-14). */
  border: 2px solid var(--c-champ-trait);
  background: var(--c-fond);
  color: var(--c-texte);
  font: inherit;
  /* ⚠️ 16 px EXPLICITE, et pas hérité. En dessous de 16 px, iOS ZOOME la page
     au moment où l'on touche le champ — on perd sa place, et il faut dézoomer
     à deux doigts. Ça marchait déjà, mais par accident : `font: inherit`
     remontait jusqu'à `body`, qui ne fixe aucune taille, donc le navigateur
     retombait sur son défaut de 16 px. Le jour où une taille serait posée plus
     haut, le zoom reviendrait sans qu'une seule ligne d'ici ne bouge. */
  font-size: 16px;
}
.sheet__saisie input:focus,
.perso-renommage input:focus,
.champ:focus { border-color: var(--c-focus); outline: none; }
/* Les deux autres champs vivent dans une rangée HORIZONTALE, où `flex: 1`
   veut dire « prends la largeur restante ». `.champ`, lui, est dans une
   colonne (`.pseudo-champ`) : le même `flex: 1` y porterait sur la HAUTEUR et
   entrerait en conflit avec les 44 px. On le neutralise, et on demande la
   largeur explicitement. */
.champ { flex: none; align-self: stretch; }

/* --- Écran d'une liste personnelle --------------------------------------- */

.perso-tete { margin-bottom: var(--esp-md); }
.perso-tete__nom {
  font-family: var(--police-affichage);
  font-weight: var(--tres-gras);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin: 0;
  word-break: break-word;
}
.perso-tete__compte { color: var(--c-texte-doux); margin: 6px 0 0; font-size: var(--taille-petit); }

.lien-discret {
  background: none;
  border: 0;
  /* Seule cible tactile de l'application qui passait sous 44 px (65×22
     mesuré) — et c'est le seul moyen de renommer une liste. La règle du
     projet ne souffre pas d'exception : usage debout, une main. */
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  padding: 4px 2px;
  color: var(--c-accent-texte);
  font: inherit;
  font-size: var(--taille-petit);
  text-decoration: underline;
  cursor: pointer;
}

.perso-renommage { display: flex; flex-wrap: wrap; gap: var(--esp-sm); }

.perso-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: var(--esp-lg); }
.perso-actions .btn { flex: 1 1 180px; justify-content: center; }

.perso-items { display: grid; gap: 2px; }

.perso-item {
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--c-trait-fin);
}
.perso-item__corps {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  min-height: 44px;
  padding: 0 8px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: var(--r-sm);
}
.perso-item__corps:hover { background: var(--c-survol); }
.perso-item__num {
  flex: none;
  min-width: 30px;
  color: var(--c-texte-faible);
  font-variant-numeric: tabular-nums;
  font-size: var(--taille-petit);
}
/* Le nom d'un item s'écrit PARTOUT de la même façon : Oswald 700 en capitales,
   comme dans les listes du guide. Ici il s'affichait en Archivo minuscules —
   le même Dracaufeu changeait donc de visage selon l'écran, alors que la charte
   de Mathieu dit « titres ET noms : Oswald 700, capitales ». */
.perso-item__nom {
  flex: 1;
  min-width: 0;
  font-family: var(--police-affichage);
  font-weight: var(--tres-gras);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
}
.perso-item.est-coche .perso-item__nom { color: var(--c-accent-texte); }

.perso-danger { margin-top: var(--esp-xl); padding-top: var(--esp-lg); border-top: 1px solid var(--c-trait-fin); }

.perso-alerte,
.collec-note {
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.55;
  margin: 0 0 var(--esp-lg);
  padding: 12px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--c-trait-fin);
  background: var(--c-surface);
}

/* --- Recherche d'items à ajouter ----------------------------------------- */

/* --- Liste reçue par lien ------------------------------------------------ */

.recu-tete { margin-bottom: var(--esp-lg); }
.recu-tete__source {
  color: var(--c-accent-texte);
  font-size: var(--taille-minuscule);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0 0 4px;
}
.recu-tete__compte { color: var(--c-texte-doux); margin: 6px 0 0; font-size: var(--taille-petit); }

.recu-compare {
  display: grid;
  gap: var(--esp-sm);
  margin-top: var(--esp-md);
  padding: var(--esp-md);
  border-radius: var(--r-lg);
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  color: var(--c-bloc-texte);
}
.recu-compare__bloc { display: flex; align-items: baseline; gap: var(--esp-md); }
.recu-compare__n {
  font-family: var(--police-affichage);
  font-size: 30px;
  font-weight: var(--tres-gras);
  font-variant-numeric: tabular-nums;
  min-width: 62px;
  text-align: right;
}
.recu-compare__quoi { color: var(--c-bloc-doux); font-size: var(--taille-petit); line-height: 1.4; }
.recu-compare__pied {
  margin: 4px 0 0;
  padding-top: var(--esp-sm);
  border-top: 1px solid var(--c-bloc-trait);
  color: var(--c-bloc-doux);
  font-size: var(--taille-minuscule);
}

.recu-item {
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  min-height: 44px;
  padding: 4px 8px;
  border-bottom: 1px solid var(--c-trait-fin);
}
/* L'écran de réception réutilise `.perso-item__nom` pour le nom d'item : il y
   avait ici une règle `.recu-item__nom` que RIEN n'émet — elle ne s'appliquait
   donc à personne, et laissait croire que cet écran avait son propre style.
   La réutilisation est assumée : les deux écrans montrent la même chose (un
   item d'une liste), et diverger n'apporterait rien. */
.recu-qte { color: var(--c-accent-texte); font-weight: var(--tres-gras); }
.recu-etat {
  flex: none;
  color: var(--c-texte-doux);
  font-size: var(--taille-minuscule);
  text-align: right;
}
.recu-item.est-commun .recu-etat { color: var(--c-accent-texte); }

@media (min-width: 720px) {
  .perso-items { grid-template-columns: repeat(2, 1fr); gap: 2px var(--esp-lg); }
  .recu-compare { grid-template-columns: 1fr 1fr; }
  .recu-compare__pied { grid-column: 1 / -1; }
}

/* ==========================================================================
   MODE CORRECTION — les crayons, le panneau de correction, l'écran de
   relecture (#/corrections).

   Volontairement discret : ces commandes n'existent que pour Mathieu et Adrien,
   et n'apparaissent que le mode activé. Elles ne doivent jamais attirer l'œil
   d'un collectionneur qui coche ses items.
   ========================================================================== */

/* Le crayon doit se REMARQUER : il n'apparaît que le mode allumé, et c'est le
   seul endroit où l'on peut agir. Discret, il passait inaperçu au milieu d'un
   en-tête de série. */


/* --- Écran de relecture --------------------------------------------------- */



.settings-card code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  padding: 2px 5px;
  border-radius: 4px;
  background: var(--c-survol);
  word-break: break-word;
}

/* ==========================================================================
   RETOUR À PORTÉE DU POUCE — bouton flottant en bas à droite

   LE SEUL bouton de retour du guide : il REMPLACE la flèche du bandeau haut,
   il ne s'y ajoute pas. Demande d'Adrien, formulée deux fois — la première
   fois j'avais ajouté celui-ci sans éteindre l'autre, ce qui donnait deux
   boutons pour une action, dont un hors d'atteinte du pouce.
   La flèche du bandeau n'est pas supprimée pour autant : une valeur suffit à
   la rallumer si Mathieu la réclame au nom de sa charte (FLECHE_HAUT dans
   js/ui/header.js). Second chemin, complémentaire : le balayage depuis le
   bord gauche.
   ========================================================================== */

.fab-retour {
  position: fixed;
  /* Le SEUL élément flottant qui reste en bas depuis le 2026-08-14 (la barre
     d'outils est remontée dans la liste). Son plan reste déclaré explicitement :
     ce bouton est écrit avant <main> pour l'ordre de tabulation, donc l'ordre du
     document ne joue plus en sa faveur. */
  right: calc(var(--esp-md) + env(safe-area-inset-right, 0px));
  bottom: calc(var(--esp-md) + env(safe-area-inset-bottom, 0px));
  z-index: calc(var(--z-flottant) + 1);
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  border-radius: var(--r-plein);
  background: var(--c-flottant-fond);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--c-flottant-trait);
  box-shadow: var(--ombre-forte);
  color: var(--c-texte);
  cursor: pointer;
  transition: transform var(--duree-courte) var(--courbe);
}
.fab-retour[hidden] { display: none; }
.fab-retour svg { width: 24px; height: 24px; }
.fab-retour:active { transform: scale(0.92); }

/* Le retour reste SUR LA MÊME LIGNE que la barre basse, jamais empilé
   au-dessus.
   -------------------------------------------------------------------------
   Il était remonté de 72 px dès que la barre apparaissait, par crainte d'un
   chevauchement. Résultat, signalé par Adrien capture à l'appui : le bouton
   flottait au MILIEU de la liste, à droite, posé sur les noms d'items comme un
   autocollant — « beurk », et il a raison.
   La crainte était infondée : la barre basse est une pilule CENTRÉE et étroite
   (« Filtre / Tout » seul, depuis que le mode rapide a disparu du tactile),
   le retour est collé au bord droit. Mesuré : 46 px d'écart à 320 px — le plus
   petit écran visé — et 81 px à 390 px. Un contrôle du parcours le vérifie à
   chaque exécution, pour que ça se voie si la barre s'élargit un jour.

   Seule exception, conservée : une fenêtre d'ORDINATEUR rendue très étroite.
   À la souris, la barre porte un bouton de plus (« Mode rapide », retiré du
   tactile) et devient assez large pour toucher le retour sous 420 px. Là
   seulement, on le remonte comme avant.                                       */
/* (La règle qui remontait le retour au-dessus de la barre basse sur écran
   étroit a été retirée le 2026-08-14 : il n'y a plus de barre basse à éviter,
   le retour est seul dans le coin.) */

@media (prefers-reduced-motion: reduce) {
  .fab-retour { transition: none; }
}

/* ==========================================================================
   FILTRE DE LISTE — rendre visible ce qu'il fait

   Adrien : « les filtres je vois PAS DU TOUT ce que ça fait ». Deux causes :
   le bouton ne changeait que de libellé, et sur une liste où rien n'est coché
   « ce qu'il me manque » affiche exactement la même chose que « tout ».
   D'où : un bouton qui change de COULEUR dès qu'un filtre est actif, et une
   ligne de texte dans la page qui dit ce qu'on regarde.
   ========================================================================== */

/* Le QUATRIÈME jaune de la zone, et le plus injustifié : cette phrase
   explique, elle n'alerte pas. En jaune vif elle pesait autant que les cases
   cochées, dont elle ne parle même pas. Elle redevient une note de bas de
   ligne — lisible, secondaire, à sa place. */
.list-filtre {
  margin: 0 0 6px;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
}
.list-filtre[hidden] { display: none; }

/* ==========================================================================
   LE COMPTE, SUR L'ACCUEIL — un bandeau, pas une carte

   « Pourquoi y a pas de bandeau bouton de connexion sur la page d'accueil ? »
   (Adrien, 2026-08-17). Depuis que la sauvegarde par fichier a été retirée, le
   compte est le SEUL moyen de retrouver sa collection ailleurs.

   ⚠️ PREMIÈRE VERSION REJETÉE, et à juste titre : une carte blanche de 225 px
   portant un bouton PLEIN — donc l'élément le plus contrasté de toute la page —
   pour ce qui n'est qu'un confort. Elle criait plus fort que « Ma collection »,
   et sur iPhone SE elle repoussait la première liste hors de l'écran (mesuré :
   première liste à 649 px sur 568). Le référentiel du skill impeccable le dit
   en une ligne : « les cartes sont le conteneur paresseux », et l'accueil en
   empile déjà.

   Donc : un bandeau teinté, un bouton CERCLÉ (secondaire, comme il se doit), et
   « Se connecter » descendu au rang de lien. Aucun jaune — #FEF43D ne veut dire
   qu'une chose, « je possède cet item ».
   ========================================================================== */

.home-compte {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--esp-sm) var(--esp-md);
  /* La marge BASSE n'est pas décorative : sans elle, le bandeau touchait le
     champ de recherche — 0 px d'écart, mesuré, alors que tout le reste de
     l'accueil respire de 22 px. Les deux se lisaient comme un seul bloc collé. */
  /* ⚠️ PAS de `width: calc(100% - 32px)` ICI, contrairement à `.banniere`.
     Ce bandeau-là vit DANS `#main`, qui porte déjà 16 px de marge intérieure ;
     reprendre le gabarit de `.banniere` (qui, elle, est en dehors et doit donc
     s'insérer elle-même) comptait la marge DEUX FOIS. Résultat mesuré : le
     bandeau commençait à 32 px des bords quand la recherche, les cartes, les
     titres et le pied sont tous à 16 — seul élément désaligné de l'accueil. */
  margin: var(--esp-md) 0 var(--esp-lg);
  /* ⚠️ NI FOND BLANC, NI COINS ARRONDIS — mesuré, il portait EXACTEMENT le même
     `rgb(255,255,255)` et le même rayon de 14 px que la carte « Ma collection »
     juste dessous. Trois conteneurs arrondis identiques s'empilaient donc, et
     rien ne disait lequel est le contenu et lequel n'est qu'une proposition.
     Deux filets suffisent : c'est un bandeau, pas une carte. */
  padding: 14px 0;
  border-top: 1px solid var(--c-trait);
  border-bottom: 1px solid var(--c-trait);
  /* ⚠️ SURTOUT PAS `--c-accent-voile` NI `--c-accent-trait` : ce sont des
     voiles de #FEF43D. Posés ici, ils faisaient un bandeau JAUNE — la couleur
     qui ne veut dire qu'une chose dans ce guide, « je possède cet item » — et
     ce, sous un commentaire qui affirmait le contraire. Vu en REGARDANT la
     capture, pas en relisant le code : le nom d'un jeton ne dit pas sa
     couleur. */
}

/* ⚠️ `100%` : la phrase prend TOUJOURS sa propre ligne, les boutons passent
   dessous. En la laissant partager la ligne (`1 1 210px`), la mise en page
   basculait entre 390 et 430 px — et à 430, c'est-à-dire sur les iPhone Pro
   Max, il ne restait que 34 px de large à la phrase, qui s'enroulait sur cinq
   lignes. Une mise en page qui change de comportement au milieu des largeurs
   réelles est une mise en page qu'on ne peut pas vérifier. */
.home-compte__texte {
  flex: 1 1 100%;
  min-width: 0;
  margin: 0;
  text-align: center;   /* la phrase et les deux boutons sur le même axe */
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.45;
}
.home-compte__texte strong { color: var(--c-texte); font-weight: var(--tres-gras); }

/* `1 1 auto` et non `0 0 auto` : à 320 px, les deux boutons côte à côte font
   325 px pour 258 px disponibles. Sans droit de rétrécir, la rangée débordait
   de l'écran au lieu de passer les boutons l'un sous l'autre. */
/* Une grille et non une rangée souple : « Connexion » (133 px) et « Créer un
   compte » (184 px) avaient 51 px d'écart, ce qui se voit immédiatement sur
   deux boutons côte à côte. `auto-fit` leur donne la même largeur quand les
   deux tiennent, et les empile sinon — sans jamais couper un libellé. */
.home-compte__actions {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: var(--esp-sm);
}

/* ⚠️ `nowrap` ET `flex: 0 0 auto` — LE DÉFAUT DE LA CAPTURE D'ADRIEN.
   La version précédente donnait `flex: 1 1 150px` aux deux boutons : à 430 px
   de large (iPhone 14/15/16 Pro Max, le sien), ils tenaient côte à côte à
   156 px chacun — trop étroit pour « CRÉER UN COMPTE », qui passait sur deux
   lignes. Le défaut n'existe QUE au-dessus de 430 px : à 390 et 320 px les
   boutons s'empilent et tout va bien. Mes deux largeurs de contrôle étaient
   justement 320 et 390 — le défaut vivait exactement entre les deux. */
.home-compte__actions .btn {
  white-space: nowrap;
  padding-inline: 12px;    /* de quoi tenir à deux dès 390 px de large */
  min-height: var(--cible-tactile);
}

.home-compte-etat {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: var(--esp-md) 0 0;   /* même piège que ci-dessus : on est dans #main */
  color: var(--c-texte-doux);
  font-size: var(--taille-minuscule);
}

.home-compte-etat__point {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: var(--r-plein);
  background: var(--c-jauge);
}

.home-compte-etat__lien {
  min-height: var(--cible-tactile);
  padding: 0 2px;
  background: none;
  border: 0;
  color: var(--c-texte);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  touch-action: manipulation;
}

/* ==========================================================================
   MODE CORRECTION — le bandeau permanent et les trois étapes

   Première version : on activait le mode, et plus rien ne le rappelait.
   Des crayons apparaissaient sans qu'on sache pourquoi, et on ne savait ni
   combien de corrections attendaient, ni comment en sortir.
   ========================================================================== */

.banniere {
  position: relative;
  z-index: 1;                       /* au-dessus des couches de décor du fond */
  display: flex;
  align-items: stretch;
  gap: 2px;
  /* Aligné sur le contenu : `.gdi-main` fait au plus --largeur-max et porte
     16 px de marge intérieure de chaque côté. */
  width: calc(100% - 32px);
  max-width: calc(var(--largeur-max) - 32px);
  margin: var(--esp-md) auto 0;
  border-radius: var(--r-md);
  background: var(--c-accent-voile);
  border: 1px solid var(--c-accent-trait);
  overflow: hidden;
}
.banniere[hidden] { display: none; }

.banniere__voir {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--esp-md);
  min-height: 52px;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.banniere__voir:hover { background: var(--c-survol); }

.banniere__icone {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: var(--r-plein);
  background: var(--c-coche);
  color: var(--c-coche-marque);
  font-size: 15px;
}

.banniere__texte { display: grid; gap: 1px; min-width: 0; }
.banniere__texte strong { color: var(--c-accent-texte); font-size: var(--taille-petit); }
.banniere__texte span { color: var(--c-texte-doux); font-size: var(--taille-minuscule); }
.banniere__fleche { flex: none; color: var(--c-texte-doux); font-size: 20px; }

.banniere__quitter {
  flex: none;
  min-height: 52px;
  padding: 0 14px;
  background: transparent;
  border: 0;
  border-left: 1px solid var(--c-accent-trait);
  color: var(--c-texte-doux);
  font: inherit;
  font-size: var(--taille-minuscule);
  cursor: pointer;
}
.banniere__quitter:hover { color: var(--c-texte); background: var(--c-survol); }

/* --- Le gros interrupteur en haut de l'écran Corrections ------------------ */



/* --- Les trois étapes ---------------------------------------------------- */



/* ==========================================================================
   CORRECTIFS D'AUDIT — 2026-08-13 (nuit)
   Chaque bloc porte le défaut qu'il répare, pour qu'on ne le défasse pas par
   inadvertance en croyant nettoyer.
   ========================================================================== */

/* Le message flottant doit passer AU-DESSUS du retour flottant.
   Sinon le coin du bouton « Annuler » — le seul moyen de rattraper 151
   cochages accidentels — tombe sous le bouton de retour, et le doigt qui le
   vise quitte l'écran en emportant le message. */
.toast { bottom: calc(180px + env(safe-area-inset-bottom, 0px)); }

/* Le bandeau du mode correction SUIT le défilement.
   En position normale il sortait de l'écran dès le premier glissement : dans
   une liste de 251 items, autant dire qu'il n'existait pas — et c'est
   précisément le rappel permanent qui manquait. */
.banniere {
  position: sticky;
  top: var(--hauteur-entete);
  /* UN ÉTAGE AU-DESSUS des en-têtes de série, pas le même : à égalité, les
     deux se collaient au même endroit et s'empilaient — « SÉRIE 1 », « Mode
     correction », « 0/230 », « TOUT COCHER » et « Quitter » dans une bande de
     60 px, tout illisible. Les en-têtes de série descendent maintenant de la
     hauteur de ce bandeau (`--hauteur-banniere`, posée par
     js/ui/banniere-correction.js). */
  z-index: calc(var(--z-serie) + 1);
}

/* Le bouton de filtre annonçait son RÔLE sur une ligne au-dessus de son état
   (« FILTRE » / « TOUT ») : seul au milieu de la barre basse, « TOUT » se
   lisait comme un troisième « Tout cocher ». Le rôle est maintenant porté par
   l'icône et par la place — dans la barre de recherche, à côté du champ — donc
   la surétiquette n'a plus lieu d'être. */

/* Le numéro d'origine dans les résultats d'ajout.
   Trois lignes « Pikachu / STAKS · Série 1 » strictement identiques étaient
   impossibles à départager — alors que la recherche globale, elle, affiche le
   numéro et les distingue parfaitement. */

/* Le dernier item d'une liste ne doit pas rester coincé sous le retour flottant. */
/* La colonne « − n + » s'écarte du coin où vit le retour flottant.
   -------------------------------------------------------------------------
   Mesuré : sans cette réserve, le bouton « + » d'une ligne est recouvert à
   81-88 % par le retour selon la position de défilement. Sur une liste
   « Doublons », « + » EST le geste, et le pouce d'une main droite tombe
   exactement là : on croit ajouter un exemplaire, on se retrouve à l'écran
   précédent sans comprendre pourquoi.
   Les écrans du guide n'ont pas ce problème (leur case à cocher est à gauche,
   le bord droit ne porte aucune commande) — la réserve ne concerne donc que
   les listes personnelles et l'écran de réception d'un partage.
   62 px = les 54 px du bouton + son écart au bord, moins le retrait déjà
   présent sur la ligne.                                                      */
/* Le nom de la série, à droite de l'en-tête d'un groupe de liste personnelle.
   -------------------------------------------------------------------------
   Cette classe n'existait nulle part dans la feuille de style : elle héritait
   donc du `h2` qui la contient, et s'affichait en Archivo 24 px GRAS BLANC —
   soit 33 % plus gros que le nom de la liste juste à sa gauche (Oswald 18 px),
   dans la mauvaise police, et en touchant le bord. Le secondaire criait plus
   fort que le principal. Aligné sur son équivalent de l'écran de liste. */
.group__meta {
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  letter-spacing: var(--lettres-capitales);
  min-width: 0;
  text-align: right;
}

.perso-items { padding-right: 62px; }

#list-body, .perso-items { padding-bottom: 8px; }
#list-body { padding-bottom: 72px; }

/* ==========================================================================
   « Nouvelle liste » — dernière ligne de la carte, pas un bouton posé dessous.

   Le défaut réparé, signalé par Adrien : un bouton ordinaire est une PILULE
   (rayon 99 px) ; il était collé, sans le moindre espace, sous une carte au
   rayon de 14 px. Deux formes étrangères jointes bord à bord.

   On ne corrige pas l'écart, on supprime la jointure : la création devient une
   ligne de la carte. Un seul bloc, un seul rayon, plus rien à accorder.
   ========================================================================== */

/* L'action ne doit pas peser autant qu'un nom de liste : à taille et graisse
   égales, « NOUVELLE LISTE » se lisait comme une troisième liste. Elle garde la
   police d'affichage pour rester dans la famille, en plus petit et plus discret. */
.menu-row--ajout { color: var(--c-bloc-doux); }
.menu-row--ajout:hover { color: var(--c-bloc-texte); }
.menu-row--ajout .menu-row__name {
  font-size: var(--taille-petit);
  letter-spacing: 0.06em;
}

.menu-row__plus {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-piste);
  color: var(--c-bloc-texte);
  font-size: 13px;
  line-height: 1;
}
.menu-row--ajout:hover .menu-row__plus { border-color: var(--c-bloc-texte); }

/* Le champ de saisie occupe la même ligne, sans casser la hauteur de la carte. */
.menu-row--saisie {
  display: flex;
  gap: var(--esp-sm);
  align-items: center;
  cursor: default;
}
/* 44 px : la règle du projet, qui n'avait pas été appliquée ici (42 px). */
.menu-row--saisie input {
  min-height: var(--cible-tactile);
  flex: 1;
  min-width: 0;
  height: 42px;
  padding: 0 12px;
  border-radius: var(--r-sm);
  /* 2 px EN PERMANENCE, seule la couleur change au focus : à 1 px l'indicateur
     de focus clavier passe sous le seuil WCAG 2.4.11, et l'épaissir seulement
     au focus fait sauter la mise en page. Correctif déjà appliqué au champ de
     recherche, jamais reporté ici (audit 2026-08-14). */
  border: 2px solid var(--c-champ-trait);
  background: var(--c-fond);
  color: var(--c-texte);
  font: inherit;
}
.menu-row--saisie input:focus { border-color: var(--c-focus); outline: none; }
.menu-row--saisie .btn { flex: none; }

/* ==========================================================================
   DESCRIPTIONS DE LISTES — « ya plein de gens qui vont pas savoir ce que sont
   chacun des items comme les boomers » (Adrien).

   Deux emplacements : sur la LIGNE (là où l'on choisit quoi ouvrir, donc là où
   la question se pose) et sur l'ÉCRAN de la liste (la phrase entière).
   Volontairement discrètes : c'est une aide, pas le sujet.
   ========================================================================== */

.menu-row__texte { flex: 1; min-width: 0; display: grid; gap: 2px; }

/* Zoom du texte à 200 % : la largeur utile tombe à ~195 px, et un mot long
   (« KELLOGG'S ») sortait de sa boîte pour s'écrire PAR-DESSUS le compteur.
   Mesuré : 12 noms sur 12 débordaient, et la page elle-même dépassait de
   14 px. On coupe le mot plutôt que la mise en page. */
.menu-row__name { overflow-wrap: anywhere; }

/* Sous 260 px de large utile (soit 320 px zoomés ×2), l'anneau ne sert plus à
   rien : il répète le compteur juste à côté, et il vole la place du nom. */
@media (max-width: 260px) {
  .menu-row { flex-wrap: wrap; }
  .menu-row__ring { display: none; }
}
.menu-row--decrit { align-items: flex-start; padding-top: 12px; padding-bottom: 12px; }
.menu-row--decrit .menu-row__meta,
.menu-row--decrit .menu-row__ring { margin-top: 2px; }

.menu-row__desc {
  color: var(--c-bloc-doux);
  font-size: var(--taille-minuscule);
  line-height: 1.35;
  /* DEUX lignes, pas une.
     Sur une seule ligne, les douze descriptions étaient coupées en plein
     milieu — mesuré : 11 sur 11. « Les pièces en plastique fournies avec les
     de… » n'apprend rien à personne, et c'est précisément à ceux qui ne savent
     pas que cette phrase s'adresse. Deux lignes rallongent la carte d'environ
     240 px au total ; l'information vaut ce prix. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* La description est une note de contexte, pas un paragraphe : elle se lit une
   fois. Resserrée pour ne pas repousser les items d'une ligne entière. */
.list-desc {
  margin: 0 0 var(--esp-sm);
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.5;
}

/* ==========================================================================
   L'ATELIER — modifier le catalogue depuis le téléphone

   « Une liste Excel, mais en bien plus stylée » (Adrien). Un tableau, donc,
   mais pas un tableau de bureau plaqué sur un téléphone :
     - trois colonnes seulement (numéro, nom, action) — au-delà, ça déborde
       ou ça devient illisible sur 320 px ;
     - les cases se modifient SUR PLACE, sans écran intermédiaire : c'est ce
       qui fait la différence entre saisir une page de 40 items en une minute
       et abandonner au bout de trois ;
     - chaque champ garde 44 px de haut, comme partout ailleurs — on saisit
       debout, au pouce.
   ========================================================================== */



/* Le numéro est court : lui donner la même largeur que le nom ferait viser une
   cible de 200 px pour y taper deux chiffres. */


/* Le tableau. `overflow-x` en filet : sur 320 px avec un nom très long, on
   préfère un défilement horizontal DANS le tableau à une page qui déborde. */

/* La colonne du nom prend ce qui reste — et coupe DANS le mot s'il le faut :
   certains noms d'items dépassent 60 caractères sans une seule espace, et la
   colonne poussait alors le tableau hors de l'écran. */


/* Une ligne AJOUTÉE se voit : c'est ce qui n'est pas encore chez les autres. */

/* Un nom publié n'est pas modifiable ICI — il ne se déguise donc pas en champ
   de saisie. Le ✎ à côté ouvre le mode correction, qui, lui, sait le faire
   sans casser les cases cochées. */


/* ---------------------------------------------------------------------------
   RANGER L'ACCUEIL — masquer des listes, changer leur ordre
   ---------------------------------------------------------------------------
   Ajouté le 2026-08-14, quand l'accueil est passé de 18 à 42 listes : « les
   collectionneurs pourraient cacher certaines listes qui ne les intéressent pas
   ou modifier l'ordre » (Adrien).

   POURQUOI DES FLÈCHES ET PAS DU GLISSER-DÉPOSER. Le guide s'utilise debout,
   d'une seule main, en brocante. Le glissé long est déjà pris — c'est le geste
   de cochage multiple (js/ui/checking.js) — et il entre en conflit avec le
   défilement, ce qui a déjà coûté une régression sur cet écran. Deux boutons
   qui déplacent d'un cran ne demandent aucune précision, se testent, et
   s'annoncent correctement à un lecteur d'écran.

   Toutes les cibles font 44px : c'est la règle du projet, pas un arrondi.
   --------------------------------------------------------------------------- */

.home-section__tete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--esp-sm);
}

/* Quand le titre partage sa rangée avec une action (« Ranger »), le filet
   court entre les deux au lieu d'aller au bord : il sépare toujours, et il
   rattache l'action à SA zone plutôt qu'au vide. */
.home-section__tete .home-section__title { flex: 1 1 auto; margin-bottom: 0; }
.home-section__tete { margin-bottom: var(--esp-sm); }

.home-section__action {
  flex: none;
  min-height: var(--cible-tactile);
  padding: 0 12px;
  margin: 0;
  background: transparent;
  border: 0;
  color: var(--c-texte-doux);
  font-family: var(--police-texte);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: var(--r-sm);
}

.home-section__action:hover { color: var(--c-texte); background: var(--c-survol); }
.home-section__action:active { background: var(--c-appui); }

.organiser-aide,
.organiser-note {
  margin: 0 0 12px;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.5;
}

.organiser-note { margin: 10px 0 0; }

.organiser-liste {
  list-style: none;
  margin: 0;
  padding: 0;
}

.organiser-row {
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  min-height: var(--cible-tactile);
  padding: 8px var(--esp-md);
  border-bottom: 1px solid var(--c-bloc-trait);
  color: var(--c-bloc-texte);
}

.organiser-row:last-child { border-bottom: 0; }

/* Masquée : on l'estompe SANS la rendre illisible — c'est ici, et seulement
   ici, qu'on peut la retrouver pour la réafficher. D'où 0.55 et pas 0.3, et
   un texte qui reste au-dessus du seuil AA. */
.organiser-row.est-masquee .organiser-nom { opacity: 0.55; }
.organiser-row.est-masquee .organiser-nom__texte { text-decoration: line-through; }

.organiser-nom {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.organiser-nom__texte {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  line-height: 1.15;
  overflow-wrap: anywhere;
}

/* `.organiser-fleche` (les flèches monter/descendre) partageait ces quatre
   règles. Le rangement de l'accueil se fait au glisser-déposer depuis le
   2026-08-13, les flèches n'existent plus dans le HTML : sélecteurs retirés
   le 2026-08-14 avec le reste du CSS mort. */
.organiser-oeil {
  flex: none;
  width: var(--cible-tactile);
  height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--c-texte-doux);
  cursor: pointer;
}

.organiser-oeil svg { width: 22px; height: 22px; }

.organiser-oeil:hover { background: var(--c-survol); color: var(--c-texte); }

.organiser-oeil:active { background: var(--c-appui); }

/* L'œil OUVERT (liste visible) est plein contraste : c'est l'état normal. */
.organiser-row:not(.est-masquee) .organiser-oeil { color: var(--c-texte); }

.organiser-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--esp-sm);
  margin-top: 14px;
}

/* Sous 360px, le nom pourrait écraser les trois boutons : on lui laisse la
   place en réduisant l'espacement, jamais la taille des cibles. */
@media (max-width: 360px) {
  .organiser-row { gap: 2px; padding: 8px 4px; }
}

/* ---------------------------------------------------------------------------
   AJOUTER À UNE LISTE PERSONNELLE — parcourir, filtrer, pointer
   ---------------------------------------------------------------------------
   Refonte du 2026-08-14. Avant, cet écran n'offrait qu'un champ de recherche :
   pour mettre quarante doublons de Tazos dans une liste, il fallait taper
   quarante noms. « Vraiment mal fichu » (Adrien), et il a raison — un
   collectionneur pense « ma page de Tazos », pas « Pikachu, puis Bulbizarre ».

   L'écran ouvre maintenant sur le SOMMAIRE des listes du guide. On en ouvre
   une, on pointe, on ressort. La recherche reste là pour l'ajout ponctuel, et
   trois filtres (Tout / Ce que j'ai / Ce qui me manque) réduisent la liste à ce
   qui a du sens : une liste de doublons ne contient que des items possédés.
   --------------------------------------------------------------------------- */

/* Les trois pastilles (Tout / Ce que j'ai / Ce qui me manque) et les blocs du
   sommaire ont été retirés le 2026-08-14 : cet écran passe désormais par le
   même filtre unique que les listes du guide, celui du bandeau. Leurs règles
   sont parties avec eux — commentaires d'origine conservés ci-dessus, ils
   expliquent POURQUOI l'écran est organisé ainsi, ce qui reste vrai. */

/* Le bouton de fin, toujours atteignable : c'est la sortie de l'écran, elle ne
   doit pas se trouver après quatre cents lignes de défilement. */

/* ---------------------------------------------------------------------------
   LA LISTE ACTIVE — « je coche dans… »
   ---------------------------------------------------------------------------
   « Ma collection et l'utilisation classique de l'application : quand tu coches
   un truc ça l'ajoute à ma collection, donc c'est une liste, donc c'est
   exactement la même chose » (Adrien, 2026-08-14).

   Cette remarque a supprimé un écran entier. Il n'y a plus de page pour
   remplir une liste : il y a une liste ACTIVE, et cocher dans le guide écrit
   dedans. Par défaut, c'est la collection.

   D'où deux exigences visuelles, et elles ne sont pas décoratives :
     · savoir en permanence où l'on coche (le bandeau, quand ce n'est pas la
       collection — sinon on remplit « Doublons » en croyant remplir sa
       collection, trois jours après avoir basculé) ;
     · changer de liste en un geste, depuis l'accueil.
   --------------------------------------------------------------------------- */

/* Quand on coche ailleurs que dans la collection, le bloc se signale — même
   couleur que la coche, pour qu'aucun autre code ne soit à apprendre. */

/* Le bandeau permanent, décliné pour la liste active : même forme que le mode
   correction, couleur de la coche au lieu de l'ambre.

   ⚠️ TOUT le texte doit être réécrit en sombre, pas seulement les boutons.
   Première version : seuls `__voir` et `__quitter` passaient en `#111`, et le
   sous-titre gardait son `--c-texte-doux` — c'est-à-dire du BLANC à 58 %, hérité
   du thème nuit. Sur ce jaune, il était illisible (rapport ~1,3:1, très
   au-dessous du seuil AA de 4,5:1). Signalé par Adrien, capture à l'appui.
   Les couleurs sont écrites en dur ici, et c'est voulu : le fond de ce bandeau
   est le jaune de la coche, identique dans les DEUX thèmes — un jeton de thème
   basculerait avec le thème alors que le fond, lui, ne bouge pas. */
#banniere-correction.est-liste {
  background: var(--c-coche);
  color: #111418;
}

#banniere-correction.est-liste .banniere__voir,
#banniere-correction.est-liste .banniere__quitter,
#banniere-correction.est-liste .banniere__icone,
#banniere-correction.est-liste .banniere__texte strong {
  color: #111418;                       /* 13,9:1 sur #FEF43D */
}

/* Le sous-titre reste hiérarchisé, mais lisible : noir à 72 % donne 10,4:1,
   très au-dessus du seuil. Ne pas le remonter vers du gris clair. */
#banniere-correction.est-liste .banniere__texte span,
#banniere-correction.est-liste .banniere__fleche {
  color: rgba(0, 0, 0, 0.72);
}

#banniere-correction.est-liste .banniere__quitter { border-color: rgba(0, 0, 0, 0.35); }

/* --- Bandeau « vous êtes sur une ancienne version » ---------------------- */
/* Rouge plein, identique dans les deux thèmes : ce n'est pas une nuance de
   l'interface, c'est une alerte, et elle doit se lire pareil partout. Les
   couleurs sont posées en dur (fond ET texte) pour que le contraste ne dépende
   d'aucun réglage — le blanc sur #8E1B12 donne 8,9:1, très au-dessus du seuil.
   Mesuré à l'écran par le test, comme le bandeau jaune : une valeur écrite ici
   ne prouve pas ce qui est rendu. */
#banniere-correction.est-perimee {
  background: #8E1B12;
  border-color: #6E140D;
  color: #FFFFFF;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 12px;
}
/* L'icône hérite d'une PASTILLE JAUNE du bandeau normal. Y poser du blanc
   donnait 1,15:1 — la même faute que le bandeau jaune (blanc à 58 %, 1,07:1),
   refaite au même endroit. Redéfinir la couleur du texte ne suffisait pas : il
   fallait redéfinir le FOND. Attrapé par le test qui mesure le contraste rendu,
   pas par la relecture — c'est précisément pour ça qu'il existe. */
#banniere-correction.est-perimee .banniere__icone {
  font-size: 20px;
  background: #FFFFFF;
  color: #8E1B12;                        /* 8,9:1 */
}
#banniere-correction.est-perimee .banniere__texte {
  flex: 1 1 200px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
#banniere-correction.est-perimee .banniere__texte strong { color: #FFFFFF; }
/* Blanc à 88 % sur ce rouge : 7,4:1. Ne pas descendre plus bas — c'est
   exactement l'erreur du bandeau jaune (blanc à 58 %, soit 1,07:1). */
#banniere-correction.est-perimee .banniere__texte span { color: rgba(255, 255, 255, 0.88); }
#banniere-correction.est-perimee .banniere__quitter {
  flex: none;
  color: #FFFFFF;
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: var(--r-sm);
  padding: 8px 12px;
  min-height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}
/* L'action principale — aller sur le vrai site — est pleine : les deux boutons
   au même niveau laisseraient croire qu'ils s'équivalent, alors que l'un est
   une précaution et l'autre la sortie. */
#banniere-correction.est-perimee .banniere__quitter--fort {
  background: #FFFFFF;
  color: #8E1B12;
  border-color: #FFFFFF;
  font-weight: var(--tres-gras);
}

/* Le champ « pseudo » des Réglages — facultatif, il voyage dans les liens
   qu'on envoie pour dire à qui appartient la liste reçue. */
.pseudo-champ { display: flex; flex-direction: column; gap: 6px; }

.pseudo-champ__label {
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
  text-transform: uppercase;
  letter-spacing: var(--lettres-capitales);
  font-weight: var(--gras);
}

/* Une phrase d'aide sous un champ — « 6 caractères minimum », et pourquoi.
   Elle vit SOUS le champ et pas dans son texte d'exemple : un exemple disparaît
   dès qu'on tape, c'est-à-dire précisément au moment où la règle sert. */
.pseudo-champ__aide {
  margin: 0;
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
}

/* ---------------------------------------------------------------------------
   LE COMPTE — écrans de connexion, « Mon compte », « Vos données »
   ---------------------------------------------------------------------------
   Tout est bâti sur `.settings-card`, `.champ` et `.btn` : aucun composant
   nouveau. C'est voulu — ces écrans se voient trois fois dans une vie
   d'utilisateur, ils n'ont aucune raison d'avoir un vocabulaire visuel à eux.
   Six règles suffisent, et ce sont toutes des règles de TEXTE.
   --------------------------------------------------------------------------- */

.compte-form { display: flex; flex-direction: column; gap: var(--esp-md); }

/* La zone de message est TOUJOURS dans le gabarit, même vide.
   Si on l'ajoutait au moment de l'erreur, son apparition pousserait le bouton
   vers le bas juste au moment où le doigt y revient — et on appuierait à côté,
   ou pire, sur autre chose. Elle occupe donc sa place dès le départ, sans
   hauteur imposée : vide, elle ne se voit pas. */
.compte-message { margin: 0; font-size: var(--taille-petit); }
.compte-message--erreur { color: var(--c-danger); }

/* L'état de la copie : une ligne de fait, jamais une alerte. Pas de réseau
   n'est pas une panne dans ce guide, c'est la situation normale d'une
   brocante — d'où le ton neutre, et surtout PAS de rouge ni de jaune. */
.compte-etat { margin: 0; font-size: var(--taille-petit); color: var(--c-texte-doux); }
.compte-etat--attente { color: var(--c-texte); font-weight: var(--gras); }
/* Une panne définitive : elle ne doit ni passer pour une attente passagère, ni
   emprunter le jaune du cochage. Le rouge du guide sert déjà aux actions
   destructrices — c'est le bon registre. */
.compte-etat--bloque {
  margin: 0 0 var(--esp-sm);
  padding: 10px 12px;
  border-radius: var(--r-md);
  background: var(--c-danger-trait);
  border: 1px solid var(--c-danger);
  color: var(--c-texte);
  line-height: 1.5;
}

.compte-pied {
  margin: var(--esp-md) 0 0;
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
}

/* ⚠️ COLLISION DE PRIORITÉ, et elle ne se voit qu'à l'écran.
   `.settings-card p` (une classe + un élément) l'emporte sur `.compte-pied` et
   `.compte-tertiaire` (une classe chacune) : dans une carte de réglages, ces
   deux-là perdaient leur marge du haut et venaient se COLLER au bouton juste
   au-dessus, sans le moindre espace. Rien dans le CSS ne le signale — les
   règles sont justes, c'est leur ordre de priorité qui décide, et il ne se lit
   pas.

   Trouvé deux fois, de deux façons différentes, et c'est ça qu'il faut retenir :
   `.compte-pied` en REGARDANT une capture de « Mon compte » (2026-08-17), et
   `.compte-tertiaire` par le contrôle écrit dans la foulée — sur les écrans de
   connexion et de création, ceux-là mêmes qu'on venait de refaire à la demande
   d'Adrien, et où personne n'avait rien vu. */
.settings-card .compte-pied,
.settings-card .compte-tertiaire { margin-top: var(--esp-md); }

/* Un lien au fil du texte. C'est un `<button>` et non un `<a>` : la navigation
   du guide passe par `data-go`, et un vrai lien ferait un second mécanisme.
   Il porte quand même les 44 px de cible tactile — la règle vaut pour tout ce
   qui se touche, y compris au milieu d'un paragraphe. */
.compte-lien {
  display: inline-flex;
  align-items: center;
  min-height: var(--cible-tactile);
  padding: 0;
  background: none;
  border: none;
  color: var(--c-texte);
  font: inherit;
  font-weight: var(--gras);
  text-decoration: underline;
  cursor: pointer;
  touch-action: manipulation;
}

/* ---------------------------------------------------------------------------
   LA CARTE DES LISTES — une seule, avec un point devant celle qui reçoit
   ---------------------------------------------------------------------------
   Refonte du 2026-08-14, deuxième passe. La première version affichait le nom
   de la liste active QUATRE fois sur le même écran : dans un bandeau, dans une
   carte « Ma collection », dans un sélecteur « Je coche dans… », et dans la
   section « Mes listes ». Adrien, capture à l'appui : « il y a de la redite,
   c'est inutile […] comme si tu devais fusionner tout ça dans un seul card ».

   Donc : UNE carte. Une ligne par liste, un point plein devant celle qui reçoit
   les coches, un chevron pour ouvrir le détail. Le bandeau ne s'affiche plus
   sur l'accueil (le point y suffit) — il reste sur les autres écrans, où rien
   d'autre ne dirait qu'on ne coche pas dans sa collection.
   --------------------------------------------------------------------------- */

.liste-card { padding: 0; }

/* ⚠️ CETTE RANGÉE A ÉTÉ UNE GRILLE, ET IL EN RESTAIT DES MORCEAUX.
   Passée en flex, elle gardait `grid-template-columns: 1fr auto` (inerte) et sa
   barre de progression gardait `grid-column: 1 / -1` (inerte aussi). Trois
   défauts en découlaient, tous visibles à l'écran et aucun signalé par un
   test — signalés par Adrien, eux :
     · le bouton de choix ne prenait que la largeur de son texte : la zone
       cliquable était minuscule « alors que plein de place » ;
     · son fond de survol s'arrêtait donc au milieu de la rangée, et sur
       iPhone ce fond RESTE après l'appui — d'où le rectangle plus clair qui
       coupe la ligne en deux ;
     · le chevron se collait à ce bord au lieu d'aller au bout de la rangée ;
     · et la barre de progression de « Ma collection », prévue pour occuper
       toute la largeur sous les deux colonnes, était écrasée à zéro par le
       flex — elle a donc simplement DISPARU, sans que rien ne le dise.
   La barre a depuis été retirée pour de bon (voir `blocListes` dans home.js) :
   la rangée n'a donc plus qu'UNE ligne, et toutes ont la même hauteur. */
.liste-ligne {
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--c-bloc-trait);
}

.liste-ligne:last-of-type { border-bottom: 1px solid var(--c-bloc-trait); }

.liste-ligne__choix {
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  /* Il prend TOUTE la place que le chevron ne prend pas : c'est la cible
     principale de la rangée, elle n'a aucune raison d'être plus petite. */
  flex: 1 1 auto;
  min-height: var(--cible-tactile);
  /* 16 px en haut comme en bas : c'est le rembourrage de `.menu-row`, dont
     « Nouvelle liste » est une instance juste en dessous dans la même carte.
     Sans ça les deux rangées ne faisaient pas la même hauteur (51 contre 58),
     ce qui se voit d'autant plus depuis que la barre de progression a disparu
     et que la carte n'a plus que ces deux lignes. */
  padding: 16px 0 16px var(--esp-md);
  min-width: 0;
  background: transparent;
  border: 0;
  color: var(--c-bloc-texte);
  text-align: left;
  cursor: pointer;
}

.liste-ligne__choix:hover { background: var(--c-bloc-survol); }

/* Le point. Vide = liste au repos ; plein et jaune = c'est elle qui reçoit.
   Même jaune que les coches : un seul code de lecture dans tout le guide. */
/* 26 px, comme le « ＋ » de « Nouvelle liste » juste en dessous : les deux
   cercles de la carte s'alignent, et les deux rangées font enfin la même
   hauteur au pixel près. */
.liste-ligne__point {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid var(--c-trait);
  position: relative;
}

.liste-ligne.est-active .liste-ligne__point {
  border-color: var(--c-coche);
}

.liste-ligne.est-active .liste-ligne__point::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--c-coche);
}

/* Le nom ne s'ÉTIRE pas : il prend sa largeur, le compte se pose juste après,
   et c'est la marge automatique du compte qui absorbe le reste de la rangée.
   Le laisser grandir écartait le nom de son compte de toute la largeur — deux
   colonnes là où il n'y a qu'une seule information. */
.liste-ligne__nom {
  flex: 0 1 auto;
  min-width: 0;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-liste);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  line-height: 1.15;
  overflow-wrap: anywhere;
}

/* Le compte se cale à la SUITE du nom, pas contre le chevron : collé au bord
   droit d'une rangée large, il se lirait comme une deuxième colonne. */
.liste-ligne__meta {
  flex: none;
  margin-right: auto;
  color: var(--c-bloc-doux);
  font-size: var(--taille-petit);
  font-variant-numeric: tabular-nums;
}

.liste-ligne.est-active .liste-ligne__meta { color: var(--c-bloc-texte); }

.liste-ligne__ouvrir {
  flex: none;
  width: var(--cible-tactile);
  min-height: var(--cible-tactile);
  background: transparent;
  border: 0;
  color: var(--c-bloc-doux);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

.liste-ligne__ouvrir:hover { background: var(--c-bloc-survol); color: var(--c-bloc-texte); }

.liste-card__aide {
  margin: 10px 2px 0;
  color: var(--c-texte-doux);
  font-size: var(--taille-petit);
  line-height: 1.5;
}

/* ---------------------------------------------------------------------------
   RÉORDONNER À LA POIGNÉE — remplace les flèches monter/descendre
   ---------------------------------------------------------------------------
   Les flèches déplaçaient d'un cran : sur 42 listes, remonter la dernière en
   tête coûtait 41 appuis. « C'est très compliqué de personnaliser correctement
   si on peut modifier aussi difficilement et avec autant de clics » (Adrien,
   2026-08-14). On glisse maintenant la poignée, comme dans n'importe quelle
   liste réordonnable de téléphone.

   ⚠️ `touch-action: none` est posé sur la POIGNÉE SEULE. Sur la ligne, il
   couperait le défilement dès qu'on y pose le doigt — c'est la régression déjà
   vécue sur `.item`, documentée dans CLAUDE.md. Ne pas l'élargir.
   --------------------------------------------------------------------------- */

.organiser-poignee {
  flex: none;
  width: var(--cible-tactile);
  height: var(--cible-tactile);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--c-texte-doux);
  cursor: grab;
  touch-action: none;          /* ICI, et nulle part ailleurs */
}

.organiser-poignee:hover { background: var(--c-survol); color: var(--c-texte); }
.organiser-poignee svg { width: 22px; height: 22px; }

/* La ligne qu'on tient : détachée du reste, pour qu'on voie où elle est. */
.organiser-row.est-saisie {
  background: var(--c-surface-haute);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  border-radius: var(--r-sm);
  position: relative;
  z-index: 2;
}

.organiser-row.est-saisie .organiser-poignee { cursor: grabbing; color: var(--c-texte); }

/* Pendant le glissé, on ne sélectionne rien par mégarde. */
body.range-en-cours { user-select: none; -webkit-user-select: none; }

/* Les lignes glissent à leur nouvelle place plutôt que de sauter — sauf si
   l'utilisateur a demandé moins d'animations. */
@media (prefers-reduced-motion: no-preference) {
  .organiser-row { transition: background-color 120ms ease; }
}

/* Le numéro, en mode vignettes. Passer une liste en photos lui faisait perdre
   sa numérotation — celle qui fait le lien avec le catalogue papier et sur
   laquelle s'appuie la recherche par numéro.

   ⚠️ CE BLOC N'EST PAS UN FLEX, ET C'EST VOLONTAIRE. Il l'a été jusqu'au
   2026-08-18 — `display:flex` + `flex-wrap:wrap` — et Mathieu l'a signalé :
   « faire attention aussi au texte ? ça se mange entre eux haha ». Deux
   dégâts, que le flex causait ensemble :

   1. Le numéro restait ORPHELIN sur sa ligne. Dès qu'un nom ne tenait plus à
      côté de lui, `wrap` renvoyait le nom entier à la ligne suivante — d'où
      « 61 » seul au-dessus de « MÉLOFÉE-MÉLODELFE », alors qu'en texte
      ordinaire le numéro et le premier mot tiennent ensemble.
   2. La tuile GRANDISSAIT, et sa case à cocher descendait d'un cran par
      rapport à ses voisines de rangée. Trois tuiles côte à côte, trois cases
      à trois hauteurs : c'est le « ça se mange entre eux ».

   En flux de texte simple, le numéro est un préfixe comme un autre : le
   navigateur coupe où il faut, et la ligne se remplit avant de passer à la
   suivante. Ne pas revenir au flex pour « mieux aligner » — c'est LUI qui
   désalignait. */
.item__nom-grille {
  display: block;
  text-align: center;
  min-width: 0;
  /* Aucun nom ne sort de sa tuile. `anywhere` et non `break-word` : les noms
     de Mathieu sont des chaînes à traits d'union sans espace
     (« HÉRICENDRE-FEURISSON-TYPHLOSION »), que `break-word` laissait déborder
     puisqu'il ne coupe qu'entre les mots. */
  overflow-wrap: anywhere;
}

.item__num--grille {
  /* Marge et non `gap` : le gap appartenait au flex, qui n'est plus là. */
  margin-right: 6px;
  opacity: 0.72;
  font-variant-numeric: tabular-nums;
}

/* LES CASES S'ALIGNENT SUR LA RANGÉE, PAS SUR LEUR TEXTE.
   Les tuiles d'une même rangée ont déjà la même hauteur (grille CSS, étirement
   par défaut) — mais leur contenu, lui, était poussé par le texte au-dessus.
   Un nom sur deux lignes suffisait à faire descendre sa case. Collée en bas,
   la case retrouve la même hauteur que ses voisines quelle que soit la
   longueur du nom. */
.items--grid .item__box { margin-top: auto; }

/* Version et adresse, dans « À propos ». Discret mais lisible : c'est ce qu'on
   vient chercher quand on doute d'être sur la dernière version. */
.settings-version { font-variant-numeric: tabular-nums; }
.settings-version strong { color: var(--c-texte); }

/* Texte réservé aux lecteurs d'écran : présent dans le document, invisible à
   l'œil. `clip-path` plutôt que `display:none`, qui le retirerait aussi de la
   restitution vocale. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Pastille « cette liste a des photos », après le nom de la liste.
   RIEN n'est affiché quand la liste n'en a aucune : on signale ce qui existe,
   pas ce qui manque. Pleine = tout est illustré, contour = en partie — la
   FORME porte l'information, pas seulement la couleur (WCAG 1.4.1). */
.menu-row__photos {
  flex: none;
  display: inline-flex;
  align-items: center;
  margin-left: 6px;
  color: var(--c-bloc-doux);
  vertical-align: middle;
}

.menu-row__photos svg { width: 15px; height: 15px; display: block; }

/* Entièrement illustrée : un peu plus present, sans changer de teinte — le
   jaune du guide reste réservé au cochage. */
.menu-row__photos.est-complet { color: var(--c-bloc-texte); }


/* ==========================================================================
   20. L'ARRIVÉE D'UN ÉCRAN
   --------------------------------------------------------------------------
   « Je crois que tu n'as pas trop réfléchi aux animations, pour rendre le site
   plus vivant, notamment au chargement de page » (Adrien, 2026-08-15). C'était
   exact : le guide n'avait AUCUNE animation d'entrée, seulement des transitions
   d'état. Chaque écran apparaissait d'un coup.

   UN SEUL MOUVEMENT, PAS UNE COLLECTION D'EFFETS. Le geste du guide est de
   cocher vite, debout, parfois 261 fois de suite : une interface qui bouge à
   chaque action deviendrait fatigante avant la fin de la première série. Le
   mouvement est donc réservé au moment où l'écran CHANGE, et à rien d'autre.

   TROIS RÈGLES QUI NE SE VOIENT PAS DANS LE RÉSULTAT :

   1. Ailleurs que sur l'accueil, seule l'OPACITÉ bouge. Un `transform`, même
      terminé, fait de l'élément un bloc de référence pour ses descendants et
      casserait les en-têtes de série collants d'un écran de liste — la
      régression de confinement déjà vécue sur ce projet.
      ⚠️ Et le remplissage est `backwards`, pas `both`. Avec `both`, la valeur
      finale RESTE appliquée : `transform: none` en image-clé se calcule alors
      en `matrix(1, 0, 0, 1, 0, 0)`, c'est-à-dire un transform bien présent, et
      le bloc de référence subsiste pour toujours. Mesuré : les sept blocs de
      l'accueil le portaient. Avec `backwards`, l'élément retrouve son état
      naturel dès la fin — identique au pixel, sans la trace.

   2. L'accueil, lui, a droit au décalage : il n'y a rien de collant dedans, et
      c'est l'écran où l'on arrive — le seul qui gagne à se présenter.

   3. Le décalage est PLAFONNÉ à cinq blocs. Échelonner une liste de 40 lignes
      donnerait une cascade qu'il faudrait attendre ; ici la page entière est
      posée en 0,6 s quoi qu'elle contienne.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  .gdi-main.entre { animation: gdi-fondu var(--duree-entree) var(--courbe-entree) backwards; }

  /* L'accueil remplace le fondu d'ensemble par le sien, bloc par bloc. */
  body.is-home .gdi-main.entre { animation: none; }
  body.is-home .gdi-main.entre > * {
    animation: gdi-monte var(--duree-entree) var(--courbe-entree) backwards;
  }
  body.is-home .gdi-main.entre > *:nth-child(2) { animation-delay: 60ms; }
  body.is-home .gdi-main.entre > *:nth-child(3) { animation-delay: 110ms; }
  body.is-home .gdi-main.entre > *:nth-child(4) { animation-delay: 150ms; }
  body.is-home .gdi-main.entre > *:nth-child(n+5) { animation-delay: 180ms; }

  /* Dans l'affiche, le titre monte un souffle après elle : la page de garde se
     compose au lieu d'apparaître d'une pièce. */
  body.is-home .gdi-main.entre .home-affiche__titre {
    animation: gdi-monte var(--duree-entree) var(--courbe-entree) 90ms backwards;
  }

  /* Le dépliant « vide » / « scellé ». C'est le seul geste de l'accueil qui
     ouvrait sec — douze lignes apparaissaient d'un coup sous le doigt. */
  .home-groupe.est-ouvert .home-groupe__corps {
    animation: gdi-monte var(--duree-moyenne) var(--courbe-entree) backwards;
  }
}

@keyframes gdi-fondu {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes gdi-monte {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}


/* ==========================================================================
   21. L'ARRIVÉE — l'animation d'accueil, PLEIN ÉCRAN
   ==========================================================================

   Le récit et le pourquoi sont dans `js/intro.js`. Ici, ce qui décide de
   l'effet et de la fluidité.

   ─── REFAITE LE 2026-08-18, ET VOICI CE QUI CLOCHAIT ────────────────────────

   « Trop rapide, pas assez organique et vivante et chaotique. Un humain doit
   pouvoir l'apprécier. De plus elle n'est pas sur toute la page, juste au
   milieu, c'est pas assez impressionnant du tout. Je lui mets 4/10, je veux du
   10/10 » (Adrien). Trois reproches, trois causes distinctes :

   1. PAS PLEIN ÉCRAN. Une grille de 36 tuiles de 48 px au centre occupait 15 %
      d'un écran de téléphone ; les 85 % restants étaient du bleu nuit vide. On
      ne peut pas impressionner avec 15 % d'un écran. Le champ déborde
      désormais des quatre côtés, en tuiles de 86 px.
   2. TROP RAPIDE. Le vol d'une tuile durait 315 ms et la vague traversait en
      140 ms : de quoi voir qu'il se passe quelque chose, pas de quoi le
      regarder. Le vol passe à 690 ms, la cascade s'étale sur ~500 ms, et
      l'ensemble sur ~3,3 s.
   3. PAS ORGANIQUE. Tout atterrissait sur une grille parfaite, à la même
      taille, parfaitement droit. Quatre désordres corrigent ça, tous tirés
      d'une graine (donc reproductibles) : la position est décalée au hasard
      de quelques pixels, la taille varie de ±14 %, chaque tuile GARDE une
      inclinaison résiduelle au lieu de se redresser, et le retard de la vague
      est brouillé pour que la diagonale ne soit jamais une ligne nette.
   4. ET SURTOUT : RIEN NE BOUGEAIT UNE FOIS POSÉ. Une image figée n'est pas
      vivante. Chaque tuile respire ensuite en boucle — quelques pixels, un
      degré — avec une période propre. C'est ce qui fait la différence entre
      une capture et une scène.

   ─── REPRISE DU 2026-08-18 (SOIR) — LE SOL CÈDE ────────────────────────────

   Deux demandes se sont enchaînées le même soir, et la seconde a remplacé ma
   réponse à la première. Les deux sont écrites ici, parce que c'est le chemin
   qui explique le résultat.

   1er temps — « fais apparaître le titre au milieu, et laisse les images de
   fond, avec un dégradé d'opacité qui diminue en intensité comme la page
   d'accueil » : j'ai supprimé la résorption des tuiles et posé un VOILE
   par-dessus le champ. Ça marchait, et ça coûtait cher — voir le piège du
   contraste plus bas, qui reste vrai et qui a laissé une trace : l'écran local
   du titre.

   2e temps — Adrien : « je me dis qu'il y a peut-être un truc plus simple et
   plus joli. Le titre apparaît au milieu à un endroit où c'est extra vide,
   juste pour le titre, et les décors sont tout autour, puis les jetons tombent
   comme s'il y avait la gravité qui les faisait sortir de l'écran par le bas,
   alors que le titre remonte, lui, au bon endroit. […] Du coup, plus de
   questions d'opacité. » Il a raison, et sur trois points mesurables :

   · la lisibilité cesse d'être un problème AU LIEU d'être résolue. Le titre
     naît dans un trou et monte dans un écran qui se vide : il n'y a bientôt
     plus rien derrière lui. Tout l'appareillage de voile devient inutile ;
   · tomber n'est pas se résorber. Une tuile qui rétrécit sur place se lit
     comme un effacement ; une tuile qui tombe est un ÉVÉNEMENT, et cinquante
     objets qui tombent chacun à sa vitesse sont chaotiques par nature — sans
     qu'on ait à fabriquer le chaos ;
   · deux mouvements contraires en même temps (tout descend, une chose monte)
     est une composition, là où « le titre monte sur un fond fixe » n'en est
     pas une.

   ⚠️ LA CHUTE PART DU CENTRE, PAS DU HAUT. C'est ce qui creuse le trou au
   milieu — « un endroit où c'est extra vide, juste pour le titre » — à
   l'instant précis où le titre s'y ouvre. Une chute de haut en bas serait plus
   « physique » et raterait complètement l'effet demandé.

   3e temps — Adrien, en voyant que j'avais gardé un halo local derrière le
   titre : « il n'y a donc AUCUN dégradé d'opacité derrière le titre qui cache
   les items, car il ne veut rien derrière ». Et : « les objets, tu les fais
   apparaître partout SAUF sur un rectangle vide qui fait juste la taille du
   titre avec une petite marge […] le titre lui aussi est un objet physique, du
   coup les objets ne peuvent pas tomber dessus, ils sont donc repoussés vers
   l'extérieur pour tomber, comme du sable qui tombe sur une brique qui monte ».

   ⚠️ C'EST LA BONNE RÉPONSE, ET ELLE INVALIDE LES DEUX PRÉCÉDENTES. Un voile
   comme un halo ne font que CACHER ce qui passe derrière. Ici il n'y a rien
   derrière : la place du titre est réservée dès la construction du champ, et
   les tuiles qui tomberaient dessus sont ÉCARTÉES sur le côté avant de
   l'atteindre. La lisibilité n'est plus un réglage, c'est une conséquence.
   Ne pas réintroduire de voile ni de halo — ce serait rouvrir le problème.

   Le titre, lui, ne change pas de rôle : il NAÎT au centre, il FLOTTE, puis il
   MONTE — et tout ce mouvement se résout EXACTEMENT à zéro, sans quoi le
   raccord au pixel avec le titre de l'accueil serait faux.

   ─── CE QUI NE CHANGE PAS ──────────────────────────────────────────────────

   Uniquement `transform`, `opacity` et `clip-path` : jamais une propriété qui
   force un recalcul de mise en page. Aucun `will-change` : le poser sur une
   cinquantaine d'éléments réserverait autant de textures en mémoire vidéo, sur
   des téléphones que ce projet a déjà vus s'étrangler. Aucune courbe par
   défaut. Et tout le minutage des tuiles reste en CSS — le fil principal se
   bloque jusqu'à 550 ms d'affilée pendant que le catalogue s'analyse.
   ========================================================================== */

.intro {
  position: fixed;
  inset: 0;
  z-index: 70;
  touch-action: manipulation;
  overflow: hidden;
}

/**
 * LE FOND, SUR SA PROPRE COUCHE — et c'est ce qui rend la transition visible.
 *
 * « Je ne vois pas la page d'accueil se révéler derrière le titre ; elle
 * pourrait apparaître encore plus tôt, par exemple quand le titre commence à
 * monter, de manière progressive » (Adrien, 2026-08-18).
 *
 * ⚠️ IL ÉTAIT SUR `.intro` LUI-MÊME, ET C'ÉTAIT LE DÉFAUT. Tant que le fond
 * appartient au voile, la page derrière ne peut apparaître qu'en effaçant TOUT
 * le voile — items compris. La révélation démarrait donc derrière un aplat
 * encore opaque, et quand il partait enfin (240 ms) elle était déjà à moitié
 * faite : invisible. Sur sa propre couche, il peut s'effacer PENDANT que les
 * items tombent encore et que le titre monte — la page se découvre sous une
 * pluie d'items, ce qui est exactement la scène qu'on cherchait.
 *
 * Le fond est écrit EN DUR, pas `var(--c-fond)` : l'intro joue toujours en
 * nuit, même chez quelqu'un qui a choisi le thème jour. Le jaune du cochage sur
 * ce bleu est un phare ; sur le fond clair, un aplat pâle sur pâle.
 */
.intro__fond {
  position: absolute;
  inset: 0;
  /* ⚠️ LA COULEUR DE LA PAGE, PAS UN BLEU NUIT ÉCRIT EN DUR. *« L'animation avec
     le fond bleu marche bien si la page est en mode nuit, mais en mode jour ça
     fait bizarre »* (Adrien, 2026-08-18) — et pour cause : ce fond valait
     `#191C2F` dans les deux thèmes, donc quelqu'un en mode jour voyait un écran
     bleu nuit s'ouvrir au milieu d'une application claire.
     Rien à « détecter » pour autant : `js/theme-tot.js` pose `data-theme` AVANT
     la première image peinte (un contrôle le vérifie), donc les jetons portent
     déjà la bonne valeur au moment où l'intro se construit. Le défaut n'était
     pas un manque d'information, c'était une couleur écrite à la main. */
  background: var(--c-fond);
}
.intro--joue .intro__fond {
  animation: intro-fond-part 780ms var(--t-monte, 2600ms) both;
}
@keyframes intro-fond-part {
  from { opacity: 1; }
  to { opacity: 0; }
}

.intro-en-cours, .intro-en-cours body { overflow: hidden; }

/* LA SORTIE EST EN CSS, avec son propre retard — comme le reste. Une minuterie
   JavaScript posée pendant l'analyse du catalogue part en retard ; une
   animation CSS, non. Le JS ne fait plus que retirer l'élément APRÈS coup.
   Le retard exact est posé en variable par `js/intro.js` : il dépend du nombre
   de rangées, donc de la hauteur de l'écran. */
.intro--joue { animation: intro-fond 240ms var(--t-sortie, 3500ms) both; }
@keyframes intro-fond { from { opacity: 1; } to { opacity: 0; } }

/* Coupée au toucher : le JS éteint l'animation ci-dessus (une animation
   l'emporte sur une transition, il faut donc la retirer pour reprendre la
   main) et cette transition prend le relais. */
.intro--sortie {
  opacity: 0;
  transition: opacity 240ms cubic-bezier(0.4, 0, 1, 1);
}

/* --- le champ d'items, qui déborde de l'écran ---------------------------- */

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


/**
 * TROIS COUCHES, TROIS MOUVEMENTS — et ce n'est pas de l'emboîtement gratuit.
 *
 * Une même propriété ne peut porter qu'UNE animation. Or trois mouvements se
 * superposent sur une tuile, et deux d'entre eux n'ont même pas le même ordre
 * de départ : la vague d'arrivée va en DIAGONALE (`--retard`), la chute part
 * du CENTRE (`--chute`). Un seul élément ne peut donc pas les porter.
 *
 *   .intro__tuile → la place à l'écran, et LA CHUTE      (retard `--chute`)
 *   .intro__vol   → l'arrivée et le rebond du cochage    (retard `--retard`)
 *   .intro__img   → le visuel, la RESPIRATION, le filet qui passe au jaune
 */
.intro__tuile {
  position: absolute;
  left: var(--x, 0px);
  top: var(--y, 0px);
  width: var(--cote, 86px);
  height: var(--cote, 86px);
}


.intro__vol {
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(var(--rot, 0deg)) scale(0.55);
}
.intro__img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: var(--r-md);
  /* La tuile EST une tuile de l'application : même surface, même filet, même
     rayon que `.items--grid .item`. Pas une métaphore du guide — un fragment. */
  background-color: var(--c-surface);
  border: 1px solid var(--c-bordure);
  /* Une seule image pour les dix-huit items : une requête, un décodage, une
     texture partagée par toutes les tuiles.
     ⚠️ 6 colonnes × 3 rangées — la valeur vit aussi dans `js/intro.js` et dans
     `outils/construire-sprite-intro.js`. Les trois doivent concorder. */
  background-image: url('../assets/intro-items.webp');
  background-size: 600% 300%;
  background-position: calc(var(--sx, 0) * 100% / 5) calc(var(--sy, 0) * 100% / 2);
  background-repeat: no-repeat;
  position: relative;
}

/* La coche : le même jaune, le même signe que dans les listes. Un
   pseudo-élément plutôt qu'un nœud de plus — cinquante tuiles, ça ferait
   cinquante éléments de plus pour un carré de 20 px. */
.intro__img::after {
  content: '✓';
  position: absolute;
  right: -5px;
  bottom: -5px;
  width: 20px;
  height: 20px;
  border-radius: var(--esp-xs);
  background: var(--c-coche);
  color: var(--c-coche-marque);
  font-family: var(--police-texte);
  font-size: 13px;
  font-weight: var(--tres-gras);
  line-height: 20px;
  text-align: center;
  opacity: 0;
  transform: scale(0.4);
}

/* ⚠️ IL N'Y A PLUS NI REBOND NI COUCHE D'ÉCARTEMENT — et c'est la disparition
   qui compte. Le titre ne se fait plus de place : il l'a dès la première image
   (voir `js/intro.js`). Six mécaniques ont vécu ici — brique invisible, rebond
   au contact, éjection en cloche, lame qui se déplie, deux avances, solveur de
   trajectoires — chacune corrigeant la précédente. Quand la sixième corrige la
   cinquième, ce n'est plus un réglage qui manque, c'est le problème qui est mal
   posé. Ne pas les réintroduire sans avoir d'abord retiré la case réservée. */
.intro--joue .intro__tuile {
  animation: intro-chute var(--chute-duree, 900ms) var(--chute, 2000ms) forwards;
}

.intro--joue .intro__vol { animation: intro-vol var(--d-tuile, 2400ms) var(--retard, 0ms) both; }
.intro--joue .intro__img {
  /* DEUX animations sur le même élément — c'est permis tant qu'elles ne
     touchent pas à la même propriété : la respiration bouge `transform`, la
     vague change `border-color`. */
  animation: intro-respire var(--respire, 4s) var(--respire-decal, 0ms) infinite alternate ease-in-out,
             intro-filet var(--d-tuile, 2400ms) var(--retard, 0ms) both;
}
.intro--joue .intro__img::after { animation: intro-coche var(--d-tuile, 2400ms) var(--retard, 0ms) both; }

/* ⚠️ IL N'Y A PLUS DE CASE AU CENTRE — la place du titre est vide dès le
   départ (voir `js/intro.js`), donc plus de `.intro__tuile--centre` et plus de
   `intro-vol-centre`. Le récit « le nom sort de la dernière case cochée » est
   perdu, et c'est assumé : il exigeait une tuile là où il ne doit plus rien y
   avoir. */

/**
 * LA CHUTE — « les jetons tombent comme s'il y avait la gravité qui les
 * faisait sortir de l'écran par le bas » (Adrien).
 *
 * Une VRAIE chute, pas une translation : la courbe est accélérante d'un bout à
 * l'autre (c'est la définition d'une chute), et elle est précédée d'un petit
 * sursaut vers le HAUT de 5 px. Ce contre-mouvement ne dure que 7 % du temps
 * et c'est lui qui fait qu'on lit « ça lâche » plutôt que « ça descend ».
 *
 * Le chaos ne vient pas d'un désordre ajouté : il vient de ce que chaque tuile
 * a SA durée (780-1160 ms), SA dérive latérale (±65 px) et SA rotation
 * (±130°), toutes tirées de la même graine — donc reproductibles, donc
 * jugeables sur une capture.
 *
 * 130vh, et pas 100 : une tuile qui part du HAUT de l'écran doit encore le
 * traverser en entier. Elles parcourent toutes la même distance à la même
 * courbe, donc celles du bas sortent tôt et celles du haut tard — ce qui est
 * exactement ce que fait la gravité sur des objets lâchés à des hauteurs
 * différentes.
 */
@keyframes intro-chute {
  0% {
    transform: translate(0, 0) rotate(0deg);
    animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1);
  }
  7% {
    transform: translate(0, -5px) rotate(calc(var(--chute-rot, 40deg) * -0.05));
    /* Accélération franche et sans fin : rien ne freine une chute. */
    animation-timing-function: cubic-bezier(0.42, 0, 0.85, 0.6);
  }
  /* ⚠️ LE DÉPORT LATÉRAL SE FAIT ICI, PAS AU DÉPART. C'est la différence entre
     « elle tombe en biais » (elle savait où aller depuis le début, donc rien ne
     l'a poussée) et « elle est repoussée » — le titre est au milieu de l'écran,
     donc au milieu de la course. Les 15 % / 85 % concentrent l'essentiel du
     déplacement de côté sur ce tiers-là. La descente, elle, reste sur sa courbe
     d'accélération : les 18 vh et 52 vh sont les valeurs que donne cette courbe
     à 38 % et 62 % du temps — les changer désynchroniserait la chute. */
  38% {
    transform: translate(calc(var(--chute-x, 0px) * 0.72), 18vh)
      rotate(calc(var(--chute-rot, 40deg) * 0.22));
    animation-timing-function: cubic-bezier(0.5, 0, 0.6, 1);
  }
  62% {
    transform: translate(calc(var(--chute-x, 0px) * 0.96), 52vh)
      rotate(calc(var(--chute-rot, 40deg) * 0.68));
    animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.7);
  }
  100% {
    transform: translate(var(--chute-x, 0px), 130vh) rotate(var(--chute-rot, 40deg));
  }
}

/**
 * LE VOL — une seule animation qui porte toutes les phases de la tuile.
 *
 * Découper en quatre animations enchaînées par des minuteries donnerait quatre
 * occasions de se désynchroniser dès que le fil principal hoquette. Et il
 * hoquette, ici, pendant que le catalogue s'analyse.
 *
 *   0 →  3 %  elle apparaît, déjà en vol
 *   3 → 33 %  ARRIVÉE : de hors écran jusqu'à sa place, en tournant (690 ms)
 *  33 → 45 %  posée — et elle RESPIRE (voir `intro-respire`)
 *  45 → 55 %  COCHAGE : le seul rebond de toute la séquence
 *  55 → 100 % ELLE RESTE, cochée, et elle respire — jusqu'à ce que la GRAVITÉ
 *             la prenne (c'est `intro-chute`, sur l'élément parent).
 *
 * ⚠️ IL Y AVAIT UNE RÉSORPTION ICI, RETIRÉE LE 2026-08-18 (SOIR). Les tuiles
 * y rétrécissaient sur place, ce qui se lit comme un effacement et non comme
 * un départ. Elles tombent maintenant. Ne pas la réintroduire « pour faire
 * plus doux » : les deux se cumuleraient sur la même propriété, et la seconde
 * gagnerait en silence.
 *
 * ⚠️ ELLE NE SE REDRESSE PAS. À l'arrivée elle garde son inclinaison —
 * quelques degrés, propres à chaque tuile. C'est ce détail-là, plus que tout le
 * reste, qui fait la différence entre un tas d'objets et un damier.
 */
@keyframes intro-vol {
  0% {
    opacity: 0;
    transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(var(--rot, 0deg)) scale(0.55);
    /* Sortie exponentielle « à l'iOS » : départ franc, pose douce, aucun
       dépassement — une cinquantaine de tuiles qui rebondissent en arrivant,
       ce serait un tremblement, pas de la finesse. */
    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  }
  3% { opacity: 1; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  33% {
    opacity: 1;
    transform: rotate(var(--incl, 0deg)) scale(var(--ech, 1));
    animation-timing-function: linear;
  }
  45% {
    transform: rotate(var(--incl, 0deg)) scale(var(--ech, 1));
    /* Le seul rebond de toute la séquence : c'est le tic du vrai geste de
       cochage du guide. */
    animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  50% { transform: rotate(var(--incl, 0deg)) scale(calc(var(--ech, 1) * 1.09)); }
  55%, 100% {
    opacity: 1;
    transform: rotate(var(--incl, 0deg)) scale(var(--ech, 1));
  }
}


/**
 * LA RESPIRATION — ce qui sépare une scène d'une capture.
 *
 * Quelques pixels et un degré, en boucle, avec une période propre à chaque
 * tuile (3 à 5 s) et un départ décalé : sans ces deux variations, cinquante
 * tuiles respireraient À L'UNISSON, ce qui ne ressemble à rien de vivant — ça
 * ressemble à un écran qui vibre.
 *
 * Elle tourne en permanence, y compris pendant le vol : imperceptible à ce
 * moment-là (la tuile traverse l'écran), elle prend tout son sens une fois
 * posée. La démarrer plus tard aurait demandé une minuterie de plus.
 */
@keyframes intro-respire {
  from { transform: translate(0, 0) rotate(0deg); }
  to   { transform: translate(var(--fx, 3px), var(--fy, -4px)) rotate(var(--fr, 1.2deg)); }
}

/* Le filet passe au jaune AVEC le rebond, pas après : c'est le même geste. */
@keyframes intro-filet {
  0%, 45% { border-color: var(--c-bordure); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  50%, 100% { border-color: var(--c-coche); }
}

/* La coche RESTE, comme la tuile. C'est le sens même du jaune ici : « je
   l'ai ». La faire disparaître, maintenant que le champ demeure, laisserait
   des cases jaunes sans marque — un état qui n'existe nulle part dans le
   guide. Celle de la case du centre part avec sa tuile (`opacity` héritée). */
@keyframes intro-coche {
  0%, 45% { opacity: 0; transform: scale(0.4); animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); }
  52% { opacity: 1; transform: scale(1.15); }
  58%, 100% { opacity: 1; transform: scale(1); }
}

/* --- le titre, qui naît d'une case cochée et VA SE POSER SUR L'ACCUEIL ---- */

/**
 * ⚠️ CE N'EST PAS UN TITRE DÉCORATIF : c'est le titre de l'accueil, à sa place.
 *
 * « La transition avec la page d'accueil devrait être fluide, tu vois, filer un
 * continu de l'animation » (Adrien). D'où le raccord : `js/intro.js` MESURE où
 * se trouve le vrai titre de l'accueil (`.home-hero__logo`, déjà rendu derrière
 * le voile) et copie sa taille de police. Le titre de l'intro naît au centre,
 * grand, puis vient se poser EXACTEMENT dessus. Quand le voile s'efface, les
 * deux titres coïncident au pixel : l'œil ne voit pas une transition, il suit
 * un seul mouvement.
 *
 * Position, largeur et tailles de police sont donc posées en ligne par le JS.
 * Rien ici ne doit les figer.
 */
.intro__titre {
  position: fixed;
  /* L'opacité vit sur `.intro__titre-vol` (l'arrivée), pas ici : c'est elle qui
     le fait apparaître, en même temps que les items. */
  opacity: 1;
  /* ⚠️ AU-DESSUS DU CHAMP, ET DIT EXPLICITEMENT. Il l'était par simple ordre du
     document (inséré après la scène), ce qui a suffi jusqu'au jour où une tuile
     a reçu un `z-index` : elle est alors passée devant lui sans que rien ne le
     signale. L'ordre du document est une garantie trop fragile ici. */
  z-index: 2;
  text-align: center;
  pointer-events: none;
  line-height: 1.05;
  /* ⚠️ IL N'Y A PLUS DE `clip-path` ICI, et c'est un gain : c'était la propriété
     la plus chère de l'animation. Il avait aussi valu un défaut invisible — ses
     coordonnées étaient données en repère ÉCRAN alors que `clip-path` se
     calcule dans la boîte de l'élément, si bien que le cercle s'ouvrait à des
     centaines de pixels de là. */
  transform-origin: 50% 50%;
  /* La même ombre que sur l'accueil, et pour la même raison : c'est du texte
     posé sur une image. Elle ne compte PAS dans le calcul de contraste (aucun
     outil ne la crédite, et WCAG non plus) — c'est du confort, pas la mesure.
     ⚠️ LE JETON, PAS LA VALEUR. Elle était recopiée à la main — donc figée sur
     la version NUIT (une ombre sombre), alors que `--affiche-ombre` vaut un
     halo BLANC en mode jour. Résultat en mode jour : un titre sombre cerné
     d'une ombre sombre, et une ombre qui changeait au relais des deux titres.
     Le commentaire disait déjà « la même que sur l'accueil » ; il ne l'était
     pas. C'est le quatrième endroit de cette animation où une couleur écrite à
     la main faisait diverger l'intro du thème. */
  text-shadow: var(--affiche-ombre);
  isolation: isolate;
}

/* ⚠️ IL N'Y A PLUS D'ÉCRAN LOCAL DERRIÈRE LE TITRE, ET IL NE DOIT PAS REVENIR.
   « Il n'y a donc aucun dégradé d'opacité derrière le titre qui cache les
   items, car il ne veut rien derrière » (Adrien). Deux versions ont essayé de
   rendre le titre lisible en assombrissant ce qui passait dessous — un voile
   plein écran, puis un halo local qui le suivait. Les deux CACHAIENT le
   problème. La place du titre est maintenant RÉSERVÉE dès la construction du
   champ (voir `js/intro.js`), et les tuiles qui tomberaient dessus sont
   écartées : il n'y a rien derrière, donc rien à cacher.
   Le contrôle « aucune tuile ne passe jamais derrière le titre » de `npm test`
   vérifie la promesse à chaque image de la séquence. */

/**
 * LE TITRE — IL GONFLE, IL VIT, PUIS IL MONTE. Et ces trois choses ne sont pas
 * portées par le même élément, pour une raison mesurée.
 *
 * « La saccade apparaît dans la montée » (Adrien). Elle venait de mes
 * images-clés intermédiaires : chaque segment de courbe repart à vitesse nulle,
 * donc le titre s'arrêtait et redémarrait trois fois en montant. Et ces nœuds
 * n'étaient là que pour la dérive LATÉRALE — « le zigzag, ce n'est pas un
 * problème, le problème c'est la saccade ».
 *
 *   `.intro__titre`       → l'apparition, le GONFLAGE, et la MONTÉE d'un seul
 *                           trait : entre 60 % et 100 %, AUCUN nœud. C'est la
 *                           seule façon d'avoir une vitesse continue.
 *   `.intro__titre-corps` → le zigzag et l'inclinaison, avec leur propre
 *                           cadence. Perpendiculaires au mouvement principal,
 *                           ils ne peuvent pas le saccader.
 *
 * ⚠️ NE PAS REMETTRE DE NŒUD ENTRE 60 % ET 100 % de `intro-titre`. Même un
 * seul, même avec une belle courbe : il rend la vitesse discontinue, et ça se
 * voit — c'est précisément ce qu'Adrien a vu.
 *
 * LE GONFLAGE, LUI, N'EST PAS LINÉAIRE : « comme si on gonflait un ballon, tu
 * vois, ça enfle naturellement, ça se dégage de la place » (Adrien). Il résiste
 * au départ, part d'un coup, puis se tend en fin de course avec un dépassement
 * de 1,5 % — assez pour qu'on sente la matière, trop peu pour lire un rebond
 * (interdit ici : sur des capitales Oswald, un rebond ressemble à du clipart).
 */
/* Trois couches, comme une tuile — voir `js/intro.js`. L'ARRIVÉE suit le champ
   (même durée de vol, retard de la case qu'il occupe) ; la MONTÉE vient bien
   plus tard, quand tout est tombé autour de lui. */
.intro--joue .intro__titre {
  animation: intro-titre var(--d-titre, 1600ms) var(--t-monte, 2600ms) both;
}
.intro__titre-vol { display: block; opacity: 0; }
.intro--joue .intro__titre-vol {
  animation: intro-titre-vol 620ms var(--t-titre, 0ms) both;
}
.intro__titre-corps { display: block; }

/* ⚠️ EXACTEMENT LES COULEURS DU TITRE DE L'AFFICHE, ET C'EST LE PRINCIPE MÊME
   DE LA PASSATION. Les deux titres se superposent au pixel puis se relaient ;
   s'ils n'ont pas la même couleur, le nom CHANGE DE TEINTE au moment précis où
   le relais doit être invisible.
   C'était le cas dans les DEUX thèmes, et personne ne l'avait vu :
     · en mode jour, blanc → bleu nuit sur la première ligne ;
     · dans les deux, jaune → la couleur du titre sur la seconde.
   `--affiche-titre` est le jeton que l'accueil emploie (`.home-affiche__titre`).
   Le lire ici n'est pas une commodité : c'est la seule façon que les deux ne
   puissent plus diverger. */
.intro__titre-haut {
  display: block;
  font-family: var(--police-affichage);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--affiche-titre);
}
.intro__titre-bas {
  display: block;
  font-family: var(--police-manuscrite);
  color: var(--affiche-titre);
  margin-top: -2px;
}
.intro--joue .intro__titre-corps {
  animation: intro-titre-vie var(--d-titre, 1600ms) var(--t-monte, 2600ms) both;
}

/* ⚠️ IL APPARAÎT EN FONDU, IL N'ARRIVE PAS EN VOLANT. « La position du titre
   peut apparaître directement au centre en dégradé d'apparition, il n'y a pas
   de raison de le considérer comme un item » (Adrien, 2026-08-18).

   Il avait d'abord le même vol qu'une tuile — même durée, même retard que la
   case qu'il occupe. C'était cohérent sur le papier et inutile à l'écran : un
   nom qui traverse la page en tournant se lit comme un objet de plus, alors
   qu'il est ce vers quoi toute la scène converge. Un fondu le pose sans
   raconter d'histoire, et laisse le mouvement aux items — qui, eux, en ont un.

   Il reste une nuance d'échelle (2 %) : sans elle le fondu paraît collé, parce
   que rien d'autre à l'écran n'apparaît sans bouger. */
@keyframes intro-titre-vol {
  0% {
    opacity: 0;
    transform: scale(0.98);
    animation-timing-function: cubic-bezier(0.25, 0, 0.3, 1);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* LA MONTÉE, une fois le champ tombé autour de lui. */
@keyframes intro-titre {
  0% {
    transform: translateY(var(--decal-y, 0px)) scale(var(--ech-depart, 0.55));
    animation-timing-function: ease-in-out;
  }
  /* L'ÉLAN : il s'enfonce de 5 px avant de partir — le contre-mouvement qui
     précède tout geste réel. Sa vitesse est nulle ici, celle de la montée
     aussi : le raccord est continu, il n'y a pas d'à-coup. */
  9% {
    transform: translateY(calc(var(--decal-y, 0px) + 5px)) scale(var(--ech-depart, 0.55));
    animation-timing-function: cubic-bezier(0.36, 0, 0.2, 1);
  }
  /* ⚠️ UN SEUL SEGMENT, SANS AUCUN NŒUD. « La saccade apparaît dans la montée »
     (Adrien) : elle venait d'images-clés intermédiaires, dont chacune repart à
     vitesse nulle. Il grandit ici jusqu'à sa taille réelle — un objet qui se
     rapproche grandit, et c'est le seul moment où l'échelle a le droit de
     bouger pendant un déplacement. */
  100% {
    transform: translateY(0) scale(1);
  }
}



/* Le zigzag et l'inclinaison — tout ce qui rend le mouvement vivant sans
   toucher à la montée. Il se résout EXACTEMENT à zéro : le raccord au pixel
   avec le titre de l'accueil en dépend, et un contrôle le vérifie. */
@keyframes intro-titre-vie {
  0%, 20% { transform: translateX(0) rotate(-1.1deg); animation-timing-function: ease-in-out; }
  46% { transform: translateX(2.5px) rotate(0.5deg); animation-timing-function: ease-in-out; }
  62% { transform: translateX(-2px) rotate(-0.35deg); animation-timing-function: ease-in-out; }
  82% { transform: translateX(2.5px) rotate(0.3deg); animation-timing-function: ease-in-out; }
  100% { transform: translateX(0) rotate(0deg); }
}

/* `prefers-reduced-motion` est traité EN AMONT, dans `js/intro.js` : l'intro
   n'est simplement pas jouée. Le reste du guide coupe net partout où cette
   préférence est posée ; une version « dégradée mais quand même animée »
   serait une exception, pour un écran purement décoratif de surcroît.
   La règle ci-dessous n'est qu'un filet. */
@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
}


/**
 * LA PASSATION — l'accueil n'est QUE son fond pendant l'intro, puis il se
 * construit, et le titre de l'intro cède sa place à celui de l'affiche.
 *
 * « Sur la page d'accueil rien ne doit apparaître avant la fin de l'animation à
 * part le fond. Mais les titres, sous-titres etc. ça doit apparaître de manière
 * jolie après l'animation, et le titre animé doit disparaître pour laisser la
 * place à l'autre, le tout dans une fluidité parfaite et un timing parfait »
 * (Adrien, 2026-08-18).
 *
 * ⚠️ CE QUI A CHANGÉ, ET POURQUOI. L'accueil était ENTIÈREMENT dessiné derrière
 * le voile de l'intro ; seul le voile le cachait. Quand il s'effaçait, la page
 * n'apparaissait pas : elle était DÉJÀ là, d'un bloc, comme un rideau qu'on
 * tire. On la masque donc réellement, sauf son fond — l'affiche et la couleur
 * de page — pour que les items tombent sur quelque chose, puis on la fait
 * monter bloc par bloc UNE FOIS LE CHAMP SORTI DE L'ÉCRAN.
 *
 * ⚠️ `opacity`, JAMAIS `display` NI `visibility`. `js/intro.js` MESURE
 * `.home-hero__logo` pour poser son propre titre au pixel dessus : il lui faut
 * un élément qui occupe toujours sa place. `display:none` rendrait le raccord
 * — payé de trois tentatives — impossible à calculer.
 *
 * ⚠️ ET RIEN QUI TOUCHE À LA MISE EN PAGE : `transform` et `opacity` seulement,
 * règle du projet. Un `margin` animé ici ferait recalculer la page entière à
 * chaque image, sur l'écran le plus chargé du guide.
 */

/* --- Pendant l'intro : l'accueil se réduit à son fond. --------------------- */

/* ⚠️ L'ENTRÉE DE L'ACCUEIL (`gdi-monte`) EST ÉTEINTE PENDANT L'INTRO, et pas
   ici : c'est `js/intro.js` qui retire la classe `entre` de `#main` tant que
   l'intro joue. Ce n'est pas un détail de confort — c'était TOUT le défaut du
   raccord. Voir le commentaire de `sansEntree()` là-bas.
   Surtout, NE PAS essayer de le refaire en CSS sous une classe qu'on retire
   ensuite (`:not(.accueil-se-revele)`, par exemple) : rendre son nom à une
   animation la RELANCE, et l'affiche repartirait de 14 px plus bas au moment
   précis de la passation — mesuré. */
.intro-en-cours:not(.accueil-se-revele) .gdi-header,
.intro-en-cours:not(.accueil-se-revele) .gdi-main > *:not(.home-affiche),
.intro-en-cours:not(.accueil-se-revele) .home-topbar,
.intro-en-cours:not(.accueil-se-revele) .home-affiche__titre > * {
  opacity: 0;
}
/* L'affiche reste : c'est le fond, la seule chose qu'Adrien laisse paraître. */
.intro-en-cours:not(.accueil-se-revele) .home-affiche { opacity: 1; }

/* --- La passation, à l'instant où le champ quitte l'écran. ---------------- */

/* ⚠️ LE VRAI TITRE ARRIVE UN SOUFFLE AVANT QUE L'AUTRE NE PARTE (60 ms, posés
   par `js/intro.js`). Les deux se superposent au pixel : s'ils se croisaient
   exactement, la somme des deux opacités passerait sous 1 au milieu du fondu et
   on verrait le nom PÂLIR une fraction de seconde. Un léger recouvrement, lui,
   ne se voit pas — deux fois le même texte blanc reste blanc.
   Le départ du titre de l'intro n'est pas ici : c'est le fondu de l'overlay
   entier (`.intro--joue`, à `--t-sortie`) qui l'emporte. Une seconde animation
   d'opacité sur le même titre le ferait disparaître deux fois trop vite. */
.accueil-se-revele .home-hero__logo,
.accueil-se-revele .home-hero__script {
  animation: accueil-passation 220ms both linear;
}

/* Puis la page se construit autour du nom, bloc par bloc. Le titre, lui, ne
   bouge plus : c'est le point fixe qui fait lire une CONTINUITÉ plutôt qu'un
   changement d'écran. */
.accueil-se-revele .gdi-header,
.accueil-se-revele .gdi-main > *:not(.home-affiche),
.accueil-se-revele .home-topbar,
.accueil-se-revele .home-hero__tag {
  animation: accueil-revele 420ms 200ms both cubic-bezier(0.22, 0.7, 0.3, 1);
}
/* Le décalage : chaque bloc suit le précédent d'un souffle. Au-delà du
   cinquième on n'attend plus rien — la page doit être utilisable.
   ⚠️ RESSERRÉ le 2026-08-18 (nuit) : les retards allaient jusqu'à 370 ms pour
   un fondu de 520, soit près d'une seconde entre l'atterrissage du titre et une
   page réellement lisible. *« La suite doit tout de suite être mise en place »*
   (Adrien). La sous-phrase se pose maintenant AVEC le titre (0 ms), et le
   dernier bloc est en place 620 ms après lui. Le décalage sert à faire lire une
   construction, pas à faire patienter. */
.accueil-se-revele .home-hero__tag { animation-delay: 0ms; }
.accueil-se-revele .home-topbar { animation-delay: 90ms; }
.accueil-se-revele .gdi-main > *:nth-child(2) { animation-delay: 60ms; }
.accueil-se-revele .gdi-main > *:nth-child(3) { animation-delay: 110ms; }
.accueil-se-revele .gdi-main > *:nth-child(4) { animation-delay: 160ms; }
.accueil-se-revele .gdi-main > *:nth-child(n+5) { animation-delay: 200ms; }

@keyframes accueil-passation {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes accueil-revele {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Elle ne joue pas du tout pour qui a demandé moins d'animations — même règle
   que l'arrivée elle-même. L'intro n'étant alors pas jouée, `intro-en-cours`
   n'est jamais posée : la page est simplement là, entière. */
@media (prefers-reduced-motion: reduce) {
  .accueil-se-revele .gdi-header,
  .accueil-se-revele .gdi-main > *,
  .accueil-se-revele .home-topbar,
  .accueil-se-revele .home-hero__tag,
  .accueil-se-revele .home-hero__logo,
  .accueil-se-revele .home-hero__script { animation: none; }
}

/* ==========================================================================
   22. OÙ EN EST LE GUIDE — l'écran `#/etat`

   Pokemat porte le guide mais n'entre ni dans le dépôt, ni chez l'hébergeur,
   ni dans la console de la base : cet écran lui dit où en est le chantier
   depuis son téléphone. Les autres y lisent ce qui a changé.

   RÈGLE PARTICULIÈRE À CET ÉCRAN, ET LA PLUS IMPORTANTE :
   ------------------------------------------------------
   AUCUN JAUNE. Le `#FEF43D` ne veut dire qu'une chose dans tout le guide,
   « je possède cet item », et rien ici ne se possède. C'est pourquoi les
   barres d'avancement ci-dessous existent au lieu de réemployer `.progress`
   (qui est jaune à juste titre : elle mesure une collection). Même piste,
   même épaisseur, remplissage NEUTRE — on lit une progression, jamais une
   possession.

   Tout le reste est emprunté à l'existant, sans une couleur de plus :
   `.collec-tete` pour l'en-tête chiffré, `.settings-card` pour les blocs,
   `.settings-version` pour la ligne de version, `.btn--ghost`, `.footnote`.

   ⚠️ PRIORITÉ CSS — le piège payé sur `.compte-pied` : `.settings-card p`
   (0,1,1) l'emporte sur une simple classe (0,1,0). Tout `<p>` de cet écran
   vivant dans une carte, ses règles sont donc écrites `.settings-card .x`
   plutôt que `.x` — sinon la couleur et la marge du paragraphe générique
   gagnent, en silence.
   ========================================================================== */

/* L'en-tête chiffré est celui de « Ma collection », À UNE CHOSE PRÈS : là-bas,
   un anneau de progression occupe le haut du bloc et justifie son retrait. Ici
   il n'y a rien au-dessus du chiffre, et ce même retrait laissait 75 px de vide
   entre le bandeau et le premier mot de l'écran. */
.collec-tete--etat { padding-top: var(--esp-sm); }

/* --- la barre d'avancement neutre d'un chantier --------------------------- */
.jauge { margin-bottom: var(--esp-lg); }
.jauge:last-child { margin-bottom: 0; }

.settings-card .jauge__tete {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--esp-sm);
  margin: 0 0 6px;
  color: var(--c-texte);
}
/* LIBELLÉ, PAS TITRE — et c'est la charte qui le dit : la police d'affichage
   (Oswald) NOMME les choses (une liste, une série, un item), le texte DIT le
   reste. « Items illustrés » n'est pas une chose du guide, c'est ce que mesure
   la barre d'en dessous.
   Écrit d'abord en Oswald 18 px, ce libellé pesait plus lourd que le « LE
   CHANTIER » qui le contient : la lecture partait de la mauvaise ligne. */
.jauge__quoi {
  font-family: var(--police-texte);
  font-weight: var(--tres-gras);
  font-size: var(--taille-petit);
  text-transform: uppercase;
  letter-spacing: var(--lettres-capitales);
}
.jauge__part {
  flex: none;
  font-size: var(--taille-petit);
  font-weight: var(--tres-gras);
  color: var(--c-texte-doux);
  /* Les pourcentages s'alignent d'une jauge à l'autre. */
  font-variant-numeric: tabular-nums;
}

.jauge__piste {
  height: 6px;
  border-radius: var(--r-plein);
  background: var(--c-bloc-piste);
  overflow: hidden;
}
.jauge__remplie {
  height: 100%;
  border-radius: inherit;
  background: var(--c-texte);
  /* Mise à l'échelle depuis la GAUCHE : sans cette origine, la barre grandit
     par son milieu et se lit comme un curseur, pas comme une progression. */
  transform-origin: left center;
  transition: transform var(--duree-moyenne) var(--courbe);
}
@media (prefers-reduced-motion: reduce) {
  .jauge__remplie { transition: none; }
}

.settings-card .jauge__detail {
  margin: 6px 0 0;
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-texte-doux);
}

/* --- ce qu'on attend de Pokemat ------------------------------------------ */
.etat-questions {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Les blocs répétés d'un même bloc se séparent par un FILET, pas par une carte
   de plus : trois conteneurs arrondis empilés ne disent plus lequel contient
   l'autre (piège payé sur l'accueil, §18). */
.etat-question,
.etat-jour,
.etat-suite {
  margin-top: var(--esp-md);
  padding-top: var(--esp-md);
  border-top: 1px solid var(--c-bordure);
}
.etat-question:first-child,
.etat-jour:first-of-type,
.etat-suite:first-of-type {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Dans une même journée, les nouvelles se suivent SANS filet : le filet marque
   le passage d'un jour à l'autre, et il ne peut pas dire deux choses. Un titre
   en capitales suffit à séparer deux nouvelles du même jour. */
.etat-nouvelle { margin-top: var(--esp-md); }
.etat-nouvelle:first-of-type { margin-top: 6px; }

.settings-card .etat-question__titre,
.settings-card .etat-nouvelle__titre,
.settings-card .etat-suite__titre {
  margin: 0 0 4px;
  color: var(--c-texte);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  line-height: 1.25;
  text-transform: uppercase;
  letter-spacing: var(--lettres-affichage);
}
.settings-card .etat-question__titre {
  display: flex;
  /* `flex-start` et non `baseline` : le titre peut tenir sur deux lignes, et la
     pastille du numéro doit rester en haut, en face de la première. */
  align-items: flex-start;
  gap: var(--esp-sm);
}

/* Le numéro de la question — une RÉFÉRENCE, pas un ornement : c'est lui qu'on
   cite dans un SMS (« pour la 10, c'est… ») sans avoir à recopier la question.
   Neutre, comme tout état d'option du guide. */
.etat-question__n {
  flex: none;
  min-width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 6px;
  border: 1px solid var(--c-trait);
  border-radius: var(--r-plein);
  font-family: var(--police-texte);
  font-size: var(--taille-minuscule);
  font-weight: var(--tres-gras);
  letter-spacing: 0;
  color: var(--c-texte-doux);
}

.settings-card .etat-question__texte,
.settings-card .etat-nouvelle__texte,
.settings-card .etat-suite__texte {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--c-texte-doux);
}

.settings-card .etat-question__date {
  margin: 6px 0 0;
  font-size: var(--taille-minuscule);
  color: var(--c-texte-doux);
}

/* --- ce qui a changé ------------------------------------------------------ */
/* La date passe DEVANT les nouvelles du jour, en surtitre : sur un journal,
   c'est elle qu'on parcourt du regard. Police de texte et non d'affichage — un
   titre de niveau 3 qui ne nomme pas une chose, mais un jour. */
.settings-card .etat-jour__date {
  margin: 0 0 2px;
  font-family: var(--police-texte);
  font-size: var(--taille-minuscule);
  font-weight: var(--tres-gras);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte-doux);
}

/* ==========================================================================
   22. CULTURE — la rubrique de Pokemat, et un article

   AUCUNE COULEUR NOUVELLE, AUCUN JETON NOUVEAU. Tout ce qui suit se peint avec
   les jetons des thèmes : la rubrique est donc entière la nuit comme le jour,
   sans qu'on ait à y repenser — c'est la règle « deux thèmes, chacun entier »
   appliquée à une surface qui n'existait pas quand elle a été écrite.

   ET PAS DE JAUNE. Le jaune ne veut dire qu'une chose dans ce guide : « je
   possède cet item ». Un article ne se coche pas ; il n'a donc aucune raison
   d'en porter, pas même pour un lien ou une pastille « vidéo ».
   ========================================================================== */

/* --- La carte d'un article ------------------------------------------------
   Elle sert à trois endroits (accueil, rubrique, « à lire ensuite ») et n'est
   écrite qu'une fois. C'est un <a> : le survol et le focus sont donc ceux d'un
   lien, sans qu'on ait à les redéclarer. */
.carte-art {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: var(--esp-md);
  align-items: center;
  padding: var(--esp-sm);
  border-radius: var(--r-md);
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  color: var(--c-bloc-texte);
  text-decoration: none;
  /* Le geste central du guide est l'appui répété ; ici c'est l'appui unique,
     mais la règle ne change pas — deux appuis rapprochés ne doivent jamais
     déclencher le zoom de Safari. */
  touch-action: manipulation;
  transition: background var(--duree-courte) var(--courbe),
              transform var(--duree-courte) var(--courbe);
}

.carte-art + .carte-art { margin-top: var(--esp-sm); }
.carte-art:hover { background: var(--c-bloc-survol); }
.carte-art:active { transform: scale(0.995); }

/* La carte de tête : l'image passe AU-DESSUS du texte, pleine largeur. C'est le
   seul article mis en avant — au-delà, une colonne d'images pleine largeur ne
   hiérarchise plus rien et repousse tout le reste hors de l'écran. */
.carte-art--grande {
  grid-template-columns: 1fr;
  gap: var(--esp-sm);
  padding: var(--esp-sm) var(--esp-sm) var(--esp-md);
}

.carte-art__visuel {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r-sm);
  /* Le rapport est posé sur le CONTENANT : la carte garde donc exactement la
     même hauteur avant et après l'arrivée de l'image, et la page ne saute pas
     au chargement. */
  aspect-ratio: 1 / 1;
  background: var(--c-surface-haute);
}

.carte-art--grande .carte-art__visuel { aspect-ratio: 16 / 9; }

.art-visuel__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* L'article sans image : une plaque colorée qui porte ses initiales. Sa teinte
   vient de son identifiant (voir `visuel()` dans js/views/culture.js), elle est
   donc TOUJOURS la même pour le même article — deux ouvertures de suite ne
   donnent pas deux couleurs, ce qui se lirait comme un défaut. */
.art-visuel__plaque {
  display: block;
  width: 100%;
  height: 100%;
  /* L'AFFICHE DU GUIDE SERT DE COUVERTURE PAR DÉFAUT — voir `visuel()` dans
     js/ui/article-html.js. Elle est déjà pré-chargée avec le site : une carte
     d'article sans photo ne coûte donc aucun octet et s'affiche hors connexion
     comme le reste. Le cadrage (`--cadrage`) vient de l'identifiant de
     l'article : constant pour lui, différent de son voisin.
     Le voile n'est pas décoratif : le bouton de lecture et la légende blanche
     se posent dessus, et la mosaïque contient des tuiles très claires. */
  background-image:
    linear-gradient(rgba(10, 11, 20, 0.45), rgba(10, 11, 20, 0.45)),
    url('../assets/affiche-accueil.webp');
  /* ⚠️ 260 % ET NON `cover`. À `cover`, la mosaïque entière tient dans une
     vignette de 96 px : on y lit une TEXTURE, pas des objets — exactement le
     reproche fait aux initiales qu'elle remplace. Agrandie, on reconnaît trois
     ou quatre Staks, et la carte dit enfin de quoi le guide parle. */
  background-size: 260%;
  background-position: var(--cadrage, 50% 50%);
}

/* En thème jour, le voile s'allège : le fond de page est clair, une image
   assombrie au même degré y ferait un trou. */
:root[data-theme='jour'] .art-visuel__plaque {
  background-image:
    linear-gradient(rgba(10, 11, 20, 0.28), rgba(10, 11, 20, 0.28)),
    url('../assets/affiche-accueil.webp');
}

/* La pastille « c'est une vidéo », posée sur l'image. */
.carte-art__lecture {
  position: absolute;
  right: 6px;
  bottom: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-plein);
  background: rgba(10, 11, 20, 0.62);
  color: #FFFFFF;
}

.carte-art__lecture svg { width: 18px; height: 18px; }

.carte-art__texte {
  display: block;
  min-width: 0;   /* sans ça, un titre long élargit la grille au lieu de couper */
}

.carte-art__titre {
  display: block;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-liste);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  line-height: 1.15;
  color: var(--c-bloc-texte);
}

.carte-art--grande .carte-art__titre { font-size: var(--taille-titre); }

.carte-art__resume {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 3px;
  font-size: var(--taille-petit);
  line-height: 1.4;
  color: var(--c-bloc-doux);
}

.carte-art--grande .carte-art__resume { -webkit-line-clamp: 3; }

/* --- La ligne « 18 août 2026 · 6 min · vidéo » --------------------------- */
.art-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-size: var(--taille-minuscule);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-bloc-doux);
}

/* Le point de séparation appartient au morceau qui SUIT (voir l'en-tête de
   `meta()` dans js/ui/article-html.js) : il ne peut donc jamais rester seul en
   bout de ligne quand la ligne passe sur deux rangs. */
.art-meta > span + span::before {
  content: '·';
  margin-right: 6px;
  opacity: 0.55;
}

.art-meta__video {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 7px 1px 4px;
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bloc-trait);
  color: var(--c-bloc-doux);
}

.art-meta__video svg { width: 12px; height: 12px; }

/* --- Le bloc de l'accueil ------------------------------------------------ */

/* « NOUVEAU » — le marqueur qui tient lieu de bouton dans le bandeau.
   Neutre, jamais jaune (le jaune ne dit que « je l'ai »), et il disparaît dès
   que l'article est ouvert : voir `nbNonLus()` dans js/core/blog.js. */
.home-culture__neuf {
  flex: none;
  padding: 2px 9px;
  border-radius: var(--r-plein);
  background: var(--c-surface-haute);
  border: 1px solid var(--c-bordure);
  font-family: var(--police-texte);
  font-size: var(--taille-minuscule);
  font-weight: var(--tres-gras);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte);
}

.home-culture__intro {
  margin: -2px 0 var(--esp-sm);
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-texte-doux);
}

.home-culture .btn--large { margin-top: var(--esp-sm); }

/* --- La rubrique --------------------------------------------------------- */
.culture-intro {
  margin-bottom: var(--esp-md);
}

.culture-intro__texte {
  font-size: var(--taille-texte);
  line-height: 1.5;
  color: var(--c-texte-doux);
}

.culture-intro__texte strong { color: var(--c-texte); }

/* --- UN ARTICLE ----------------------------------------------------------
   LA LARGEUR DE LECTURE EST PLUS ÉTROITE QUE CELLE DU GUIDE (1080 px).
   Une liste d'items se parcourt en diagonale et gagne à s'étaler en colonnes ;
   un texte suivi, non : au-delà d'une soixantaine de caractères par ligne,
   l'œil rate le début de la ligne suivante. C'est le seul écran du guide qui a
   sa propre largeur, et c'est pour cette raison-là. */
.article {
  max-width: 40rem;
  margin: 0 auto;
}

.art-entete { margin-bottom: var(--esp-lg); }

.art-surtitre {
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-minuscule);
  letter-spacing: var(--lettres-capitales);
  color: var(--c-texte-doux);
}

.art-titre {
  margin-top: 2px;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-categorie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  line-height: 1.08;
  color: var(--c-texte);
}

.art-entete .art-meta { color: var(--c-texte-doux); }
.art-entete .art-meta__video { border-color: var(--c-trait); }

/* La vidéo : son image, et un appui qui ouvre YouTube. Ce n'est PAS un lecteur
   incrusté — voir l'en-tête de js/views/culture.js, les quatre raisons y sont. */
.art-video {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r-md);
  border: 1px solid var(--c-bloc-bordure);
  background: var(--c-surface-haute);
  text-decoration: none;
  touch-action: manipulation;
}

.art-video__image {
  display: block;
  aspect-ratio: 16 / 9;
}

.art-video__lecture {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Cible tactile de plein droit : 64 px, bien au-delà des 44 px minimum. */
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  border-radius: var(--r-plein);
  background: rgba(10, 11, 20, 0.66);
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  color: #FFFFFF;
  transition: transform var(--duree-courte) var(--courbe);
}

.art-video__lecture svg { width: 34px; height: 34px; }
.art-video:hover .art-video__lecture { transform: scale(1.06); }

.art-video__legende {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 26px var(--esp-md) var(--esp-sm);
  font-size: var(--taille-petit);
  font-weight: var(--gras);
  color: #FFFFFF;
  /* Le texte est blanc sur une image inconnue : le voile n'est pas une
     décoration, c'est ce qui garantit qu'on le lit sur une vignette claire. */
  background: linear-gradient(to top, rgba(10, 11, 20, 0.86), rgba(10, 11, 20, 0));
}

/* L'attente d'un article publié depuis un téléphone : son texte vit dans la
   base, il arrive une fraction de seconde après la fiche. */
.art-chargement {
  padding: var(--esp-lg) 0;
  font-size: var(--taille-texte);
  color: var(--c-texte-doux);
}

.art-chapo {
  margin-top: var(--esp-lg);
  font-size: calc(var(--taille-texte) + 2px);
  font-weight: var(--gras);
  line-height: 1.5;
  color: var(--c-texte);
}

/* --- Le corps du texte ---------------------------------------------------
   Tout ce qui suit habille du HTML ENGENDRÉ par outils/construire-blog.js. La
   liste des balises possibles y est fermée : titres, paragraphes, listes,
   citations, encadrés, images, liens. Rien d'autre ne peut arriver ici. */
.art-corps {
  margin-top: var(--esp-lg);
  font-size: calc(var(--taille-texte) + 1px);
  line-height: 1.65;
  color: var(--c-texte);
}

.art-corps p { margin-bottom: var(--esp-md); }

.art-corps h2 {
  margin: var(--esp-xl) 0 var(--esp-sm);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-titre);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  line-height: 1.15;
  color: var(--c-texte);
}

.art-corps h3 {
  margin: var(--esp-lg) 0 var(--esp-xs);
  font-family: var(--police-texte);
  font-weight: var(--tres-gras);
  font-size: calc(var(--taille-texte) + 1px);
  color: var(--c-texte);
}

.art-corps a {
  color: var(--c-texte);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1.5px;
  text-decoration-color: var(--c-texte-doux);
}

.art-corps strong { font-weight: var(--tres-gras); }

.art-liste {
  margin: 0 0 var(--esp-md);
  padding-left: var(--esp-lg);
}

.art-liste li { margin-bottom: var(--esp-xs); }
.art-liste li::marker { color: var(--c-texte-doux); }

/* Une citation : un filet à gauche, pas un bloc coloré. Un aplat de plus
   couperait la colonne de lecture en deux. */
.art-citation {
  margin: var(--esp-lg) 0;
  padding-left: var(--esp-md);
  border-left: 3px solid var(--c-trait);
  font-family: var(--police-manuscrite);
  font-size: calc(var(--taille-texte) + 5px);
  line-height: 1.45;
  color: var(--c-texte);
}

.art-citation p { margin-bottom: 0; }

/* L'encadré « Le savais-tu ? » : une surface surélevée, comme partout ailleurs
   dans le guide — la hiérarchie passe par l'élévation et le filet, jamais par
   un renversement de couleur. */
.art-encadre {
  margin: var(--esp-lg) 0;
  padding: var(--esp-md);
  border-radius: var(--r-md);
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  color: var(--c-bloc-texte);
}

.art-encadre__titre {
  margin-bottom: var(--esp-xs);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
}

.art-encadre p:last-child { margin-bottom: 0; }
.art-encadre .art-liste:last-child { margin-bottom: 0; }

.art-figure {
  margin: var(--esp-lg) 0;
}

.art-figure img,
.art-figure .art-visuel__img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-md);
  background: var(--c-surface-haute);
}

.art-figure--couverture {
  margin-top: 0;
  aspect-ratio: 16 / 9;
}

.art-figure--couverture .art-visuel__img,
.art-figure--couverture .art-visuel__plaque {
  height: 100%;
  object-fit: cover;
  border-radius: var(--r-md);
}

.art-figure figcaption {
  margin-top: 6px;
  font-size: var(--taille-petit);
  line-height: 1.4;
  color: var(--c-texte-doux);
}

/* --- Le pied de l'article ------------------------------------------------ */
.art-pied {
  margin-top: var(--esp-xl);
  padding-top: var(--esp-lg);
  border-top: 1px solid var(--c-trait-fin);
  text-align: center;
}

.art-pied__video {
  display: inline-flex;
  align-items: center;
  gap: var(--esp-sm);
}

.art-pied__video svg { width: 20px; height: 20px; }

.art-signature {
  margin-top: var(--esp-md);
  font-family: var(--police-manuscrite);
  font-size: calc(var(--taille-texte) + 3px);
  color: var(--c-texte-doux);
}

.art-signature strong { font-weight: 400; color: var(--c-texte); }

.art-ensuite { margin-top: var(--esp-xl); }

/* --- Grand écran ---------------------------------------------------------
   Les cartes passent en deux colonnes ; la carte de tête garde la largeur
   entière, elle est là pour être vue en premier. */
@media (min-width: 769px) {
  .culture-liste {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--esp-sm);
    align-items: start;
  }

  .culture-liste .carte-art--grande { grid-column: 1 / -1; }
  .culture-liste .carte-art + .carte-art { margin-top: 0; }
}

/* --- Moins d'animations ---------------------------------------------------
   Même règle que partout : ce qui bouge s'arrête, ce qui informe reste. */
@media (prefers-reduced-motion: reduce) {
  .carte-art,
  .art-video__lecture { transition: none; }
  .carte-art:active { transform: none; }
  .art-video:hover .art-video__lecture { transform: none; }
}

/* ==========================================================================
   23. L'ATELIER DE RÉDACTION (redaction.html)

   Une page à part, qui n'appartient pas au guide : Mathieu y écrit un article
   et l'envoie. Elle emploie les mêmes jetons et les mêmes briques — un atelier
   qui ne ressemblerait pas au guide donnerait l'impression d'écrire ailleurs
   que là où ça sera lu.

   ⚠️ ELLE NE PART PAS DANS LA LISTE `CORE` DU SERVICE WORKER, et c'est
   volontaire : les milliers de gens qui lisent le guide n'écriront jamais
   d'article, et n'ont pas à télécharger l'atelier avec. Il se met en cache
   tout seul à la première visite (stratégie « cache d'abord » du reste du
   site) — Mathieu l'a donc hors connexion dès qu'il l'a ouvert une fois.
   ========================================================================== */

.atelier-entete .gdi-title-inline {
  /* Sur cette page il n'y a pas de grand titre qui défile : le nom de l'atelier
     est là dès le départ, sans le fondu du guide. */
  opacity: 1;
}

.atelier-entete__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  /* La marge de sécurité du bandeau : au moins 6 px entre un élément
     interactif et le bord. Règle du projet, contrôlée par les tests. */
  padding-right: 8px;
}

.atelier { padding-bottom: var(--esp-xl); }

/* --- Écrire / Aperçu ----------------------------------------------------- */
.atelier-bascule {
  display: flex;
  gap: 4px;
  padding: 4px;
  margin-bottom: var(--esp-md);
  border-radius: var(--r-plein);
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
}

.atelier-bascule__btn {
  flex: 1;
  min-height: var(--cible-tactile);
  border: 0;
  border-radius: var(--r-plein);
  background: transparent;
  color: var(--c-texte-doux);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  cursor: pointer;
  touch-action: manipulation;
}

/* Le choix courant se marque en NEUTRE, jamais en jaune : le jaune ne veut
   dire qu'une chose dans ce projet, « je possède cet item ». */
.atelier-bascule__btn.est-actif {
  background: var(--c-surface-haute);
  color: var(--c-texte);
  box-shadow: var(--ombre);
}

/* --- L'aide du début ------------------------------------------------------
   Elle s'efface une fois comprise (voir `poserAide()` dans js/redaction.js) :
   sur cette page, la place appartient au texte qu'on écrit. */
.atelier-aide {
  margin-bottom: var(--esp-md);
  padding: var(--esp-md);
  border-radius: var(--r-md);
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  color: var(--c-bloc-texte);
}

.atelier-aide__titre {
  margin-bottom: var(--esp-sm);
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
}

.atelier-aide__etapes {
  margin: 0 0 var(--esp-sm);
  padding-left: var(--esp-lg);
  font-size: var(--taille-petit);
  line-height: 1.5;
}

.atelier-aide__etapes li { margin-bottom: var(--esp-xs); }
.atelier-aide__etapes li::marker { color: var(--c-bloc-doux); }

.atelier-aide__note {
  margin-bottom: var(--esp-sm);
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-bloc-doux);
}

/* --- Les deux volets ------------------------------------------------------ */
.atelier-volet { display: none; }
.atelier-volet.est-visible { display: block; }

/* --- Les champs ---------------------------------------------------------- */
.atelier-champ {
  display: block;
  margin-bottom: var(--esp-md);
}

.atelier-champ__nom {
  display: block;
  margin-bottom: 6px;
  font-family: var(--police-affichage);
  font-weight: var(--graisse-affichage);
  font-size: var(--taille-serie);
  letter-spacing: var(--lettres-affichage);
  text-transform: uppercase;
  color: var(--c-texte);
}

.atelier-champ__nom em {
  font-family: var(--police-texte);
  font-weight: 400;
  font-size: var(--taille-petit);
  font-style: normal;
  text-transform: none;
  letter-spacing: 0;
  color: var(--c-texte-doux);
}

.atelier-champ__aide {
  display: block;
  margin-top: 4px;
  font-size: var(--taille-petit);
  color: var(--c-texte-doux);
}

.atelier-champ__aide.est-erreur { color: var(--c-danger); }

.atelier-saisie {
  display: block;
  width: 100%;
  min-height: var(--cible-tactile);
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 1.5px solid var(--c-champ-trait);
  background: var(--c-surface);
  color: var(--c-texte);
  font-family: var(--police-texte);
  /* 16 px MINIMUM : en dessous, Safari zoome sur le champ dès qu'on y touche,
     et l'on se retrouve à écrire un article dans une page agrandie. */
  font-size: 16px;
  line-height: 1.5;
  touch-action: manipulation;
}

.atelier-saisie:focus {
  outline: 2px solid var(--c-focus);
  outline-offset: 1px;
  border-color: var(--c-focus);
}

.atelier-saisie--resume { resize: vertical; }

.atelier-texte {
  min-height: 46vh;
  resize: vertical;
  line-height: 1.6;
}

.atelier-champ--texte { margin-bottom: var(--esp-sm); }

/* --- La barre d'outils ---------------------------------------------------
   ELLE PASSE À LA LIGNE, elle ne défile pas. Première version : une seule
   rangée qui défilait horizontalement, pour ne pas manger de hauteur. Mesuré
   sur un iPhone 12 : les deux derniers boutons — « Encadré » et surtout
   « Photo » — étaient hors de l'écran, sans rien pour le laisser deviner. Une
   commande qu'on ne voit pas n'existe pas, et « Photo » est justement celle
   dont Mathieu aura le plus besoin. Deux rangées coûtent 50 px ; la zone de
   texte fait 46 % de la hauteur de l'écran, elle les rend sans broncher. */
.atelier-outils {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: var(--esp-sm);
}

.atelier-outil {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  min-height: var(--cible-tactile);
  padding: 0 12px;
  border-radius: var(--r-plein);
  border: 1px solid var(--c-bouton-trait);
  background: var(--c-surface);
  color: var(--c-texte);
  font-size: var(--taille-petit);
  cursor: pointer;
  touch-action: manipulation;
}

.atelier-outil:hover { background: var(--c-survol); }
.atelier-outil:active { background: var(--c-appui); }

.atelier-outil__signe {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: var(--r-sm);
  background: var(--c-surface-haute);
  font-family: var(--police-affichage);
  font-size: 13px;
  color: var(--c-texte-doux);
}

.atelier-outil__signe--gras { font-weight: 700; color: var(--c-texte); }

/* --- Les photos déposées -------------------------------------------------- */
.atelier-photos { margin-bottom: var(--esp-md); }

.atelier-photos__titre {
  margin-bottom: 6px;
  font-size: var(--taille-minuscule);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte-doux);
}

.atelier-photos__liste {
  display: flex;
  gap: var(--esp-sm);
  overflow-x: auto;
  padding-bottom: 4px;
}

.atelier-photo {
  position: relative;
  flex: 0 0 auto;
  width: 92px;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
}

.atelier-photo img {
  width: 100%;
  height: 64px;
  object-fit: cover;
}

.atelier-photo__nom {
  display: block;
  padding: 3px 5px;
  font-size: var(--taille-minuscule);
  color: var(--c-texte-doux);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.atelier-photo__retirer {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: var(--r-plein);
  background: rgba(10, 11, 20, 0.66);
  color: #FFFFFF;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
}

/* --- Mettre en ligne soi-même --------------------------------------------- */
.atelier-publier {
  margin-bottom: var(--esp-md);
  padding: var(--esp-md);
  border-radius: var(--r-md);
  background: var(--c-bloc-fond);
  border: 1px solid var(--c-bloc-bordure);
  color: var(--c-bloc-texte);
}

.atelier-publier:empty { display: none; }

.atelier-publier__mot {
  margin-top: var(--esp-sm);
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-bloc-doux);
}

/* Le lien « Se connecter » est un <a> : sans ces trois lignes, il ne prendrait
   pas la forme d'un bouton (les règles de `.btn` visent le texte, pas la boîte). */
.atelier-publier a.btn {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* L'identifiant du compte : à lire caractère par caractère, donc en chasse
   fixe et coupable n'importe où — c'est une suite de 28 signes sans espace. */
.atelier-publier__uid {
  margin: var(--esp-sm) 0;
  padding: var(--esp-sm);
  border-radius: var(--r-sm);
  background: var(--c-surface-haute);
  font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;
  font-size: var(--taille-petit);
  overflow-wrap: anywhere;
  color: var(--c-texte);
}

.atelier-miens { margin-bottom: var(--esp-md); }

.atelier-miens__titre {
  margin-bottom: 6px;
  font-size: var(--taille-minuscule);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte-doux);
}

.atelier-mien {
  display: flex;
  align-items: center;
  gap: var(--esp-sm);
  padding: var(--esp-sm);
  border-radius: var(--r-md);
  background: var(--c-surface);
  border: 1px solid var(--c-bordure);
}

.atelier-mien + .atelier-mien { margin-top: 6px; }

.atelier-mien__nom {
  flex: 1;
  min-width: 0;
  font-size: var(--taille-petit);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Bas de page ---------------------------------------------------------- */
.atelier-etat {
  margin-bottom: var(--esp-md);
  font-size: var(--taille-petit);
  line-height: 1.45;
  color: var(--c-texte-doux);
}

/* Article trop lourd pour partir en ligne : la même couleur que les autres
   avertissements de l'atelier, jamais le jaune (réservé au cochage). */
.atelier-etat.est-erreur { color: var(--c-danger); }

.atelier-fin {
  display: flex;
  flex-direction: column;
  gap: var(--esp-sm);
  margin-bottom: var(--esp-md);
}

.atelier-effacer { color: var(--c-danger); }
.atelier-effacer.est-armee { border-color: var(--c-danger-trait); }

/* --- L'aperçu -------------------------------------------------------------
   Il montre l'article dans sa vraie mise en page, sur la vraie matière : c'est
   la même feuille de style, les mêmes classes `art-*`. La seule différence est
   ce cadre, qui dit « ceci est un aperçu » — sans lui, on ne saurait plus si
   l'on regarde le site ou l'atelier. */
.atelier-apercu__note {
  margin-bottom: var(--esp-sm);
  font-size: var(--taille-minuscule);
  letter-spacing: var(--lettres-capitales);
  text-transform: uppercase;
  color: var(--c-texte-doux);
}

.atelier-apercu {
  padding: var(--esp-md);
  border-radius: var(--r-lg);
  border: 1px dashed var(--c-trait);
  background: var(--c-fond-haut);
}

.atelier-apercu__vide {
  padding: var(--esp-xl) 0;
  text-align: center;
  color: var(--c-texte-doux);
}

/* Dans l'aperçu, l'article ne se centre pas sur 40 rem : il occupe la colonne
   qu'on lui donne, sinon il flotterait au milieu d'un cadre trop large. */
.atelier-apercu .article { max-width: none; }

/* --- Grand écran : écrire et voir en même temps --------------------------- */
@media (min-width: 1024px) {
  .atelier-bascule { display: none; }

  .atelier-grille {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--esp-lg);
    align-items: start;
  }

  /* Les deux volets sont visibles quoi qu'ait choisi la bascule : sur cet
     écran, le choix n'a plus de sens. */
  .atelier-volet,
  .atelier-volet.est-visible { display: block; }

  .atelier-volet--apercu {
    position: sticky;
    top: calc(var(--entete-plein) + var(--esp-md));
    max-height: calc(100svh - var(--entete-plein) - var(--esp-xl));
    overflow-y: auto;
  }
}
