/* --- Typographie d'affichage : Barlow Condensed, auto-hébergée -----------
   Auto-hébergée pour la même raison que les icônes Tabler ci-dessous :
   aucune requête externe, fonctionne hors ligne (PWA). Deux graisses
   italiques seulement (600 et 700), sous-ensemble latin (couvre les
   accents français, dans Latin-1 Supplement). Fichiers dans wwwroot/fonts/,
   extraits une fois du paquet npm @fontsource/barlow-condensed — aucune
   dépendance npm ne reste dans le projet.
   `--police-affichage` centralise la pile (avec repli explicite vers
   system-ui) pour n'être déclarée qu'une fois ; posée uniquement sur les
   éléments de mise en avant (titres, chiffres clés, pseudo de l'hôte), pas
   sur le corps de texte ni la page d'administration. Jamais en dessous de
   1rem : un italique condensé perd trop à petite taille. */

@font-face {
  font-family: "Barlow Condensed";
  font-style: italic;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/barlow-condensed-latin-600-italic.woff2") format("woff2");
}

@font-face {
  font-family: "Barlow Condensed";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/barlow-condensed-latin-700-italic.woff2") format("woff2");
}

:root {
  --police-affichage: "Barlow Condensed", system-ui, sans-serif;

  /* Échelle typographique. **Choisir un rôle, jamais un nombre** : écrire une
     valeur en dur rouvre la porte à la dérive que cette échelle referme — la
     feuille comptait 20 tailles distinctes, dont six entre 0.7 et 1rem, soit des
     écarts de moins d'un pixel que personne ne voit mais que chacun devait
     trancher à chaque nouvelle règle.
     Trois paliers de texte, et les écarts sont désormais perceptibles (1.6 px). */
  /* **Quatrième palier ajouté le 14/08/2026, et il porte un rôle réel** : la mention
     légale d'un fond de carte, qui doit rester lisible mais ne jamais concurrencer ce
     qu'elle surplombe. En `--texte-etiquette` elle pesait autant que les chiffres de la
     sortie. L'écart de 1,6 px avec le palier suivant reste perceptible, ce qui est le
     critère retenu lors de la réduction de l'échelle — ce n'est donc pas une réouverture
     de la dérive des vingt tailles. À réserver aux mentions légales. */
  --texte-mention: 0.7rem;
  --texte-etiquette: 0.8rem;  /* libellés de zone, notes, mentions discrètes */
  --texte-courant: 0.9rem;    /* texte courant, boutons, listes */
  --texte-fort: 1rem;         /* emphase : auteur d'un commentaire, « chez X » */

  /* **LA RÈGLE DES TROIS RÔLES**, posée le 14/08/2026 après inventaire de toute la
     feuille — et c'est elle qu'il faut respecter avant de choisir une taille :

       intitulé      `--texte-etiquette` + `--couleur-texte-discret`, TOUJOURS ensemble
       donnée        `--texte-courant`   + `--couleur-texte` (héritée du body)
       mise en avant `--texte-fort`      + police d'affichage ou `--couleur-accent-texte`

     **Pourquoi c'était nécessaire** : les six intitulés du projet portaient à eux seuls
     les **quatre** combinaisons possibles de ces deux dimensions — 0,9 plein, 0,8 plein,
     0,9 discret, 0,8 discret. Aucune règle, donc chaque nouvel intitulé était tranché à
     l'unité, et un intitulé pouvait ressortir davantage que la donnée qu'il annonce
     (`#liste-reponses p` affichait les pseudos en gris sous un titre de groupe en gras
     plein). D'où l'impression de tailles variables sans logique.

     **Un intitulé ne prend jamais la couleur pleine, une donnée jamais la discrète.**
     C'est ce qui rend la nuance constante d'un écran à l'autre.

     Ne relèvent d'aucun des trois rôles, et gardent donc `--texte-courant` : les
     **contrôles** (boutons, libellés de radio ou de case à cocher), qui doivent rester
     lisibles au doigt, et les **états** en italique (« annulée », « n'a pas répondu »). */

  /* Même valeur que `--texte-fort`, nom distinct **et c'est délibéré** : un champ
     de saisie sous 16 px fait zoomer iOS Safari à la mise au point (voir
     Typographie dans docs/interface.md). Séparer les deux rôles empêche de faire
     descendre les champs en croyant ne toucher qu'à de l'emphase. */
  --texte-champ: 1rem;

  /* Boutons à libellé long : ils doivent rétrécir sur un téléphone étroit, sinon
     « Roulé et mangé » déborde. Une seule déclaration partagée là où il y en
     avait cinq, aux minima et maxima tous différents sans raison apparente. */
  --texte-bouton-adaptatif: clamp(0.75rem, 2.8vw, 0.9rem);

  /* ---------- L'échelle d'espacement (21/08/2026) ----------

     **On choisit un rôle, jamais un nombre** — exactement la règle de l'échelle de texte
     ci-dessus, et posée pour la même raison. Relevé avant d'écrire : **348 déclarations**
     d'espacement dans cette feuille pour **26 valeurs distinctes**, dont **quinze hors de
     toute grille** (0,1 · 0,15 · 0,2 · 0,3 · 0,35 · 0,4 · 0,55 · 0,6 · 0,65 · 0,7 · 0,8 ·
     0,85 · 0,9 · 1,1 · 1,4 rem).

     **Ce que ça donnait à l'écran**, mesuré sur les onze pages : l'écart entre deux blocs
     d'une page — une seule et même relation — valait **4, 8, 10, 12, 16, 19, 20, 24 ou 32
     px** selon l'endroit. Signalé par le développeur sur le bouton « Lancer une autre
     édition », qui n'en est qu'un cas parmi d'autres.

     **Base de 4 px**, celle que la feuille employait déjà majoritairement : onze des vingt-six
     valeurs étaient des multiples de 0,25 rem.

     **Deux valeurs gardent leur place hors de l'échelle** : celles qui portent un commentaire
     disant qu'elles ont été mesurées à l'écran pour corriger un défaut précis. Les aligner
     rouvrirait un défaut déjà payé. */
  /* **Le palier des pastilles**, et il a été ajouté en convertissant : les six `0.1rem` du
     projet habillent des objets de quelques pixels — pastille de réaction, badge d'hôte,
     compteur d'invités. Les monter au palier suivant les aurait épaissis de moitié. C'est le
     seul cas où l'écart se compte en pixels et non en respiration. */
  --espace-minime: 0.125rem;
  --espace-minuscule: 0.25rem;  /* une icône et son mot, deux lignes d'un même paragraphe */
  --espace-petit: 0.5rem;       /* à l'intérieur d'un groupe : boutons d'une rangée, champs */
  --espace-moyen: 0.75rem;      /* un intitulé et ce qu'il annonce */
  --espace-grand: 1rem;         /* deux blocs d'une même carte */
  --espace-bloc: 1.5rem;        /* deux zones d'une page */

  /* **Un cas nommé et non un palier** : dégager les deux boutons `fixed` du haut d'écran, en
     40x40 posés à 16 px du bord. 16 + 40 = 56, et c'est une mesure, pas un choix — trois
     règles s'en servent déjà (bandeau d'erreur, bandeau visiteur, mot pour le groupe). */
  --espace-degagement: 3.5rem;

  /* Affichage. Les deux chiffres ne servent qu'aux statistiques ; `--titre-section` a un
     second emploi depuis le 20/08/2026, le pseudo de l'hôte sur la carte du mardi — qui est
     bien le titre du premier groupe de cette carte, et non une simple emphase. */
  --titre-section: 1.2rem;
  --chiffre: 1.8rem;
  --chiffre-principal: 2.2rem;

  /* Icônes. `.ti` se dimensionne en `em`, donc régler une icône passe par
     `font-size` — mais ce sont des tailles d'**objet**, pas de texte : elles ne
     partagent rien avec l'échelle ci-dessus et ne doivent pas y être ramenées. */
  --icone-petite: 0.85rem;  /* glyphe dans un bouton rond de 22 px */
  --icone-texte: 1rem;      /* glyphe posé au fil du texte */
  --icone-bouton: 1.15rem;  /* bouton-icône */
  --icone-grande: 1.4rem;   /* icône décorative, tuile d'ajout de photo */

  /* **Seule couleur du projet définie ici et non dans un bloc de thème**, et c'est
     délibéré : elle habille le halo du tracé, dont le rôle est de se détacher du
     **fond de carte** OpenStreetMap — lequel est le même quel que soit le thème
     choisi. La faire varier avec la palette la ferait varier avec quelque chose dont
     elle ne dépend pas (voir Trace de la sortie). */
  --couleur-halo-trace: rgba(255, 255, 255, 0.85);

  /* **Le liseré sous le tracé est sombre, et ce n'est pas le même que le halo ci-dessus.**
     Les deux étaient confondus jusqu'au 15/08/2026, tous deux blancs — et c'était le défaut :
     un liseré blanc sur un fond topographique pâle, semé de routes blanches, ne détache rien.
     Épaissir le trait avait aidé sans suffire, parce que le problème n'était pas la taille
     mais **l'absence de contraste entre le liseré et le fond**.

     Sombre, il fonctionne dans les deux sens : il tranche sur les fonds clairs (plan,
     topographique) et, sur l'orthophoto qui est sombre, c'est la couleur vive du trait
     lui-même qui porte. Une seule valeur pour les trois calques, comme les teintes de pente.

     **Le halo clair reste employé par les chevrons de sens**, qui sont des triangles sombres
     et ont besoin d'un cerclage clair pour se détacher : les fusionner les rendrait
     invisibles. D'où deux variables là où il n'y en avait qu'une. */
  --couleur-liseret-trace: rgba(20, 22, 28, 0.9);

  /* **Les six teintes de pente**, ici pour la même raison que le halo : elles se lisent
     sur le fond de carte, identique dans les trois thèmes, et sur le profil qui reprend la
     même échelle. Les faire varier avec la palette les ferait dépendre de ce dont elles ne
     dépendent pas.

     **Chaud = ça monte, froid = ça descend**, gris = plat. La première version coloriait
     la valeur absolue de la pente : une descente de 12 % et un mur de 12 % avaient donc la
     même couleur, ce qui n'est pas du tout la même sortie en VTT.

     Trois contraintes ont dicté ces valeurs : **aucun vert** (les forêts d'une carte en
     sont pleines, l'orthophoto encore plus), **aucun couple bleu/vert** que les daltonismes
     courants confondent — règle déjà appliquée aux séries du graphe — et le couple retenu,
     bleu contre orange, qui est justement le plus robuste de ce point de vue. Pas de jaune
     franc non plus : il disparaît sur les champs clairs d'une orthophoto. La couleur ne
     porte de toute façon jamais seule l'information, la légende donne les seuils. */
  --couleur-pente-montee-forte: #C21A1A;
  --couleur-pente-montee-moyenne: #E2661E;
  --couleur-pente-montee-douce: #F0A030;
  --couleur-pente-plat: #8892A0;
  --couleur-pente-descente-douce: #55A8E0;
  /* **Violet foncé plutôt que bleu foncé** : une descente raide doit se lire comme un
     avertissement, pas comme la suite paisible d'un dégradé de bleus. L'échelle des
     descentes fonce donc avec la pente — L* de 66 à 29,5 — ce qui ajoute l'intensité à la
     teinte. Mesuré au passage : ce violet est **plus** distinguable des cinq autres en
     deutéranopie que le bleu foncé qu'il remplace (écart CIE76 de 27,7 contre 20,7), et le
     violet-magenta qui semblait plus alarmant encore a été écarté pour la raison inverse —
     16,6 face au gris du plat, soit une confusion possible. */
  --couleur-pente-descente-forte: #5B2D8E;

  /* Le remplissage des chevrons de sens. Sombre et neutre : ils doivent se détacher du
     trait coloré **et** du fond de carte, dans les trois thèmes. */
  --couleur-chevron-trace: #1F2430;
}

/* **Le halo de sélection du navigateur est neutralisé** (03/09/2026), et c'est la moitié qui
   manquait à l'essai du matin. Sur un appareil tactile, le navigateur peint de lui-même un
   rectangle gris translucide sur ce qu'on touche — et **il ignore le `border-radius`**, d'où
   « des formes carrées sur des items arrondis », signalé à l'usage. Il était là depuis toujours,
   sur tous les éléments interactifs ; il n'avait simplement jamais été regardé.

   **Le premier essai s'était ajouté à lui au lieu de le remplacer** : on posait une opacité
   par-dessus le carré, ce qui rendait le carré plus visible. La leçon vaut au-delà — avant
   d'ajouter un retour visuel, regarder celui que le navigateur donne déjà.

   Le jeu portait déjà cette neutralisation pour son propre compte ; elle lui est désormais
   inutile, et sa ligne est laissée telle quelle plutôt que touchée en passant. */
html {
  -webkit-tap-highlight-color: transparent;
}

/* **Un appui doit se voir, et il épouse la forme de ce qu'on touche.** Le projet n'avait
   **aucun** `:active` avant ce jour — vérifié, zéro occurrence. Sur un poste le survol compense
   et le manque ne s'est jamais vu ; au doigt, rien ne bougeait entre le moment où l'on touche et
   celui où le contenu arrive, ce qui fait paraître une interface morte indépendamment de sa
   vitesse.

   **L'opacité plutôt qu'un fond ou un enfoncement**, et c'est ce qui règle le défaut du premier
   essai : elle **ne dessine rien**, donc elle ne peut pas contredire un arrondi — elle atténue
   l'élément tel qu'il est, quelle que soit sa forme. Elle ne touche à aucune métrique non plus,
   donc rien ne se déplace, ce qui compte dans un projet dont les espacements ont eu leur propre
   revue. Et elle marche sur un bouton plein comme sur du texte nu.

   **Aucune transition, délibérément** : l'immédiateté *est* le signal. Un fondu de cent
   millisecondes retarderait la seule chose qu'on ajoute, et rendrait le retour dépendant de
   `prefers-reduced-motion`, qui n'a rien à voir avec le fait de savoir si un appui a été pris.

   **Deux familles** : ce qui est un bouton pour le navigateur — les 196 `<button>` du HTML, ceux
   que le JS fabrique, les faux boutons par `role` — puis les surfaces qui se comportent comme tel
   sans en être. Un bouton désactivé en est exclu : il ne répond pas, il ne doit pas faire mine de
   répondre. Les liens de texte ordinaires aussi : un lien dans une phrase n'est pas une surface
   tactile, et l'y traiter comme telle ferait clignoter des paragraphes entiers. */
button:not(:disabled):active,
[role="button"]:not([aria-disabled="true"]):active,
.tuile-ajout-photo:active,
.vignette-carte:active,
.carte-sortie-libre:active,
.ouvrir-detail-sortie:active,
.hote-detail:active,
.depot-trace:active,
.choix-reglage:active,
a.ligne-fil:active,
a.bouton-livetrack[href]:active,
#meteo-sortie:not(.erreur):active,
#detail-libre-meteo:not(.erreur):active {
  opacity: 0.55;
}

/* --- Icônes Tabler en masques SVG ---------------------------------------
   Auparavant chargées comme police depuis jsDelivr : ~206 Ko de CSS + ~451 Ko
   de police pour les 11 icônes réellement utilisées. Elles sont désormais
   embarquées ici, ce qui supprime la dépendance externe (et son risque de
   chaîne d'approvisionnement) et les fait fonctionner hors ligne — utile pour
   une PWA.

   `mask` plutôt que `background-image` : le masque est peint avec
   `background-color: currentColor`, donc l'icône hérite de la couleur du texte
   exactement comme le faisait la police. Taille en `em` pour hériter aussi de
   `font-size`. C'est ce qui permet de garder les classes `ti ti-<nom>` telles
   quelles : ni le HTML ni le JS n'ont eu à changer.

   Tracés issus de Tabler Icons 3.46.0 (licence MIT).
   Pour en ajouter une : récupérer le SVG « outline » du même jeu, retirer le
   chemin de cadre transparent, fixer `stroke` à une couleur opaque (le masque
   ne lit que l'alpha), puis encoder en URL. ---------------------------------- */

.ti {
  display: inline-block;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  vertical-align: -0.125em;
  flex-shrink: 0;
  -webkit-mask: var(--icone) no-repeat center / contain;
  mask: var(--icone) no-repeat center / contain;
}

.ti-arrow-left { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3Cpath d=%22M5 12l6 6%22 /%3E%3Cpath d=%22M5 12l6 -6%22 /%3E%3C/svg%3E"); }
.ti-bell { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6%22 /%3E%3Cpath d=%22M9 17v1a3 3 0 0 0 6 0v-1%22 /%3E%3C/svg%3E"); }
.ti-bike { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M2 18a3 3 0 1 0 6 0a3 3 0 0 0 -6 0%22 /%3E%3Cpath d=%22M16 18a3 3 0 1 0 6 0a3 3 0 0 0 -6 0%22 /%3E%3Cpath d=%22M12 19v-4l-3 -3l5 -4l2 3h3%22 /%3E%3Cpath d=%22M13.007 5a2 2 0 1 0 4 0a2 2 0 1 0 -4 0%22 /%3E%3C/svg%3E"); }
.ti-calendar-event { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z%22 /%3E%3Cpath d=%22M16 3v4%22 /%3E%3Cpath d=%22M8 3v4%22 /%3E%3Cpath d=%22M4 11h16%22 /%3E%3Cpath d=%22M8 15h2v2h-2z%22 /%3E%3C/svg%3E"); }
.ti-bus { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0%22 /%3E%3Cpath d=%22M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0%22 /%3E%3Cpath d=%22M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8%22 /%3E%3Cpath d=%22M16 5l1.5 7l4.5 0%22 /%3E%3Cpath d=%22M2 10l15 0%22 /%3E%3Cpath d=%22M7 5l0 5%22 /%3E%3Cpath d=%22M12 5l0 5%22 /%3E%3C/svg%3E"); }
.ti-camera { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 7h1a2 2 0 0 0 2 -2a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2%22 /%3E%3Cpath d=%22M9 13a3 3 0 1 0 6 0a3 3 0 0 0 -6 0%22 /%3E%3C/svg%3E"); }
.ti-chart-bar { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -6%22 /%3E%3Cpath d=%22M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -10%22 /%3E%3Cpath d=%22M9 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -14%22 /%3E%3Cpath d=%22M4 20h14%22 /%3E%3C/svg%3E"); }
.ti-chevron-down { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M6 9l6 6l6 -6%22 /%3E%3C/svg%3E"); }
.ti-chevron-left { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M15 6l-6 6l6 6%22 /%3E%3C/svg%3E"); }
.ti-chevron-right { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M9 6l6 6l-6 6%22 /%3E%3C/svg%3E"); }
.ti-clock { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0%22 /%3E%3Cpath d=%22M12 7v5l3 3%22 /%3E%3C/svg%3E"); }
.ti-download { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2%22 /%3E%3Cpath d=%22M7 11l5 5l5 -5%22 /%3E%3Cpath d=%22M12 4l0 12%22 /%3E%3C/svg%3E"); }
.ti-file { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M14 3v4a1 1 0 0 0 1 1h4%22 /%3E%3Cpath d=%22M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z%22 /%3E%3C/svg%3E"); }
.ti-ghost { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 11a7 7 0 0 1 14 0v7a1.78 1.78 0 0 1 -3.1 1.4a1.65 1.65 0 0 0 -2.6 0a1.65 1.65 0 0 1 -2.6 0a1.65 1.65 0 0 0 -2.6 0a1.78 1.78 0 0 1 -3.1 -1.4v-7%22 /%3E%3Cpath d=%22M10 10l.01 0%22 /%3E%3Cpath d=%22M14 10l.01 0%22 /%3E%3Cpath d=%22M10 14a3.5 3.5 0 0 0 4 0%22 /%3E%3C/svg%3E"); }
.ti-hand-middle-finger { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M8 13v-2.5a1.5 1.5 0 0 1 3 0v1.5%22 /%3E%3Cpath d=%22M14 10.5a1.5 1.5 0 0 1 3 0v1.5%22 /%3E%3Cpath d=%22M17 11.5a1.5 1.5 0 0 1 3 0v4.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47%22 /%3E%3Cpath d=%22M11 11.5v-8a1.5 1.5 0 1 1 3 0v8.5%22 /%3E%3C/svg%3E"); }
.ti-history { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 8l0 4l2 2%22 /%3E%3Cpath d=%22M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5%22 /%3E%3C/svg%3E"); }
.ti-home { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l-2 0l9 -9l9 9l-2 0%22 /%3E%3Cpath d=%22M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7%22 /%3E%3Cpath d=%22M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6%22 /%3E%3C/svg%3E"); }
.ti-map-pin { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0%22 /%3E%3Cpath d=%22M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z%22 /%3E%3C/svg%3E"); }
.ti-message { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M8 9h8%22 /%3E%3Cpath d=%22M8 13h6%22 /%3E%3Cpath d=%22M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z%22 /%3E%3C/svg%3E"); }
/* **Le seul tracé du jeu qui ne vient PAS de Tabler**, et il faut le savoir : composé le
   23/08/2026 dans sa grammaire (24×24, `stroke 2`, bouts arrondis) à partir des primitives que
   `ti-ghost` emploie déjà — cercle, yeux, sourire. Le jeu extrait ne contient aucun visage, et
   Tabler en a plusieurs : le jour où on récupère le vrai `mood-*`, c'est cette ligne-ci qu'on
   remplace, et rien d'autre. */
.ti-mdr { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0%22 /%3E%3Cpath d=%22M8 11l1.5 -1.5l1.5 1.5%22 /%3E%3Cpath d=%22M13 11l1.5 -1.5l1.5 1.5%22 /%3E%3Cpath d=%22M8 14a4 4 0 1 0 8 0h-8%22 /%3E%3C/svg%3E"); }
.ti-moon-stars { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454l0 .008%22 /%3E%3Cpath d=%22M17 4a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2%22 /%3E%3Cpath d=%22M19 11h2m-1 -1v2%22 /%3E%3C/svg%3E"); }
.ti-pencil { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4%22 /%3E%3Cpath d=%22M13.5 6.5l4 4%22 /%3E%3C/svg%3E"); }
.ti-minus { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E"); }
.ti-player-play { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M7 4v16l13 -8z%22 /%3E%3C/svg%3E"); }
.ti-plus { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M12 5l0 14%22 /%3E%3Cpath d=%22M5 12l14 0%22 /%3E%3C/svg%3E"); }
.ti-send { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M10 14l11 -11%22 /%3E%3Cpath d=%22M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5%22 /%3E%3C/svg%3E"); }
.ti-share { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0%22 /%3E%3Cpath d=%22M15 6a3 3 0 1 0 6 0a3 3 0 1 0 -6 0%22 /%3E%3Cpath d=%22M15 18a3 3 0 1 0 6 0a3 3 0 1 0 -6 0%22 /%3E%3Cpath d=%22M8.7 10.7l6.6 -3.4%22 /%3E%3Cpath d=%22M8.7 13.3l6.6 3.4%22 /%3E%3C/svg%3E"); }
.ti-share-2 { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M8 9h-1a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-8a2 2 0 0 0 -2 -2h-1%22 /%3E%3Cpath d=%22M12 14v-11%22 /%3E%3Cpath d=%22M9 6l3 -3l3 3%22 /%3E%3C/svg%3E"); }
.ti-sunset-2 { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M3 13h1%22 /%3E%3Cpath d=%22M20 13h1%22 /%3E%3Cpath d=%22M5.6 6.6l.7 .7%22 /%3E%3Cpath d=%22M18.4 6.6l-.7 .7%22 /%3E%3Cpath d=%22M8 13a4 4 0 1 1 8 0%22 /%3E%3Cpath d=%22M3 17h18%22 /%3E%3Cpath d=%22M7 20h5%22 /%3E%3Cpath d=%22M16 20h1%22 /%3E%3Cpath d=%22M12 5v-1%22 /%3E%3C/svg%3E"); }
.ti-sun { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0%22 /%3E%3Cpath d=%22M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7%22 /%3E%3C/svg%3E"); }
.ti-thumb-up { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3%22 /%3E%3C/svg%3E"); }
.ti-thumb-up-filled { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22%23000%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3%22 /%3E%3C/svg%3E"); }
.ti-tool { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5%22 /%3E%3C/svg%3E"); }
.ti-tools-kitchen-2 { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M19 3v12h-5c-.023 -3.681 .184 -7.406 5 -12m0 12v6h-1v-3m-10 -14v17m-3 -17v3a3 3 0 1 0 6 0v-3%22 /%3E%3C/svg%3E"); }
.ti-trash { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M4 7l16 0%22 /%3E%3Cpath d=%22M10 11l0 6%22 /%3E%3Cpath d=%22M14 11l0 6%22 /%3E%3Cpath d=%22M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12%22 /%3E%3Cpath d=%22M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3%22 /%3E%3C/svg%3E"); }
.ti-trees { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M16 5l3 3l-2 1l4 4l-3 1l4 4h-9%22 /%3E%3Cpath d=%22M15 21l0 -3%22 /%3E%3Cpath d=%22M8 13l-2 -2%22 /%3E%3Cpath d=%22M8 12l2 -2%22 /%3E%3Cpath d=%22M8 21v-13%22 /%3E%3Cpath d=%22M5.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4l-.176 -.005%22 /%3E%3C/svg%3E"); }
.ti-users { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M5 7a4 4 0 1 0 8 0a4 4 0 1 0 -8 0%22 /%3E%3Cpath d=%22M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2%22 /%3E%3Cpath d=%22M16 3.13a4 4 0 0 1 0 7.75%22 /%3E%3Cpath d=%22M21 21v-2a4 4 0 0 0 -3 -3.85%22 /%3E%3C/svg%3E"); }
.ti-x { --icone: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22 %3E%3Cpath d=%22M18 6l-12 12%22 /%3E%3Cpath d=%22M6 6l12 12%22 /%3E%3C/svg%3E"); }

/* **Le fond du thème sombre, sorti de son bloc** (08/09/2026) : `theme.js` en a besoin
   **quel que soit le thème affiché**, pour la barre d état d une PWA Android — Chrome refuse une
   couleur claire quand le système est en mode sombre, et il faut alors lui annoncer du sombre
   plutôt que de le laisser teinter les icônes d après une couleur qu il n applique pas.
   **Une variable et non une valeur recopiée** : la règle des rôles vaut aussi ici, et le bloc
   sombre juste en dessous la reprend. */
:root {
  --couleur-fond-sombre: #18181b;
}

:root[data-theme="sombre"] {
  color-scheme: dark;
  --couleur-reaction: #FF6B9D;        /* l'accent du thème, 4,90:1 au pire fond */
  --couleur-accent: #2FD9E0;
  --couleur-surface-teintee: #27272a;
  --couleur-danger: #ff8c42;
  --couleur-texte: #e8e6ed;
  --couleur-texte-discret: #a1a1aa;
  --couleur-fond: var(--couleur-fond-sombre);
  --couleur-surface-encastree: #18181b;
  --couleur-texte-bouton-principal: var(--couleur-fond);
  --couleur-texte-bouton-danger: var(--couleur-fond);
  --couleur-bordure: #3f3f46;
  --couleur-surface-neutre: #303035;
  --couleur-ombre-bouton: rgba(0, 0, 0, 0.45);
  /* **L'accent employé comme FORME**, sur la surface d'un bouton : un glyphe, un trait, un
     pictogramme. Seuil 3:1 (WCAG 1.4.11, composant non textuel), là où `--couleur-accent-texte`,
     son symétrique, est l'accent employé comme *texte* et vise 4,5:1. Deux thèmes tiennent avec
     l'accent brut ; `clair-orange` non — son accent (#FB923C) et la surface teintée de ses
     boutons (#FCE8D6) sont deux oranges, mesurés à **1,90:1** (`D3` du 21/08/2026).
     **Même famille que `--couleur-reaction`**, née du même problème sur le même thème. */
  --couleur-accent-forme: var(--couleur-accent);   /* #2FD9E0 sur #27272a : 8,60:1 */
  --couleur-accent-texte: #FF6B9D;
  --couleur-accent-secondaire: #FF6B9D;
  --couleur-accent-secondaire-texte: #FF6B9D;
  /* Texte posé **sur** l'accent secondaire, quand il sert de fond plein — le mot pour le groupe
     est le premier à s'en servir ainsi (21/08/2026). **Le nom est long exprès** :
     `--couleur-accent-secondaire-texte` juste au-dessus veut dire l'inverse — l'accent secondaire
     *employé comme* couleur de texte — et deux noms qui ne diffèrent que par l'ordre des mots se
     confondraient à la première relecture.

     **Sombre : texte foncé**, l'accent y étant un rose clair. 6,62:1, mesuré. */
  --couleur-texte-sur-accent-secondaire: #18181b;
  --couleur-serie-velo: #2FD9E0;
  --couleur-serie-table: var(--couleur-accent-secondaire);
  /* Motif du thème sombre, distinct des hachures diagonales des deux
     thèmes clairs : trois silhouettes (éclair, doigt d'honneur, chope)
     sur une tuile raccordable de 180x120 unités, rendue à 120x80 px.
     L'opacité est cuite dans le SVG via `fill-opacity` puisqu'un
     `background-image` ne peut pas recevoir d'`opacity` en CSS. Valeur
     volontairement plus basse que les hachures (0,04 contre 0,05) : des
     silhouettes pleines couvrent bien plus de surface que de simples
     traits, à opacité égale le rendu serait plus marqué. */
  --motif-fond: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%2280%22 viewBox=%220 0 180 120%22%3E%3Cdefs%3E%3Cpath id=%22c%22 d=%22M56,18 A8,8 0 0 0 40,18 L40,52 A9,9 0 0 0 22,52 L22,58 A8,8 0 0 0 22,74 L22,76 Q22,88 34,88 L74,88 Q86,88 86,76 L86,52 A6,6 0 0 0 74,52 A9,9 0 0 0 56,52 Z%22/%3E%3Cmask id=%22a%22 maskUnits=%22userSpaceOnUse%22 x=%220%22 y=%220%22 width=%22100%22 height=%22100%22%3E%3Crect width=%22100%22 height=%22100%22 fill=%22%23fff%22/%3E%3Cg fill=%22none%22 stroke=%22%23000%22 stroke-width=%223%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M40,54 L40,70%22/%3E%3Cpath d=%22M56,54 L56,70%22/%3E%3Cpath d=%22M74,54 L74,70%22/%3E%3Cpath d=%22M23,60 C20,68 23,76 30,79%22/%3E%3C/g%3E%3C/mask%3E%3Cmask id=%22b%22 maskUnits=%22userSpaceOnUse%22 x=%220%22 y=%220%22 width=%22100%22 height=%22100%22%3E%3Crect width=%22100%22 height=%22100%22 fill=%22%23fff%22/%3E%3Crect x=%2268%22 y=%2248%22 width=%2212%22 height=%2222%22 rx=%226%22 fill=%22%23000%22/%3E%3Cg fill=%22none%22 stroke=%22%23000%22 stroke-width=%223%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M14,32 C22,28 30,36 38,32 C46,28 54,36 62,32%22/%3E%3Cpath d=%22M24,42 L24,80%22/%3E%3Cpath d=%22M37,44 L37,80%22/%3E%3Cpath d=%22M50,42 L50,80%22/%3E%3C/g%3E%3C/mask%3E%3Cg id=%22D%22 mask=%22url(%23a)%22%3E%3Cuse href=%22%23c%22/%3E%3C/g%3E%3Cg id=%22P%22 mask=%22url(%23b)%22%3E%3Cpath d=%22M12,33 C5,31 5,20 13,19 C13,10 25,6 31,13 C38,5 50,7 52,15 C61,12 69,17 67,25 C73,28 72,33 66,33 Z%22/%3E%3Cpath d=%22M14,32 C22,28 30,36 38,32 C46,28 54,36 62,32 L62,80 C62,88 56,92 48,92 L28,92 C20,92 14,88 14,80 Z%22/%3E%3Crect x=%2262%22 y=%2240%22 width=%2224%22 height=%2238%22 rx=%2213%22/%3E%3C/g%3E%3Cpath id=%22E%22 d=%22M60,8 L30,54 L47,54 L40,92 L72,42 L54,42 Z%22 stroke-linejoin=%22round%22/%3E%3C/defs%3E%3Cg fill=%22%23fff%22 fill-opacity=%220.04%22%3E%3Cg transform=%22translate(0,0) scale(0.6)%22%3E%3Cg transform=%22rotate(-8 50 50)%22%3E%3Cuse href=%22%23E%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(60,0) scale(0.6)%22%3E%3Cg transform=%22rotate(6 50 50)%22%3E%3Cuse href=%22%23D%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(120,0) scale(0.6)%22%3E%3Cg transform=%22rotate(-5 50 50)%22%3E%3Cuse href=%22%23P%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(-30,60) scale(0.6)%22%3E%3Cg transform=%22rotate(-7 50 50)%22%3E%3Cuse href=%22%23D%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(30,60) scale(0.6)%22%3E%3Cg transform=%22rotate(9 50 50)%22%3E%3Cuse href=%22%23P%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(90,60) scale(0.6)%22%3E%3Cg transform=%22rotate(7 50 50)%22%3E%3Cuse href=%22%23E%22/%3E%3C/g%3E%3C/g%3E%3Cg transform=%22translate(150,60) scale(0.6)%22%3E%3Cg transform=%22rotate(-7 50 50)%22%3E%3Cuse href=%22%23D%22/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}

:root[data-theme="clair"] {
  color-scheme: light;
  --couleur-reaction: #1F5F96;         /* l'accent du thème, 5,81:1 */
  /* Assombri de #4898D8 le 15/08/2026 : a 2,93:1 contre le fond il passait **sous les
     3:1** exiges des elements graphiques (WCAG 1.4.11), donc le bord des boutons pleins
     etait mou. A 3,47:1 il passe, et le texte sombre pose dessus reste a 4,82:1. */
  --couleur-accent: #3F8ACB;
  --couleur-texte-bouton-principal: #18181B;
  /* **Surfaces et bordure ramenées à des gris neutres le 15/08/2026**, ce que cette fiche
     annonçait depuis le début sans que le CSS le fasse : « surfaces et bordure en gris
     neutres plutôt qu'en bleus désaturés — pour que le bleu ne serve que d'accent, sans
     teinter toute l'interface ». Mesuré avant correction, l'inverse était vrai — la surface
     était à HSL(210°, 47 %) et la bordure à HSL(209°, 35 %), soit **onze variables du thème
     tenant dans neuf degrés de teinte**. Tout était le même bleu, seule la clarté variait,
     et c'est ce qui donnait au thème son air délavé.
     Les gris retenus étaient alors **légèrement chauds** (autour de 30°), au motif qu'un gris
     franchement neutre posé sur un blanc cassé chaud se lit sale. **C'était vrai, et c'est
     précisément ce qui a été corrigé le 20/08/2026** : le remède avait chassé le bleu et laissé
     du beige. Mesuré, la carte était à 40° de teinte et l'accent à 208° — **168° d'écart**,
     quand le thème orange n'en a qu'**un seul** (carte 28°, accent 27°). Le bleu ne se lisait
     donc pas comme un thème bleu, mais comme le thème orange dont on aurait retiré l'orange.

     **La règle appliquée est celle des systèmes de couleur** : une fois la température choisie,
     elle vaut pour *tous* les neutres — fond, carte, encastré, bordure. Ici le texte et
     l'accent sont froids, les neutres le sont donc aussi. Le fond chaud n'est pas conservé
     « par cohérence avec clair-orange » : les deux thèmes clairs n'ont aucune raison de
     partager un fond, ils ont des accents opposés.

     **Et l'élévation est inversée du même geste.** La carte était plus sombre que la page
     (L 90 % contre 97 %), donc l'objet le plus important de l'écran était sa zone la plus
     sombre : il reculait. Elle est désormais quasi blanche sur une page légèrement teintée,
     ce que fait tout thème clair — et le `drop-shadow` que `.conteneur-sortie` portait déjà
     dans ce thème devient enfin lisible, une ombre sous une carte plus foncée que sa page
     se lisant comme un creux. */
  --couleur-surface-teintee: #FCFDFE;
  --couleur-danger: #B54B26;
  --couleur-texte: #13324F;
  --couleur-texte-discret: #4F6377;
  --couleur-fond: #EDF1F6;
  /* **Un cran sous la carte, et c'est mesuré** : la fiche palette visait 1,15:1 entre une
     surface encastrée et la carte qui la contient, pour que les cartes météo horaires s'y
     distinguent autrement que par leur arrondi — et relevait que ce thème n'y arrivait pas
     (1,08:1), point laissé ouvert. #E5EDF5 le tient à **1,161:1**, texte à 11,12:1 et texte
     discret à 5,25:1. */
  --couleur-surface-encastree: #E5EDF5;
  /* Bordure à **1,94:1** contre le fond — la parité avec la valeur du 15/08/2026 (2,00:1),
     tenue volontairement en repassant la teinte au froid. La raison d'alors vaut toujours à
     moitié : ce thème n'a **ni halo** (le sombre en a un) **ni bordure colorée** (l'orange a la
     sienne à 2,18:1). Mais depuis le 20/08/2026 la carte ne dépend plus de son seul contour
     pour se détacher — elle est plus claire que la page **et** porte son ombre. Un premier
     candidat plus doux (#C8D4E0) tombait à 1,33:1 : écarté pour cette raison. */
  --couleur-bordure: #9CB1C8;
  --couleur-surface-neutre: #E8EEF5;
  --couleur-ombre-bouton: rgba(19, 50, 79, 0.20);
  --couleur-accent-forme: var(--couleur-accent);   /* #3F8ACB sur #FCFDFE : 3,61:1 */
  --couleur-accent-texte: #1F5F96;
  --couleur-accent-secondaire: #B03A6B;
  --couleur-accent-secondaire-texte: #B03A6B;
  /* Blanc, l'accent secondaire y étant un magenta profond. 5,73:1, mesuré. */
  --couleur-texte-sur-accent-secondaire: #FFFFFF;
  --couleur-serie-velo: #2571AE;
  --couleur-serie-table: var(--couleur-accent-secondaire);
  --couleur-ombre-conteneur: rgba(19, 50, 79, 0.16);
  --motif-fond: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2232%22 height=%2232%22 viewBox=%220 0 32 32%22%3E%3Cline x1=%220%22 y1=%2226%22 x2=%2226%22 y2=%220%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-opacity=%220.05%22/%3E%3Cline x1=%226%22 y1=%2232%22 x2=%2232%22 y2=%226%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-opacity=%220.05%22/%3E%3C/svg%3E");
}

:root[data-theme="clair-orange"] {
  color-scheme: light;
  --couleur-reaction: #9C4803;  /* l'accent du thème **assombri d'un cran** : sa valeur
     nominale (#AC5004) plafonne à 4,42:1 sur le fond teinté, juste sous le 4,5:1 exigé
     pour du texte — et le compteur à côté du pouce en est. Ici 5,17:1 */
  --couleur-accent: #FB923C;
  --couleur-texte-bouton-principal: #18181B;
  --couleur-surface-teintee: #FCE8D6;
  --couleur-danger: #B54B26;
  --couleur-texte: #2C2416;
  --couleur-texte-discret: #6B5F45;
  --couleur-fond: #FAF8F5;
  --couleur-surface-encastree: #FAF8F5;
  --couleur-bordure: #D4A253;
  --couleur-surface-neutre: #E9E9E6;
  --couleur-ombre-bouton: rgba(0, 0, 0, 0.18);
  /* L'accent brut est ici à 1,90:1 sur la surface d'un bouton : la forme reprend la valeur
     déjà calculée pour l'encre, plutôt que d'ajouter un hex de plus à la palette. */
  --couleur-accent-forme: var(--couleur-accent-texte);   /* #AC5004 sur #FCE8D6 : 4,52:1 */
  --couleur-accent-texte: #AC5004;
  --couleur-accent-secondaire: #1F6F8B;
  --couleur-accent-secondaire-texte: #1E6C87;
  /* Blanc, l'accent secondaire y étant une sarcelle sombre. 5,67:1, mesuré. */
  --couleur-texte-sur-accent-secondaire: #FFFFFF;
  --couleur-serie-velo: #AC5004;
  --couleur-serie-table: var(--couleur-accent-secondaire);
  --motif-fond: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2232%22 height=%2232%22 viewBox=%220 0 32 32%22%3E%3Cline x1=%220%22 y1=%2226%22 x2=%2226%22 y2=%220%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-opacity=%220.05%22/%3E%3Cline x1=%226%22 y1=%2232%22 x2=%2232%22 y2=%226%22 stroke=%22%23000%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-opacity=%220.05%22/%3E%3C/svg%3E");
}

body {
  font-family: system-ui, sans-serif;
  color: var(--couleur-texte);
  background-color: var(--couleur-fond);
  /* Pour retirer la texture sur les trois thèmes d'un coup : remplacer
     var(--motif-fond) par none ci-dessous, seul point de contrôle. */
  background-image: var(--motif-fond);
  background-repeat: repeat;
  max-width: 480px;
  margin: 0 auto;
  padding: var(--espace-grand);
  text-align: center;
}

/* **`showModal()` rend le fond inerte au clic, pas au doigt** — c'est la méprise qui a valu
   la plainte des membres sur la visionneuse : on regarde une photo, on glisse un peu trop
   verticalement, et c'est la page derrière qui défile. Le comportement est conforme à la
   spécification : un dialogue modal neutralise les *interactions* avec le reste du document,
   jamais son défilement.

   La règle est posée **sur `dialog[open]` et non sur la visionneuse**, sur arbitrage : les
   sept surfaces modales du projet sont toutes des `<dialog>` — popups, carte plein écran,
   visionneuse —, elles avaient toutes le défaut, et une règle ciblée l'aurait laissé partout
   ailleurs en donnant l'impression de l'avoir traité.

   `overflow` posé sur `body` remonte bien à la zone de vue tant que `html` reste `visible`,
   ce qui est le cas ici. Effet de bord assumé sur poste fixe : la barre de défilement
   disparaît le temps de la modale, donc le contenu centré se décale d'une quinzaine de
   pixels — derrière un voile sombre, personne ne le voit. La position de lecture, elle, est
   conservée, ce qu'un `position: fixed` aurait perdu. */
body:has(dialog[open]) {
  overflow: hidden;
}

h1 {
  color: var(--couleur-texte);
}

/* **L'en-tête coûtait 308 px avant la moindre information**, mesuré sur un rendu réel à
   393x852 (Galaxy S25) : les boutons du sondage — le geste le plus fréquent de
   l'application — terminaient à 800 px, à cinquante pixels du bord de l'écran.

   **Le logo garde sa taille**, c'est une demande explicite : ses détails doivent rester
   lisibles. Le gain vient donc des espacements et d'eux seuls. Le `h1` ne contient qu'une
   image, mais gardait les marges par défaut du navigateur — 0,67 em de part et d'autre,
   soit environ 21 px chacune pour un titre de 36 px de haut. Ramenées à 0,3 em.

   La règle propre au thème sombre a disparu avec ce changement : elle abaissait la marge
   haute à 0,4 em, ce qui serait désormais **plus grand** que le défaut commun. */
.entete-sortie h1 {
  margin-top: 0.3em;
  margin-bottom: 0.3em;
}

.entete-sortie {
  width: fit-content;
  margin: 0 auto;
  text-align: left;
}

#liste-reponses,
/* `#formulaire-ajout-pas-du-mardi` **partage cette déclaration plutôt que d'en recopier une**
   (voir les Conventions de CLAUDE.md) : c'est le même objet que le formulaire d'ajout de
   membre — un bloc de saisie en tête de panneau d'administration — et l'écart ne se verrait
   qu'en comparant deux onglets, c'est-à-dire jamais. */
#formulaire-ajout-membre,
#formulaire-ajout-pas-du-mardi,
#formulaire-ajout-voyage,
#journal-admin {
  text-align: left;
}

/* **Le logo est un fond, et non trois `<img>` dont on en masque deux** (`RC-PERF-005`,
   28/08/2026). Chaque page déclarait les trois logos en `<img src>`, et le CSS cachait les
   deux qui ne correspondaient pas au thème — or **un navigateur télécharge un `<img>` même
   sous `display: none`**. Mesuré au panneau réseau, pas déduit : l'écran d'accueil tirait
   **501 154 octets** d'images pour n'en afficher que 198 Ko. 234 Ko perdus sur chacune des
   dix pages, 332 Ko sur l'accueil qui déclare aussi les trois titres.

   **Une `background-image` n'est chargée que si une règle qui s'applique la nomme**, donc
   seule celle du thème courant part sur le réseau. C'est la seule des trois pistes qui
   marche ici : `<picture>` et `srcset` choisissent sur une requête média, alors que le
   critère du projet est un **attribut sur `body`**.

   **Rien n'apparaît plus tôt ni plus tard qu'avant** : le logo était déjà invisible jusqu'à
   ce que `theme.js` pose `data-theme`, et les scripts sont en bas de page. Le fetch se
   déclenche désormais à cet instant-là au lieu de la lecture du HTML, c'est-à-dire au moment
   même où l'image pouvait s'afficher.

   **Le gabarit vient d'`aspect-ratio` et non plus de l'image.** Un `<img>` connaît ses
   dimensions natives ; une boîte vide, non. Les ratios sont ceux des fichiers, et les trois
   `max-width` sont inchangés — vérifié au navigateur, les boîtes rendues sont identiques au
   dixième de pixel près.

   **`role="img"` et `aria-label` dans le HTML remplacent l'`alt`** que la balise portait : un
   fond n'est rien pour un lecteur d'écran sans eux.

   Chaque thème garde son propre cadrage : les ratios diffèrent, une contrainte commune leur
   donnerait des tailles visuelles inégales. */
.logo {
  display: block;
  margin: 0 auto;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

:root[data-theme="sombre"] .logo {
  background-image: url("../images/logo-sombre.webp");
  max-width: 208px;
  aspect-ratio: 624 / 639;
}

:root[data-theme="clair"] .logo {
  background-image: url("../images/logo-clair-bleu.webp");
  max-width: 200px;
  aspect-ratio: 600 / 578;
}

:root[data-theme="clair-orange"] .logo {
  background-image: url("../images/logo-clair.webp");
  max-width: 208px;
  aspect-ratio: 624 / 560;
}

/* **Le logo est réduit sur les quatre pages secondaires**, depuis le 20/08/2026 — et il
   garde sa taille sur l'écran du mardi, qui est l'accueil et où la marque est le sujet.
   Mesuré au gabarit de référence (390 x 844) : l'en-tête consommait **34 % du premier
   écran** avant la moindre information, sur les cinq pages. Or l'historique, les
   statistiques, le voyage et les sorties pas du mardi sont des **listes qu'on vient
   parcourir**, et on n'y arrive jamais sans être passé par l'accueil : la marque y a déjà
   été vue.

   **La demande « le logo garde sa taille » est donc respectée là où elle a un sens**, et
   arbitrée page par page plutôt qu'en bloc. Une barre compacte au défilement avait été
   envisagée puis écartée : `#bouton-menu` étant déjà `position: fixed`, elle n'aurait
   apporté aucune navigation supplémentaire, et **aucune place à l'arrivée** — un en-tête
   qui ne se compacte qu'une fois défilé ne rend rien à l'écran non défilé. */
body.page-secondaire .logo {
  max-width: 90px;
}

/* Le titre suit le logo, même raison et même forme (`RC-PERF-005`) — l'explication complète
   vit au-dessus, sur `.logo`. Il n'existe que sur l'écran d'accueil, où il pesait à lui seul
   98 Ko d'images jamais montrées.

   **Chaque thème garde le pilote qu'il avait** : deux fixent la hauteur, le troisième la
   largeur. `width: fit-content` sur une boîte à `aspect-ratio` et à hauteur définie fait
   déduire la largeur du ratio — ce que faisait l'image par ses dimensions natives, en somme.
   Écrire les largeurs à la place aurait marché aussi, au prix de trois décimales arrondies
   qui décalaient déjà le rendu de 0,1 px.

   **`max-width: 100%` en plus, et il ne change rien à ce qui existe** : les trois titres
   tiennent dans le plus étroit des gabarits visés. Il ne sert que si un écran plus étroit
   apparaît un jour — auquel cas la boîte rétrécit en gardant son ratio, là où l'ancien `<img>`
   à hauteur fixe aurait débordé.

   **Mesuré avant et après, six boîtes** (trois thèmes × logo et titre) : cinq sont identiques
   au dixième de pixel, la sixième — le titre du thème orange — rend 225,6 px de large contre
   225,5. L'écart naît de l'arrondi sous-pixel de sa hauteur, amplifié par l'inclinaison de
   2° : il est plus petit qu'un pixel d'écran, et il est noté ici pour n'avoir pas à être
   recherché. */
.titre-image {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  transform: rotate(-2deg);
}

:root[data-theme="sombre"] .titre-image {
  background-image: url("../images/titre-sombre.webp");
  height: 72px;
  width: fit-content;
  aspect-ratio: 880 / 216;
  /* Annule l'inclinaison sticker de .titre-image (règle commune aux trois
     thèmes) : sur ce thème le titre reste droit, à la demande. */
  transform: none;
}

:root[data-theme="clair"] .titre-image {
  background-image: url("../images/titre-clair-bleu.webp");
  height: 54px;
  width: fit-content;
  aspect-ratio: 652 / 162;
}

:root[data-theme="clair-orange"] .titre-image {
  background-image: url("../images/titre-clair.webp");
  width: 224px;
  aspect-ratio: 672 / 149;
}

/* **Le bouton porte un fond depuis le 20/08/2026, et ce n'est pas décoratif.** Il est
   `position: fixed` : au défilement il se pose sur ce qui passe dessous. Sur la liste des
   réponses il tombait à côté d'un pseudo, sur la grille de photos **sur** une vignette — et
   là il ne s'agissait plus d'esthétique, puisque sur une image claire trois barres claires
   ne se voient plus du tout. Même famille que le bouton « Fermer » de la carte en plein
   écran (voir `A1r` de la revue du 17/08) : parfaitement fonctionnel, parfaitement invisible.

   **Pas de bordure, et c'est délibéré** : le projet n'a pas de `box-sizing` global, donc un
   1 px de contour ferait un bouton de 42 px là où la taille de cible est réglée à 40. Le fond
   et l'ombre suffisent à le décoller de ce qu'il survole. */
/* **Les deux boutons du haut d'écran partagent enfin leur déclaration** (22/08/2026). Le
   commentaire de `#bouton-fil` l'affirmait depuis toujours — « elles partagent tout sauf leur
   position, d'où une déclaration commune plutôt que deux » — alors que c'étaient **deux blocs
   distincts à plus de quatre mille lignes d'écart**, répétant quatorze déclarations
   identiques. C'est le mode d'échec de `.etiquette-bouton` que les Conventions de `CLAUDE.md`
   décrivent, et il s'est payé : poser la couleur du glyphe sur les deux (`D3`) a demandé deux
   éditions pour une seule décision.

   **La couleur vit ici, et les deux pictogrammes la reprennent par `currentColor`** : le
   masque d'une icône Tabler comme les trois traits du menu. C'est ce qui rend la décision
   unique — le seuil de 3:1 de `--couleur-accent-forme` ne se décide plus qu'à un endroit.

   Chacun ne garde que ce qui le distingue : son bord d'écran, et la mise en colonne des trois
   traits pour le menu. */
#bouton-menu,
#bouton-fil {
  position: fixed;
  top: 1rem;
  z-index: 1000;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 40px;
  height: 40px;
  background: var(--couleur-surface-teintee);
  border: none;
  border-radius: 50%;
  box-shadow: 0 1px 4px var(--couleur-ombre-bouton);
  padding: 0;
  cursor: pointer;
  color: var(--couleur-accent-forme);
}

#bouton-menu {
  left: 1rem;
  flex-direction: column;
  gap: 5px;
}

#bouton-menu .barre {
  width: 24px;
  height: 3px;
  border-radius: 2px;
  /* `currentColor`, pour que la couleur du pictogramme se décide au seul endroit où elle est
     posée — la déclaration partagée des deux boutons, plus haut. Un masque Tabler fait déjà
     exactement ça. */
  background-color: currentColor;
}

.voile {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.4);
  z-index: 900;
}

.panneau-lateral {
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: 260px;
  /* Filet pour les écrans très bas (paysage) : le contenu du menu peut alors
     dépasser la hauteur disponible, et doit rester atteignable. */
  overflow-y: auto;
  background-color: var(--couleur-fond);
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2);
  transform: translateX(-100%);
  transition: transform 0.25s ease;
  z-index: 950;
}

.panneau-lateral.ouvert {
  transform: translateX(0);
}

/* Colonne d'au moins la hauteur de l'écran : c'est elle qui donne à
   `#item-theme` l'espace libre que sa marge automatique absorbe pour se coller
   en bas. L'écart s'ajuste donc seul à la hauteur de l'appareil, sans JS ni
   media query.
   `min-height` et non `height` : sur un écran très bas (téléphone en paysage)
   le contenu peut dépasser, et il doit alors pouvoir s'étendre et défiler
   plutôt que d'être rogné.
   `box-sizing` est indispensable — sans lui le `padding` s'ajouterait aux
   100 % et le bas du panneau déborderait d'autant. */
#panneau-menu ul {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  box-sizing: border-box;
  list-style: none;
  margin: 0;
  padding: 4rem var(--espace-grand) var(--espace-grand);
}

/* **Un `<dialog>` depuis le 15/08/2026**, plus un `<div>` doublé d'un voile (voir
   `popups.js`, où le pourquoi est raconté). Trois réglages ne sont pas décoratifs mais
   annulent la feuille de style du navigateur, qui habille `dialog` de son côté :

   - `border: none` — elle en pose une, pleine et visible ;
   - `color: inherit` — elle pose `color: canvastext`, une couleur système qui ignore les
     trois thèmes du projet : sans ça le texte d'une popup resterait noir en thème sombre ;
   - `margin: 0` — elle pose `margin: auto` pour centrer, ce qui entrerait en conflit avec
     le centrage par `transform` conservé ici.

   Plus de `z-index` : un dialogue modal vit dans la couche supérieure, donc au-dessus de la
   visionneuse et de la carte sans qu'aucun palier n'ait à être choisi. C'est un des six
   paliers de l'escalier qui disparaît. */
.popup {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  border: none;
  color: inherit;
  background-color: var(--couleur-surface-teintee);
  border-radius: 14px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  padding: var(--espace-bloc);
  width: min(320px, calc(100% - 2rem));
  max-height: calc(100% - 2rem);
}

/* **`display` doit vivre ici et surtout pas dans la règle ci-dessus.** Le navigateur masque
   un `<dialog>` fermé par `display: none` dans sa propre feuille ; une valeur écrite sans le
   sélecteur `[open]` la remplacerait, et **les popups seraient visibles en permanence, sur
   toutes les pages**. C'est le piège de cette migration, et il est silencieux au sens où
   aucune erreur ne le signale — seulement une page couverte de boîtes de confirmation. */
.popup[open] {
  display: flex;
  flex-direction: column;
  gap: var(--espace-grand);
}

/* Le fond, qui remplace exactement les `<div class="voile">` supprimés — même couleur, même
   opacité. Il n'a pas besoin d'être positionné : le navigateur l'étend sur toute la vue. */
.popup::backdrop {
  background-color: rgba(0, 0, 0, 0.4);
}

/* **Un groupe de champs à l'intérieur d'une popup** (29/08/2026, le formulaire du calendrier).
   Le `gap` de `.popup[open]` vaut `--espace-grand` : c'est l'écart juste entre le titre, la
   saisie et les boutons, et c'est trop entre deux champs d'une même saisie, qui sont un
   groupe — « serré à l'intérieur d'un groupe, large entre deux groupes ». */
.champs-popup {
  display: flex;
  flex-direction: column;
  gap: var(--espace-petit);
}

/* Le `display` ci-dessus l'emporterait sur le `[hidden]` du navigateur, à spécificité égale
   (voir la revue des espacements du 21/08/2026). Rien ne masque ce bloc aujourd'hui ; la règle
   est là pour que ça reste vrai le jour où on le masquera. */
.champs-popup[hidden] {
  display: none;
}

/* **Le réglage du rappel** (29/08/2026) : un intitulé et deux listes sur une ligne, qui passe à
   la ligne quand la largeur manque. `flex-wrap` plutôt qu'une largeur fixe — sur 393 px, « Me le
   rappeler » plus deux listes plus un bouton ne tiennent pas côte à côte, et une liste rognée
   vaut moins qu'une ligne de plus. */
/* **Le rythme du formulaire passe au conteneur, pas aux champs** (04/09/2026). Les deux `<select>`
   héritaient de `margin: 0.25rem 0 0.75rem` — la paire qui porte les 16 px entre deux champs
   empilés (`D14`) — or ici ils ne sont pas empilés, ils sont **côte à côte dans un flex centré**.
   `align-items: center` centre la boîte **marges comprises**, si bien que l'asymétrie 4/12
   remontait le contenu visible de (12 − 4) / 2 : le libellé « Me le rappeler » se retrouvait
   4 px plus bas que les champs qu'il annonce. Mesuré avant, 4,0 px.

   **La marge est donc reportée telle quelle sur le conteneur**, aux mêmes valeurs : le rythme du
   formulaire ne bouge pas d'un pixel, seul l'alignement interne est réparé. */
/* **Le rappel : trois états qui ne se recouvrent jamais** (04/09/2026) — rien, on saisit, c'est
   posé. Les listes ne servent qu'à poser ; un rappel posé se lit (« 1 jour avant ») au lieu de se
   relire dans deux contrôles.

   **`[hidden]` est posé sur les deux conteneurs, et il le faut** : ce sont des `flex`, dont le
   `display` l'emporterait sur celui du navigateur à spécificité égale. C'est le piège que le
   projet a payé quatre fois, dont le bouton d'archive des photos corrigé le matin même — toute
   règle qui pose un `display` sur quelque chose de masquable doit poser aussi son `[hidden]`. */
/* Le conteneur que `rappel.js` remplit : la liste des rappels posés, la saisie, et le bouton
   d'ajout. **Aligné à gauche et non hérité du centre** — la page est centrée, et sans cette règle
   le bouton d'ajout se posait au milieu sous une liste alignée à gauche. */
.zone-rappel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--espace-petit);
  text-align: left;
}

/* La liste des rappels posés : des lignes, pas des puces — c'est bien une liste sémantiquement,
   mais elle se lit comme une suite de faits. */
.liste-rappels {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--espace-minuscule);
}

.rappel-pose,
.rappel-saisie {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-petit);
  margin: 0;
}

.rappel-saisie[hidden] {
  display: none;
}

/* **Le rythme appartient au conteneur, pas aux listes.** Elles héritent sinon de
   `margin: 0.25rem 0 0.75rem` — la paire qui porte les 16 px entre deux champs **empilés**
   (`D14`) —, or elles sont ici **côte à côte dans un flex centré**, où `align-items: center`
   centre la boîte marges comprises : l'asymétrie 4/12 décalait le contenu visible de 4 px. */
.rappel-saisie select {
  margin: 0;
}

/* Le bloc d'actions de la saisie : valider et renoncer restent ensemble et se posent à droite,
   la rangée se pliant de toute façon (355 px demandés pour 316 disponibles). */
.actions-rappel {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin-left: auto;
}

/* Le rappel posé se lit comme une donnée, pas comme un contrôle : le couple que le projet emploie
   partout pour une valeur, sans emphase — c'est l'intitulé au-dessus qui dit de quoi il s'agit. */
.texte-rappel {
  font-size: var(--texte-courant);
}

/* **Ces deux règles sont qualifiées par la zone, et il faut qu'elles le soient** : les deux
   boutons portent `.bouton-nu`, qui pose `font-size: var(--icone-bouton)` — 18,4 px — et qui est
   déclarée **plus bas dans la feuille**. À spécificité égale (0-1-0 chacune), c'est la dernière
   écrite qui gagne, donc mes tailles étaient ignorées en silence.
   **Le fond du sujet est que `.bouton-nu` est faite pour des boutons-icônes** et porte la taille
   d'un glyphe ; l'employer sur un bouton de **texte** hérite de cette taille. Mesuré : le bouton
   d'ajout sortait à 18,4 px là où la règle demandait 12,8 — signalé à l'usage, « le texte + ajouter
   un rappel semble disproportionné ». Le même piège attend tout bouton textuel portant cette
   classe. */
/* La croix de retrait, au gabarit des autres croix du projet. La même sert à renoncer à une
   saisie : dans les deux cas elle referme, elle ne détruit pas un contenu. */
.zone-rappel .retirer-rappel {
  font-size: var(--texte-fort);
  line-height: 1;
  padding: var(--espace-minuscule) var(--espace-petit);
}

/* L'ajout se lit comme une action discrète, pas comme le geste de l'écran : même famille que le
   « + » de la tuile d'ajout d'une photo, qui ouvre sans rien décider.

   **`--texte-etiquette` depuis le 04/09/2026, signalé à l'usage** — « le texte + ajouter un rappel
   semble disproportionné ». À `--texte-courant` il pesait autant que le rappel posé au-dessus, donc
   autant que la donnée ; c'est une invitation, pas un fait. Le palier employé est celui des
   mentions, avec la couleur qui va avec — le couple que le projet emploie partout pour ce qui
   accompagne sans réclamer l'attention. */
/* **Une carte repliée ne montre que son intitulé** (06/09/2026) : photos, trace et Tricount se
   replient tant qu'ils sont vides, sur le détail d'une sortie libre. Le raisonnement et le choix
   des trois vivent dans `reglerCartesRepliables` (`sorties-pas-du-mardi.js`).

   **Le sélecteur épargne ce qui porte `aria-expanded`**, c'est-à-dire l'intitulé — qui est aussi
   la bascule. Viser l'attribut plutôt qu'une classe suit l'idiome du projet : c'est l'état annoncé
   aux lecteurs d'écran qui décide de ce qu'on voit, il ne peut donc pas mentir. Même raisonnement
   que le formulaire de proposition, dont le `hidden` se lit sur son `aria-expanded`.

   **`display: none` et pas seulement un masquage visuel** : les enfants sortent de l'arbre, donc
   ni cliquables ni atteignables au clavier — c'est la même exigence que le mode visiteur. Poser
   plutôt une hauteur nulle sur la carte aurait emporté l'intitulé avec le reste. */
#vue-detail .carte-repliee > *:not([aria-expanded]) {
  display: none;
}

/* Le chevron dit dans quel sens ça va, et il pivote avec l'état — jamais avec une classe qu'il
   faudrait tenir d'accord avec lui. `ti-chevron-down` est déjà dans le jeu embarqué, et c'est
   celui que « Voir le détail » emploie : aucune icône de plus à extraire. */
.chevron-repli {
  margin-left: var(--espace-minuscule);
  transition: transform 0.2s ease;
}

[aria-expanded="true"] > .chevron-repli {
  transform: rotate(180deg);
}

/* L'intitulé devient une commande : le curseur le dit, comme la ligne d'hôte de l'historique. */
#vue-detail [aria-expanded] {
  cursor: pointer;
}

/* **Les deux gestes « ajouter une entrée » de l'écran partagent leur déclaration** (06/09/2026).
   Mesurés avant : le « + » d'un invité en `--texte-fort` sombre (13,6 px, `rgb(19,50,79)`), celui
   d'un rappel en `--texte-etiquette` discret (12,8 px, `rgb(79,99,119)`) — deux tailles et deux
   couleurs pour le même geste, à deux cartes d'écart.

   **Les formes restent différentes, et c'est justifié** : le rond compact se pose à côté d'un
   intitulé, le bouton verbeux sous une liste. Seuls la couleur et le palier convergent.

   **Ce qui n'a PAS été fait, et pourquoi** : les faire converger jusqu'à la forme demanderait soit
   de rendre visible l'`aria-label` « Ajouter un invité » — du texte nouveau à l'écran, qui se fait
   valider —, soit de toucher `rappel.js`, partagé par les deux popups du calendrier et le
   formulaire de proposition, où il n'y a pas d'intitulé à côté duquel poser un rond.

   **Les deux zones de dépôt de fichier ne sont pas de cette famille**, et la mesure a corrigé le
   diagnostic de départ : `.tuile-ajout-photo` et `.depot-trace` sont déjà d'accord — même
   pointillé, même rayon de 8 px, même couleur — et seule leur forme suit celle de leur contenu.
   Déposer un fichier n'est pas ajouter une ligne : deux gestes, deux familles. */
.zone-rappel .ajouter-rappel,
.bouton-icone-invite {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.zone-rappel .ajouter-rappel {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  padding: var(--espace-minuscule) 0;
}

.champ-rappel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-petit);
  margin: var(--espace-minuscule) 0 var(--espace-moyen);
}


/* Le `display` ci-dessus l'emporterait sur le `[hidden]` du navigateur, à spécificité égale.
   Celui-ci est vraiment masqué — la carte du détail l'est sur une sortie passée. */
.champ-rappel[hidden] {
  display: none;
}

.option-suppression-donnees {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  font-size: var(--texte-courant);
}

/* Flex plutôt que `block` avec une espace typographique entre l'icône et le
   texte : c'est ce qui donne le même écart que `.choix-reglage-etiquette`, donc
   des libellés qui démarrent tous à la même abscisse. */
.lien-menu {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  padding: var(--espace-petit);
  border-radius: 8px;
  color: var(--couleur-texte);
  text-decoration: none;
}

/* **Le réglage des notifications se colle en bas, mais reste AU-DESSUS du trait**
   (19/08/2026, deuxième tour). Le trait ne dit qu'une chose depuis toujours : « en dessous,
   le réglage du thème ». Passer les notifications dessous lui aurait fait annoncer deux
   choses sans rapport ; il reste donc sur `#item-theme`, et c'est la marge automatique — pas
   le trait — qui déménage ici.

   **Le `padding-top` n'est pas décoratif** : il est le seul écart qui subsiste quand la marge
   automatique tombe à zéro, faute d'espace libre à absorber — un téléphone en paysage, où le
   contenu remplit déjà le panneau. Sans lui, le bouton toucherait « Statistiques » et se
   lirait comme une sixième destination.

   **Un essai à deux choix a vécu une heure le 23/08/2026** — même forme que le thème, une
   ligne par état — et il a été écarté : deux lignes pour un booléen, dans un menu qui en
   compte déjà dix, ça ne valait pas le gain. Ce qui reste de l'essai est ce qui compte, et il
   est dans le JS : **le libellé dit l'état** (« Notifications activées ») et **il n'est écrit
   qu'une fois l'état lu**. Ce bloc-ci n'a jamais changé — il vise l'item, pas son contenu. */
#item-notifications:not([hidden]) {
  margin-top: auto;
  padding-top: var(--espace-petit);
}

/* **Deux `margin-top: auto` dans une colonne flex se partagent l'espace libre** — le bouton
   se poserait au milieu du panneau, et le thème en bas. Le thème rend donc la sienne dès que
   les notifications sont là, et la **reprend** dès qu'elles ne le sont pas : masquées
   (navigateur sans push, mode visiteur). **Depuis le 20/08/2026 il ne reste que ces deux cas** :
   les neuf pages qui portent un menu portent toutes l'élément, et les six autres n'ont pas
   de menu du tout. Sans ce `:has()`, le bas du menu se disloquerait sur un iPhone Safari.

   `:has()` est moins exigeant que le `color-mix()` déjà employé plus bas (Chrome 105 contre
   111) : il n'élargit donc pas le socle de navigateurs demandé par la feuille. */
#panneau-menu ul:has(#item-notifications:not([hidden])) #item-theme {
  margin-top: 0;
}

/* Sépare les pages du choix de thème, qui est un réglage et non une
   destination. Posé sur l'élément qui *suit*, comme les autres séparateurs du
   projet (voir Carte de sortie). */
/* `margin-top: auto` pousse le bloc en bas du panneau en absorbant tout
   l'espace libre. Si le contenu remplit déjà l'écran, la marge vaut zéro et
   l'agencement retombe naturellement à la suite des liens — aucun cas
   particulier à traiter. */
#item-theme {
  margin-top: auto;
  padding-top: var(--espace-petit);
  border-top: 1px solid var(--couleur-bordure);
}

/* **L'entrée du compte partage cet état depuis le 06/09/2026**, et elle ne le recopie pas — deux
   éléments qui doivent se ressembler partagent leur déclaration (voir Conventions dans CLAUDE.md).
   **C'était une classe morte** : `compte.html` posait bien `.actif` sur son lien, mais la feuille
   ne stylait que `.lien-menu.actif`, et ce lien-là porte `.pseudo-courant`. Le menu ne disait donc
   jamais qu'on était déjà sur sa propre page, alors que le balisage l'annonçait depuis le
   20/08/2026. */
.lien-menu.actif,
.pseudo-courant.actif {
  background-color: var(--couleur-surface-teintee);
}

/* **Le bouton des notifications porte `.lien-menu`, il ne la recopie pas.** Il déclenche une
   action au lieu de mener quelque part, donc c'est un `<button>` — mais il vit dans la même
   liste que les liens et doit s'y fondre exactement. Ce bloc n'annule que les valeurs par
   défaut du navigateur pour un bouton ; l'espacement, l'arrondi et la couleur viennent de la
   déclaration partagée. Deux éléments qui doivent se ressembler partagent leur déclaration
   (voir Conventions dans CLAUDE.md) : une seconde classe jumelle aurait dérivé au premier
   ajustement, comme `.etiquette-bouton` en son temps. */
button.lien-menu {
  width: 100%;
  border: none;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* `.lien-menu-desactive` retirée le 17/08/2026 (`A23`) : aucune page ni aucun module ne la
   citait, contrôle fait sur les 247 classes déclarées en tête de règle. */

/* **Une rangée de réglage, et le nom dit « réglage » et non « thème » depuis le
   23/08/2026.** Elle s'appelait `.choix-theme` du temps où le thème était le seul réglage du
   menu ; les notifications ont pris la même forme — deux choix et une pastille au lieu d'un
   bouton qui affichait un verbe —, et une classe nommée d'après le thème aurait alors habillé
   des notifications. Deux réglages qui doivent se ressembler **partagent** leur déclaration
   (voir Conventions dans CLAUDE.md) : la recopier sous un second nom l'aurait fait dériver au
   premier ajustement, comme `.etiquette-bouton` en son temps. */
.choix-reglage {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  padding: var(--espace-petit);
  border-radius: 8px;
  cursor: pointer;
}

/* Plus discret que `.lien-menu` : un réglage n'est pas une destination.
   L'alignement tient malgré la différence de taille grâce à la règle ci-dessous
   qui fige la boîte des icônes. */
.choix-reglage-etiquette {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  font-size: var(--texte-courant);
}

/* `.ti` dimensionne les icônes en `em`, donc un libellé plus petit donnerait
   une icône plus étroite et décalerait la colonne de texte. En `rem`, la boîte
   ne dépend plus de la taille du libellé : les deux blocs du menu gardent la
   même gouttière alors que leurs textes diffèrent. */
.lien-menu .ti,
.choix-reglage-etiquette .ti {
  width: 1rem;
  height: 1rem;
}

/* Aligné sur le texte des libellés (padding de .choix-reglage) plutôt que sur le
   bord du panneau : la ligne se lit comme une note du groupe de choix. */
.indice-theme {
  margin: var(--espace-minuscule) 0 0;
  padding: 0 var(--espace-petit);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.choix-reglage input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* `order` place la pastille en fin de ligne sans toucher au HTML — l'ordre du
   DOM doit rester `input` puis pastille, dont dépend le sélecteur
   `input:checked + .choix-reglage-pastille` qui coche sans une ligne de JS. */
.choix-reglage-pastille {
  order: 1;
  position: relative;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  border: 2px solid var(--couleur-texte-discret);
  border-radius: 50%;
  transition: border-color 0.2s ease;
}

.choix-reglage input:checked + .choix-reglage-pastille {
  border-color: var(--couleur-accent);
}

.choix-reglage input:checked + .choix-reglage-pastille::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background-color: var(--couleur-accent);
}

/* Le radio natif est masqué : sans ça, le focus clavier serait invisible. */
.choix-reglage input:focus-visible + .choix-reglage-pastille {
  outline: 2px solid var(--couleur-accent);
  outline-offset: 2px;
}

/* `.etiquette-bouton` a longtemps partagé cette déclaration : c'était un <label>
   qui devait *imiter* un bouton, et les deux règles écrites séparément avaient
   dérivé — 0.9rem contre 1rem, d'où un « Ajouter des photos » visiblement plus
   petit que le « Publier » d'à côté. La classe a disparu avec le bouton, remplacé
   par une tuile « + » dans la grille (voir `.tuile-ajout-photo`), mais **la leçon
   vaut toujours** et s'applique désormais entre cette tuile et les vignettes :
   deux éléments qui doivent se ressembler partagent leur déclaration.
   `font-family: inherit` est indispensable ici : les contrôles de formulaire
   n'héritent **pas** de la police du document, ils prennent celle du navigateur.
   Sans cette ligne, les libellés de boutons ne sont pas dans la même police que le
   texte qui les entoure — invisible tant que les deux se ressemblent, faux quand
   même.

   `0.9rem` et non `1rem` : c'est la taille la plus fréquente de la feuille et celle
   du texte d'un commentaire, donc celle du contenu qui entoure les boutons. À 1rem
   ils étaient le plus gros texte de leur carte, alors que les titres de zone sont à
   0.75rem — un bouton pesait plus lourd que le contenu qu'il sert.
   **Ne pas « harmoniser » les `textarea` là-dessus** : ils restent à 1rem, et c'est
   délibéré. En dessous de 16px, iOS Safari zoome automatiquement à la mise au point
   d'un champ de saisie. Un bouton n'a pas cette contrainte, un champ oui — les deux
   n'ont donc aucune raison de partager leur taille. */
button {
  background-color: transparent;
  border: 1.5px solid var(--couleur-accent);
  color: var(--couleur-texte);
  border-radius: 8px;
  padding: var(--espace-petit) var(--espace-grand);
  font-family: inherit;
  font-size: var(--texte-courant);
  cursor: pointer;
}

button:hover {
  background-color: var(--couleur-surface-teintee);
}

/* **La frontière du bouton plein est son remplissage**, puisqu'il n'a pas de bordure — et sur
   `clair-orange` ce remplissage est à **1,90:1** contre une carte, **2,14:1** contre la page.
   C'est le seuil de WCAG 1.4.11 raté, sur le geste le plus fréquent de l'application : les
   trois boutons du sondage.

   **La même règle avait déjà été appliquée au thème bleu le 15/08/2026** — son accent a été
   assombri parce qu'à 2,93:1 « le bord des boutons pleins était mou ». Elle ne l'avait pas été
   au thème orange, et c'est cette asymétrie que le 22/08/2026 corrige.

   **Un `outline` à décalage négatif, et ni une bordure ni une ombre interne** : le projet n'a
   pas de `box-sizing` global, donc une bordure de 1,5 px grossirait chaque bouton de 3 px et
   déplacerait la grille du sondage ; et une ombre `inset` se faisait **écraser** par
   `.boutons-sondage button`, qui redéclare `box-shadow` pour en donner une aux deux boutons
   secondaires — mesuré, le liseré n'apparaissait pas là où il comptait le plus. `outline` n'est
   disputé par aucune règle du projet sur ces boutons et ne touche à aucune géométrie. Il cède
   la place à l'anneau de mise au point quand le bouton est focalisé, ce qui est l'ordre de
   priorité voulu. **Et il disparaît de lui-même sur les deux autres thèmes**, où
   `--couleur-accent-forme` vaut l'accent : le liseré y est de la couleur du fond, donc
   invisible. Une seule déclaration, un seul thème concerné, aucune valeur en dur.

   Le remplissage et le libellé ne bougent pas : le libellé est à 7,83:1 sur l'orange, il n'a
   jamais été en cause. */
.bouton-principal {
  background-color: var(--couleur-accent);
  color: var(--couleur-texte-bouton-principal, white);
  border: none;
  border-radius: 20px;
  box-shadow: 0 3px 8px var(--couleur-ombre-bouton);
  outline: 1.5px solid var(--couleur-accent-forme);
  outline-offset: -1.5px;
  padding-top: var(--espace-moyen);
  padding-bottom: var(--espace-moyen);
}

.bouton-secondaire {
  background-color: var(--couleur-surface-neutre);
  border: none;
  border-radius: 20px;
}

.bouton-danger {
  background-color: var(--couleur-danger);
  color: var(--couleur-texte-bouton-danger, white);
  border: none;
  border-radius: 20px;
  box-shadow: 0 3px 8px var(--couleur-ombre-bouton);
}

.bouton-principal:hover {
  background-color: var(--couleur-accent);
  opacity: 0.85;
}

/* **La moitié manquante de la paire ci-dessus** (20/08/2026). `button:hover` peint
   `--couleur-surface-teintee` et sa spécificité (0,1,1) bat celle de `.bouton-secondaire`
   (0,1,0) : au survol, un bouton secondaire prenait donc la couleur des cartes — et sur une
   carte, qui *est* de cette couleur, il disparaissait purement et simplement. Le bouton
   principal était protégé par la règle juste au-dessus, le secondaire ne l'a jamais été.

   **Vu au navigateur en construisant la page compte**, jamais avant : le projet est pensé
   pour le téléphone, où il n'y a pas de survol. Onze boutons secondaires de l'écran principal
   étaient dans ce cas. Même traitement que le principal — on réaffirme la couleur de repos et
   on marque le survol par l'opacité, qui ne peut pas se confondre avec un fond. */
.bouton-secondaire:hover {
  background-color: var(--couleur-surface-neutre);
  opacity: 0.85;
}

:root[data-theme="sombre"] .bouton-principal {
  box-shadow: 0 3px 8px var(--couleur-ombre-bouton), 0 0 10px 2px rgba(47, 217, 224, 0.7), 0 0 22px 4px rgba(255, 107, 157, 0.35);
}

.carte-sortie {
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-top: none;
  border-radius: 0 0 14px 14px;
  padding: var(--espace-grand) 1.25rem;
  margin: 0 0 var(--espace-grand);
}

.carte-sortie p {
  margin: var(--espace-minuscule) 0;
}

.conteneur-sortie {
  border-radius: 8px 8px 14px 14px;
}

:root[data-theme="sombre"] .conteneur-sortie {
  filter: drop-shadow(0 0 5px rgba(47, 217, 224, 0.7)) drop-shadow(0 0 11px rgba(255, 107, 157, 0.4));
}

:root[data-theme="clair"] .conteneur-sortie {
  filter: drop-shadow(0 4px 10px var(--couleur-ombre-conteneur));
}

.onglets-sorties {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-bottom: 1px solid var(--couleur-bordure);
  margin: var(--espace-grand) 0 0;
}

.onglet-sortie {
  border: none;
  border-radius: 8px 8px 0 0;
  padding: var(--espace-petit) var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  background-color: transparent;
  color: var(--couleur-texte-discret);
}

.onglet-sortie.sans-hote::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 4px;
  border-radius: 50%;
  background-color: var(--couleur-accent-secondaire);
  vertical-align: middle;
}

.onglet-sortie.actif {
  position: relative;
  z-index: 1;
  border: 1px solid var(--couleur-bordure);
  border-bottom: none;
  margin-bottom: -1px;
  background-color: var(--couleur-surface-teintee);
  color: var(--couleur-texte);
}

/* **La gouttière n'est pas décorative** : `justify-content: space-between` ne sépare les
   deux blocs que s'il reste de la place. Avec un libellé long — « Finalement non... » — le
   bouton grandit jusqu'à toucher « C'est chez qui ? », constaté sur un téléphone réel. Le
   `gap` est le minimum qu'aucun contenu ne peut manger, et `min-width: 0` autorise le bloc
   de gauche à se replier plutôt qu'à pousser. */
.ligne-hote {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--espace-moyen);
  margin-bottom: var(--espace-grand);
}

.ligne-hote .bloc-hote {
  min-width: 0;
}

/* **La gouttière ne suffisait pas, constaté sur un vrai téléphone** : avec « Finalement
   non... », il ne reste pas assez de place pour les deux, et chacun se cassait sur deux
   lignes — l'intitulé comme le bouton. Deux règles qui vont ensemble : plus personne ne se
   coupe, et la ligne se replie en deux rangées quand elle déborde. Le bouton passe alors
   sous l'intitulé, ce qui se lit infiniment mieux que deux blocs déchiquetés côte à côte. */
.ligne-hote {
  flex-wrap: wrap;
}

.label-hote,
/* **Le pseudo de l'hôte est le sujet de l'écran, pas une emphase parmi d'autres.** Il
   partage sa déclaration avec `.chez-ancienne` et `.auteur-commentaire` — famille, italique,
   graisse, couleur — mais **diverge sur la seule taille**, et c'est délibéré : une signature
   de commentaire est une métadonnée, celui-ci est ce qu'on vient lire.

   **Ce que la mesure a montré, et qui n'était pas l'intuition** : le pseudo était déjà plus
   grand que son intitulé (16 px contre 14,4) et plus gras (600 contre 400). Il paraissait
   pourtant plus faible, parce qu'une **condensée italique** à 16 px pèse visuellement moins
   qu'une system-ui à 14,4. Le nominal ne suffisait donc pas : il fallait un vrai palier. */
#hote-actuel {
  font-size: var(--titre-section);
}

/* **L'état vide ne porte pas la couleur de mise en avant.** « Personne » en framboise se lit
   comme une alerte, alors que c'est une case encore libre — et le ton du projet interdit de
   faire d'une absence un reproche (voir Contexte dans CLAUDE.md). */
#hote-actuel.sans-hote {
  color: var(--couleur-texte-discret);
}

#bouton-je-recois {
  white-space: nowrap;
}

.bloc-hote {
  display: flex;
  align-items: center;
  gap: 8px;
}

.icone-hote {
  color: var(--couleur-texte);
  font-size: var(--icone-grande);
}

.info-hote {
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* **Intitulé principal**, un cran au-dessus des autres : « C'est chez qui ? » est la question
   que la page existe pour poser, et la traiter comme « Invités » ou « Commentaires » l'a
   noyée dans le décor.
   **La couleur reste discrète**, comme pour tout intitulé — c'est la taille seule qui le
   distingue, sinon on retombe sur la combinaison « 0,9 plein » qui rendait les étiquettes
   plus lourdes que les données.
   **Limite à tenir : un seul par écran.** Un second intitulé principal ramènerait tous les
   intitulés au même plan, ce qui est exactement le désordre d'origine. */
.label-hote {
  font-size: var(--texte-courant);
  color: var(--couleur-texte-discret);
}

/* **Mise en avant** au sens de la règle des trois rôles : ici l'information *est* le sujet
   de la carte. La taille est posée explicitement — elle valait déjà 1 rem, mais par héritage
   du `body`, ce qui la rendait invisible à la lecture de la feuille et donc impossible à
   raisonner.
   **L'italique n'est pas un choix de style, c'est une contrainte de la police** : seules
   deux graisses de Barlow Condensed sont embarquées, toutes deux italiques (voir
   Typographie). Tout ce qui prend `--police-affichage` est donc italique par construction —
   le titre de la page, les compteurs du sondage, l'auteur d'un commentaire et ce pseudo.

   **Qui**, partout où c'est écrit : le pseudo de l'hôte sur la carte du mardi, le « chez X »
   de l'historique, et l'auteur d'un commentaire. Les trois portaient **trois déclarations
   rigoureusement identiques** dans trois sections du fichier, jusqu'au 15/08/2026 — le cas
   exact que la convention traque : deux éléments qui doivent se ressembler partagent leur
   déclaration au lieu de la recopier, sans quoi ils divergent.

   **La couleur est celle du second accent, et c'est un changement de fond.** Elle valait
   `--couleur-accent-texte`, c'est-à-dire la même teinte que les boutons dans les deux thèmes
   clairs : un pseudo bleu au milieu d'une interface bleue ne ressort pas. Le thème sombre,
   lui, avait déjà deux teintes — cyan pour l'action, rose pour l'identité — et c'est ce qui
   le rend lisible d'un coup d'œil. Les trois thèmes ont désormais ce couple : bleu/magenta,
   orange/bleu-vert, cyan/rose. Contrastes mesurés sur la surface de carte : 4,91:1, 4,97:1 et
   5,56:1, tous au-dessus du seuil AA. */
.pseudo-hote,
.chez-ancienne,
.auteur-commentaire,
/* Le pseudo du membre courant dans le menu : c'est le nom d'un membre du groupe, comme les
   trois au-dessus. */
.pseudo-courant span {
  font-family: var(--police-affichage);
  font-style: italic;
  font-weight: 600;
  font-size: var(--texte-fort);
  color: var(--couleur-accent-secondaire-texte);
}

#bouton-je-recois {
  padding: var(--espace-petit) var(--espace-bloc);
  font-size: var(--texte-courant);
}

/* **Le mot est collé à l'hôte, il n'est plus du contexte** (20/08/2026, second tour du
   même jour). Il vivait sous les boutons du sondage, groupé avec les invités par un
   espacement serré — les deux étant « des choses de l'hôte » face à la météo, « une chose
   du jour ». Ce classement tenait sur le papier et ratait ce que le mot **est** : non pas
   une information de plus sur la soirée, mais **son thème**, écrit par celui qui reçoit.
   Il est donc remonté dans le DOM contre la ligne d'hôte, et l'entorse à l'ordre décidé le
   15/08 est justifiée et chiffrée dans `index.html`.

   **Serré au-dessus, large en dessous**, et c'est là que le groupe se dit : la ligne d'hôte
   resserre sa marge basse à 0,5 rem quand le mot la suit (voir `.ligne-hote:has(...)` plus
   haut), tandis que 1,25 rem détache le mot des compteurs qui ouvrent la décision. Sans ce
   contraste, un mot posé à 1 rem de l'hôte et 1 rem des compteurs n'appartiendrait à aucun
   des deux — le défaut exact que ce tour corrige (voir les Conventions de CLAUDE.md). */
#mot-hote-sortie {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--espace-petit);
  /* `--espace-grand` depuis le 22/08/2026 (`D8`) : 1,25 rem rendait 20 px, hors échelle. */
  margin: 0 0 var(--espace-grand);
}

/* **La ligne d'hôte se resserre quand le mot la suit**, et seulement là. Écrit avec `:has()`
   plutôt qu'avec une marge négative sur le mot : le sélecteur dit ce qui se passe, la marge
   négative aurait demandé un commentaire pour être comprise. Le `:not([hidden])` compte —
   la zone du mot est masquée dès qu'il n'y a pas d'hôte, ou que l'hôte n'a rien écrit et que
   ce n'est pas lui qui regarde. */
.ligne-hote:has(+ #mot-hote-sortie:not([hidden])) {
  margin-bottom: var(--espace-petit);
}

#mot-hote-sortie[hidden] {
  display: none;
}

#mot-hote-sortie[role="button"] {
  cursor: pointer;
}

/* **Le mot est la seule chose que l'hôte ait écrite dans cette zone**, il prend donc le
   palier du contenu (20/08/2026) là où il était au même niveau que la météo et les invités.
   La zone de contexte se lit désormais sur **deux plans** : ce que l'hôte a écrit, et le
   reste. Vaut aussi pour la carte d'une sortie pas du mardi, qui partage cette classe et où
   le mot est également le seul texte rédigé.

   **L'invitation à écrire ne suit pas** : `.mot-hote-manquant`, déclarée juste après, garde
   sa taille d'étiquette pour la raison écrite là-bas — une invitation qui pèse plus lourd
   que la donnée écrase la hiérarchie. */
.texte-mot-hote {
  margin: 0;
  text-align: left;
  font-size: var(--texte-fort);
}

/* **Le mot de l'hôte se lit comme une citation de lui**, sur les deux écrans qui l'affichent
   (20/08/2026). Deux choses le disent ici, et chacune le rattache à **lui** plutôt qu'à la
   carte :

   - la **police d'affichage en italique**, celle qui écrit son pseudo juste au-dessus et qui
     ne sert à rien d'autre autour — le mot se lit donc dans sa voix ;
   - la **graisse 600**, la sienne aussi. Elle n'est pas un ornement : en 400, le condensé
     italique pesait moins que les compteurs qui le suivent — mesuré à l'écran, pas supposé.
     Sur un condensé, c'est le poids plus que la taille qui fait ressortir.

   **La taille, elle, n'est pas partagée, et c'est le point le plus fin** : le mot pèse
   exactement ce que pèse le pseudo de l'hôte **sur le même écran** — `--titre-section` face
   à `#hote-actuel` sur le mardi, `--texte-fort` face à `.chez-ancienne` sur l'historique. Le
   partager aurait fait dépasser le mot au-dessus du « chez Max » de l'historique, soit
   l'inverse exact de la règle voulue : le mot est sa voix, pas son remplaçant. Cet écart de
   taille entre les deux écrans est d'ailleurs celui que `.pseudo-hote` et `.chez-ancienne`
   portent déjà, pour la raison écrite là-bas.

   **Cette déclaration porte donc la voix, pas la mise en page**, et c'est le découpage qui
   compte : ce qui ne doit jamais diverger est partagé, ce qui diffère légitimement est posé
   à côté. Sur le mardi, le mot est un filet vertical aligné à gauche, contre une ligne
   d'hôte elle-même alignée à gauche dans sa carte ; sur l'historique, il est centré sous un
   en-tête centré, et un filet à gauche l'y désolidariserait de « chez Max » — essayé, vu,
   écarté. Les Conventions demandent de partager la déclaration de deux éléments qui doivent
   se ressembler ; elles ne demandent pas de leur imposer la même mise en page.

   **`:not(.mot-hote-manquant)` n'est pas une précaution, c'est le cœur de la règle** : le
   premier sélecteur porte un id, il l'emporterait donc sur `.mot-hote-manquant` et ferait
   grossir l'invitation « C'est quoi le projet ? » jusqu'au palier de titre, filet compris —
   soit exactement la hiérarchie écrasée que la déclaration d'à côté avait corrigée le matin
   même. Une invitation à écrire ne pèse pas le poids de ce qui est écrit.

   **Non partagé avec la carte d'une sortie pas du mardi ni avec la description d'un voyage**,
   qui portent pourtant `.texte-mot-hote` : là il n'y a **pas d'hôte**, donc personne à qui
   rattacher une voix, et ce texte y est le corps de la carte. C'est la raison qu'il faut
   avoir lue avant de les « harmoniser ». */
/* **Sur l'historique, la voix vise le paragraphe et non plus la zone** (13/09/2026) : la zone accueille
   désormais l'invitation et l'édition du mot d'une sortie libre passée, et posée sur `#detail-mot-hote`
   la voix les attrapait — police d'affichage dans un champ et deux boutons, mesuré contre la carte du
   mardi. Même forme sur les deux écrans, donc. */
#mot-hote-sortie .texte-mot-hote:not(.mot-hote-manquant),
#detail-mot-hote .texte-mot-hote:not(.mot-hote-manquant) {
  font-family: var(--police-affichage);
  font-style: italic;
  font-weight: 600;
  line-height: 1.3;
}

/* Le filet d'identité, propre au mardi : la teinte est celle du second accent, réservée à
   l'identité dans les trois thèmes (voir `docs/themes-palette.md`), donc exactement celle du
   pseudo de l'hôte deux lignes plus haut. */
#mot-hote-sortie .texte-mot-hote:not(.mot-hote-manquant) {
  font-size: var(--titre-section);
  padding-left: var(--espace-moyen);
  border-left: 4px solid var(--couleur-accent-secondaire-texte);
}

/* **Ce champ est revenu à 1 rem le 28/08/2026** (`RC-UX-045`), et la règle qui l'en faisait
   descendre a disparu. Elle posait `--texte-courant`, soit 0,9 rem — **14,4 px mesurés à la
   sonde**, éditeur déplié —, et son commentaire était l'aveu du défaut : il annonçait « suit la
   règle générique input/textarea (1rem) » dans la phrase même où il la défaisait, avec un motif
   **visuel** (« il paraissait plus gros à côté ») et un « à ajuster si besoin ».

   C'est exactement le motif que l'instruction posée 385 lignes plus haut anticipe et refuse :
   en dessous de 16 px, **iOS Safari agrandit la page à la mise au point et ne la réduit pas au
   retrait du focus**. Or c'est le champ par lequel l'hôte écrit « 9 h au parking » depuis son
   téléphone, sur l'écran principal — la page restait zoomée pour la suite de la visite.

   **La gêne visuelle était réelle, et la comparaison qui l'a produite était la mauvaise** :
   l'ancien commentaire mesurait ce champ contre le **champ invités**, un `<input>` qui garde la
   police par défaut du navigateur (Arial ici) — plus étroite que le `system-ui` hérité du
   `body`, d'où l'impression de « plus gros ». Le bon voisin est `#champ-commentaire`, le
   `textarea` de la carte d'en dessous : **16 px, `system-ui`**, mesuré. Ce champ y est
   désormais identique, et il n'y a **aucune déclaration à écrire pour ça** — les deux héritent.

   **Deux essais écartés en chemin, l'un par la mesure** : `font-family: initial` rend du
   **Times New Roman**, l'initial CSS d'une police étant `serif` et non le défaut des contrôles
   de formulaire ; et recopier `system-ui` ici en aurait fait une seconde déclaration à tenir
   d'accord avec celle du `body`, ce que les Conventions écartent.

   **Aucune sonde ne pouvait voir le défaut d'origine** : `#mot-hote-sortie` porte `hidden`
   jusqu'à ce que l'hôte ouvre l'éditeur, et le `textarea` n'est même **construit** qu'à ce
   moment-là par `sortie.js` — donc rien à mesurer quand `champsSousSeize()` passe. C'est
   l'angle mort que `outils/controles-ecran.js` documente lui-même. */

/* **État vide**, donc traité comme tous les autres : `--texte-etiquette`, couleur discrète,
   italique système. Il héritait du 1 rem du `body`, ce qui en faisait le texte **le plus
   gros de la carte** — une invitation à écrire pesait plus lourd que le pseudo de l'hôte et
   que la météo. C'est ce qui écrasait la hiérarchie de cet écran. */
/* **L'état vide et l'invitation à écrire partagent leur déclaration** (28/08/2026,
   `RC-CODE-046`) : les deux disent « il n'y a rien ici, à toi de jouer », dans le même registre
   et souvent sur le même écran. C'est la variante en italique du rôle défini plus haut. */
.mot-hote-manquant,
.message-vide {
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

/* **Le crayon qui dit « cette zone s'édite ».** Nom générique depuis le 20/08/2026 : il
   servait au seul mot de l'hôte, il sert aussi à la ligne d'hôte d'une ancienne sortie. Une
   seconde classe jumelle aurait dérivé au premier ajustement (voir les Conventions de
   CLAUDE.md). */
.icone-modifier {
  margin-left: var(--espace-petit);
  font-size: var(--icone-petite);
  color: var(--couleur-texte-discret);
}

.boutons-mot-hote {
  display: flex;
  gap: var(--espace-petit);
}

#invites-sortie {
  text-align: left;
  /* `--espace-grand` depuis le 22/08/2026 (`D8`) : 1,25 rem rendait 20 px, hors échelle. */
  margin-bottom: var(--espace-grand);
}

#invites-sortie[hidden] {
  display: none;
}

.entete-invites {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-petit);
}

.label-invites {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  margin: 0;
}

/* **Le libellé devient l'état vide quand il n'y a aucun invité** (20/08/2026), et il change
   de rôle en même temps que de texte — donc de taille. **C'est l'échelle qui le dit**, pas un
   réglage à l'œil : `--texte-etiquette` est pour les « libellés de zone, notes, mentions
   discrètes », tandis que les **états en italique** (« annulée », « n'a pas répondu ») gardent
   `--texte-courant`. « Invités » est un libellé ; « Pas d'invité pour l'instant » est un état.

   Effet de bord recherché : la zone de contexte n'a plus que **deux plans de lecture** au lieu
   de trois — ce que l'hôte a écrit (1 rem), et le reste (0,9 rem, comme la météo juste en
   dessous). L'italique distingue l'état de la donnée, comme partout ailleurs. */
.label-invites.sans-invite {
  font-style: italic;
  font-size: var(--texte-courant);
}

.compteur-invites-attente {
  background-color: var(--couleur-accent);
  color: var(--couleur-texte-bouton-principal, white);
  border-radius: 20px;
  padding: var(--espace-minime) var(--espace-petit);
  font-size: var(--texte-etiquette);
  font-weight: 600;
}

.compteur-invites-attente[hidden] {
  display: none;
}

/* **24 px et pas 22, parce que c'est le plancher de WCAG 2.2** (SC 2.5.8), que ce bouton
   ratait de deux pixels — mesuré le 17/08/2026, en même temps que les croix de retrait.
   Contrairement à elles, la cible **est** le rond qu'on voit : ici on ne gonfle donc pas
   une surface invisible par du padding, on aligne la taille visible sur la taille
   atteignable. C'est ce qui vaut pour tout bouton dont la forme est dessinée. */
.bouton-icone-invite {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border-radius: 50%;
  font-size: var(--icone-petite);
  line-height: 1;
}

/* Même garde que `.liens-voyage` et `.carte-stats` : `display: flex` l'emporte sur le
   `[hidden] { display: none }` du navigateur, à spécificité égale. Ce bouton est masqué dès
   qu'il n'y a personne à proposer — sa hauteur tombait à zéro, donc rien ne se voyait, mais il
   **restait dans l'arbre** : atteignable au clavier et annoncé aux lecteurs d'écran. */
.bouton-icone-invite[hidden] {
  display: none;
}

#liste-invites,
.liste-invites {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-petit);
}

#liste-invites:not(:empty),
.liste-invites:not(:empty) {
  margin-bottom: var(--espace-petit);
}

/* **L'italique manquait ici, et c'est le seul état vide qui n'en avait pas** : « Pas
   d'invité pour l'instant » côtoyait « C'est quoi le projet ? » et « Pas de commentaire »,
   tous deux en italique, sans qu'aucune raison ne le justifie. Les trois disent la même
   chose — cette zone est vide — et se lisent donc pareil. */
.invites-vide {
  margin: 0 0 var(--espace-petit);
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

.invite-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  background-color: var(--couleur-surface-neutre);
  color: var(--couleur-accent-secondaire-texte);
  border-radius: 20px;
  padding: var(--espace-minuscule) var(--espace-moyen);
  font-size: var(--texte-courant);
}

.invite-en-attente {
  border: 1px dashed var(--couleur-accent-secondaire-texte);
  background-color: transparent;
}

.invite-statut-attente {
  font-style: italic;
  color: var(--couleur-texte-discret);
  font-size: var(--texte-etiquette);
}

/* Boutons sans habillage : ni fond, ni bordure, le glyphe seul. Les croix de
   retrait d'invité et le crayon de correction partagent cette déclaration au lieu
   de la recopier — c'est le même objet à deux endroits (voir la convention dans
   CLAUDE.md). Ne reste propre à chacun que sa taille et sa surface tactile. */
.action-invite,
.bouton-nu {
  background: none;
  border: none;
  line-height: 1;
  cursor: pointer;
}

/* **Le padding fait la surface tactile, exactement comme sur `.bouton-nu`** — c'est la
   moitié de la déclaration qui manquait ici. `padding: 0` réduisait la cible au glyphe :
   la croix « Retirer mon commentaire » mesurait **11 × 16 px** au navigateur, contre les
   24 × 24 minimum de WCAG 2.2. Mesuré le 17/08/2026 ; `CLAUDE.md` affirmait déjà que les
   deux classes sont « le même objet », c'est la feuille qui ne suivait pas.

   La marge négative de même valeur annule l'effet du padding sur la mise en page : la
   cible grandit, rien ne bouge à l'écran. Elle est posée sur les **quatre** côtés, et non
   sur le seul `margin-right` de `.bouton-nu` — ces croix vivent au bout d'une ligne, pas
   au bord d'une carte, il n'y a donc pas d'alignement optique à rattraper.

   **Et le padding seul ne suffisait toujours pas, pour une raison qui ne se devine pas** :
   le contenu de ces boutons est le **caractère** `×`, pas un glyphe du jeu d'icônes. Son
   avance ne mesure que 11 px à `--icone-texte`, là où `.bouton-nu` peint une icône de
   1,15 rem. Même padding, donc, mais 23,7 px de large au lieu de 31 — soit trois dixièmes
   de pixel sous le seuil, ce qui aurait rendu le correctif inutile en le croyant fait.
   D'où le plancher, écrit en `px` parce que c'est l'unité dans laquelle WCAG 2.2 énonce
   ses 24 × 24. La hauteur les dépasse déjà (28,8), le plancher la couvre quand même : une
   `line-height` retouchée un jour ne doit pas rouvrir le défaut en silence. */
.action-invite {
  min-width: 24px;
  min-height: 24px;
  padding: var(--espace-petit);
  /* **Exactement l opposé du padding ci-dessus**, et les deux bougent ensemble : la marge
     annule le padding pour que la cible tactile déborde sans décaler l icône. Un `calc` parce
     que CSS ne sait pas nier une variable. */
  margin: calc(-1 * var(--espace-petit));
  font-size: var(--icone-texte);
}

/* Le crayon qui ouvre la correction des présences. Il portait `.bouton-secondaire`,
   donc une bordure d'accent et un arrondi : un cadre coloré pour une action
   secondaire, posé à côté d'un intitulé gris — il pesait beaucoup plus que ce qu'il
   fait.
   **Le padding reste alors que le cadre disparaît** : il ne se voit plus, mais
   c'est lui qui fait la surface tactile. Sans lui, la cible au doigt se réduirait
   aux 18px du glyphe. La marge négative le rattrape optiquement, pour que le
   crayon s'aligne sur le bord du contenu et non son cadre invisible. */
.bouton-nu {
  padding: var(--espace-petit);
  margin-right: calc(-1 * var(--espace-petit));
  font-size: var(--icone-bouton);
  color: var(--couleur-texte-discret);
}

.retirer-invite {
  color: var(--couleur-texte-discret);
}

/* **`--couleur-accent-forme` et non l'accent brut** (28/08/2026, `RC-UX-055`). Ce « ✓ » est un
   glyphe sur la surface teintée d'un bouton, donc exactement le cas pour lequel cette variable
   a été créée le 21/08/2026 (`D3`) — et il était le **troisième** site du genre, oublié quand
   les deux autres ont été traités.

   Mesuré sur l'écran principal, fond effectif, avant puis après — un invité en attente fabriqué
   pour l'occasion, ce bouton n'existant pas autrement :

     sombre        #2FD9E0 sur #27272A   8,60:1  →  inchangé
     clair         #3F8ACB sur #FCFDFE   3,61:1  →  inchangé
     clair-orange  #FB923C sur #FCE8D6   1,90:1  →  #AC5004, 4,52:1

   **Le thème `clair` ne bouge pas, et c'est à dire franchement** : `--couleur-accent-forme` y
   vaut l'accent brut. 3,61:1 passe le seuil **3:1** qui régit un glyphe (WCAG 1.4.11) et non le
   **4,5:1** de la prose — et la sonde `contrastes()` le classera toujours en texte, ce « ✓ »
   étant un nœud de texte. C'est une limite connue de la sonde sur les glyphes, pas un défaut
   restant.

   **`--couleur-accent-texte` passerait partout** (5,56 / 6,58 / 4,52) et a été écartée après
   mesure : elle vaut **#FF6B9D en thème sombre**, donc un « ✓ » rose à côté d'un « × » gris,
   sur un thème dont l'accent est cyan. Corriger un contraste ne vaut pas de changer l'identité
   visuelle d'un bouton.

   **L'administration porte le même bouton et bénéficie du même changement**, mais je ne l'ai pas
   remesurée : son panneau n'affiche pas cet état sur le jeu d'écran disponible. L'audit y avait
   relevé 2,14:1 en orange.

   **Aucune revue d'écran ne pouvait le rencontrer** : ce bouton n'existe dans le DOM que sur un
   invité **en attente de validation**, état qui demande qu'un membre autre que l'hôte ait
   proposé quelqu'un sur un mardi à venir. La sonde était bonne, c'est l'état qui manquait.

   Le voisin immédiat était juste, ce qui rend l'oubli net : `.retirer-invite` prend
   `--couleur-texte-discret` et mesure 5,1 à 5,3:1 sur les trois thèmes, dans la même pastille. */
.valider-invite {
  color: var(--couleur-accent-forme);
}

/* La rangée « un nom + Ajouter » du panneau de correction partage cette
   déclaration : c'est le même objet à deux endroits, et deux règles séparées
   finiraient par diverger (voir la convention dans CLAUDE.md). Seul le formulaire
   de la carte du mardi se replie, d'où le `[hidden]` réservé à son id. */
#formulaire-ajout-invite,
.ligne-ajout-invite {
  display: flex;
  gap: var(--espace-petit);
  align-items: center;
}

#formulaire-ajout-invite[hidden] {
  display: none;
}

#formulaire-ajout-invite input {
  flex: 1;
  margin: 0;
}

/* **La carte se lit en deux temps, et le trait dit où passe la coupure** (20/08/2026).
   Au-dessus, ce qui sert à décider : l'hôte, **son mot**, les compteurs, les boutons du
   sondage. En dessous, ce qu'on lit une fois décidé : les invités, la météo.

   **Le mot a quitté cette chaîne le même jour**, en remontant contre l'hôte — et il a changé
   de camp par la même occasion. Ce n'est pas une perte pour la structure, c'est elle qui
   devient juste : « raclette ce soir » est précisément ce qui fait choisir « je mange »,
   donc le mot appartient à la décision et non au contexte (voir sa déclaration plus haut,
   et `index.html` pour le chiffrage du déplacement).

   **Le trait se pose sur le premier bloc de contexte réellement affiché** : les invités
   s'ils sont là, sinon la météo. D'où l'enchaînement `[hidden] + ...` — un « premier
   visible » que CSS ne sait pas dire autrement, mais que l'ordre fixe du DOM rend sûr ici.

   **À maintenir en même temps que le HTML**, et le déplacement du mot en est l'exemple
   vivant : déménager un bloc sans toucher à cette chaîne aurait laissé le trait accroché au
   mot, c'est-à-dire au milieu de la décision. */
#invites-sortie:not([hidden]),
#invites-sortie[hidden] + #meteo-sortie {
  border-top: 0.5px solid var(--couleur-bordure);
  padding-top: var(--espace-grand);
  /* `--espace-bloc` depuis le 22/08/2026 (`D8`) : 1,25 rem rendait 20 px, hors échelle, et
     c'est bien une rupture entre deux zones — la décision au-dessus, le contexte en dessous. */
  margin-top: var(--espace-bloc);
}

/* **Donnée**, donc `--texte-courant` — et c'est aussi ce que porte `.meteo-ancienne` sur la
   page historique. Les deux écrans affichaient la même donnée à deux tailles différentes,
   celle-ci héritant du 1 rem du `body` faute de règle. (Ce commentaire était séparé de sa
   règle par la chaîne ci-dessus, insérée entre les deux le matin même — remis à sa place.) */
/* **Son propre retrait vertical, et il ne dépend de rien** (22/08/2026, `D7`). Cette zone porte
   `role="button"` et déplie les prévisions, mais elle ne recevait de hauteur que par la chaîne
   `#invites-sortie[hidden] + #meteo-sortie` ci-dessus — c'est-à-dire **seulement quand elle est
   le premier bloc de contexte**. Dès qu'un invité est inscrit, le trait passe sur les invités,
   la chaîne ne s'applique plus et la cible tombait de **37 px à 20 px**, sous le minimum de
   24 x 24. Mesuré. Un dégagement tactile ne peut pas dépendre d'un sélecteur d'adjacence :
   celui-ci est donc porté par l'élément lui-même, et la chaîne continue de poser le sien
   par-dessus quand elle s'applique. */
#meteo-sortie {
  padding: var(--espace-minuscule) 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: var(--espace-minuscule) 0;
  font-size: var(--texte-courant);
  cursor: pointer;
}

.meteo-jour,
.meteo-coucher {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

#meteo-sortie.erreur {
  cursor: default;
}

.erreur {
  color: var(--couleur-texte-discret);
  font-size: var(--texte-courant);
}

/* Le défilement, le masque de bord et le `scroll-snap` viennent de la déclaration **partagée
   avec `.pastilles-annees`** (plus bas) : les deux rangées sont le même objet. Ne reste ici que
   ce qui est propre aux prévisions.

   **Ce qui a motivé le masque, et qui vaut d'être su** : 645 px de contenu pour 316 px
   visibles, et la troisième heure s'affichait coupée en plein milieu de son texte (« 19:0 »),
   ce qui se lit comme un défaut et non comme « ça continue ». */
.previsions-horaires {
  display: flex;
  gap: var(--espace-petit);
  margin-top: var(--espace-petit);
}

.previsions-horaires[hidden] {
  display: none;
}

.carte-heure {
  background-color: var(--couleur-surface-encastree);
  border-radius: 8px;
  padding: var(--espace-moyen) var(--espace-petit);
  text-align: center;
  min-width: 72px;
  flex-shrink: 0;
}

.carte-heure p {
  margin: var(--espace-minime) 0;
  font-size: var(--texte-courant);
}

.boutons-sondage {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin: var(--espace-bloc) 0;
}

.boutons-sondage button {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  border: none;
  border-radius: 20px;
  box-shadow: 0 3px 8px var(--couleur-ombre-bouton);
  padding: clamp(0.4rem, 1.5vw, 0.6rem) clamp(0.25rem, 1vw, 1rem);
  /* **`--texte-courant` et non `--texte-bouton-adaptatif`** : le clamp tombait à 0,75 rem
     sur un téléphone (2,8 vw valent 12 px à 430 px de large), soit **20 % de moins que
     « Finalement non… »** juste au-dessus, alors que ces trois boutons sont l'action
     principale de la page. Le clamp existe pour « Roulé et mangé », un libellé deux fois
     plus long qui vit sur `.boutons-correction` ; ici les libellés tiennent en 0,9 rem
     — « Je mange » occupe environ 56 px pour 82 px disponibles sur les 288 px les plus
     étroits du projet, et « Je roule / Je mange » passe déjà à la ligne par un `<br>`. */
  font-size: var(--texte-courant);
  overflow-wrap: break-word;
}

.boutons-sondage button:not(.bouton-principal) {
  background-color: var(--couleur-surface-neutre);
}

.boutons-sondage button:not(.bouton-principal):hover {
  background-color: var(--couleur-surface-teintee);
}

/* **Mise en avant** : c'est le résumé de la sortie, l'information qu'on cherche en ouvrant
   la page. Taille posée explicitement, comme `.pseudo-hote` et pour la même raison — elle
   valait déjà 1 rem, mais par héritage silencieux. */
#recap-sondage {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--espace-grand);
  font-family: var(--police-affichage);
  font-size: var(--texte-fort);
  font-style: italic;
  font-weight: 500;
  margin: var(--espace-bloc) 0 var(--espace-petit);
  cursor: pointer;
}

.compteur-item {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

/* Le récap sépare ses deux compteurs d'un `gap` large, qui vaut entre eux mais pas devant le
   chevron : celui-ci appartient à la ligne entière, pas à « 2 mangent ». On annule l'écart au
   site plutôt que de réduire le `gap` pour tout le monde. */
#recap-sondage .chevron-deplier {
  margin-left: calc(var(--espace-petit) - var(--espace-grand));
}



#recap-sondage .ti-tools-kitchen-2 {
  color: var(--couleur-accent-secondaire);
}

#liste-reponses[hidden] {
  display: none;
}

/* Les pseudos de la liste : c'est **la donnée-réponse**, donc couleur pleine et graisse.
   Ils étaient en discret sous un intitulé de groupe en gras plein, soit le rapport
   exactement inversé — l'étiquette ressortait plus que ce qu'elle annonce (voir la règle des
   trois rôles). */
#liste-reponses p {
  margin: var(--espace-minuscule) 0;
  font-size: var(--texte-courant);
  font-weight: 600;
}

/* Sauf ceux qui n'ont rien répondu : l'italique les distingue déjà, et le semi-gras
   insisterait sur une absence — c'est précisément ce que le projet ne veut pas faire (voir
   Contexte dans CLAUDE.md). */
#liste-reponses p.reponse-manquante {
  font-weight: 400;
}

/* L'intitulé de groupe (« Roulent et mangent ») : même rôle que `.titre-groupe-presence`
   sur la page historique, donc désormais **le même traitement**. Il portait un gras en
   couleur pleine, ce qui en faisait l'élément le plus lourd de la liste alors qu'il ne fait
   que nommer les colonnes. La structure est maintenant portée par le contraste 0,8 gris
   contre 0,9 plein, comme partout ailleurs. */
#liste-reponses .groupe-reponses {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
  margin: var(--espace-moyen) 0 var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

#liste-reponses .groupe-reponses:first-child {
  margin-top: 0;
}

#liste-reponses .membre-reponse {
  margin-left: var(--espace-bloc);
}

.reponse-manquante {
  font-style: italic;
}

/* **`url` est dans la liste pour la règle des 16 px avant tout** (voir Typographie) :
   un champ hors de ce sélecteur retombe sur le style natif du navigateur, dont la taille
   est sous 16 px — donc iOS Safari zoome à la mise au point. Le champ du lien LiveTrack
   était dans ce cas.

   **`search` et `select` y sont entrés le 20/08/2026, pour la troisième occurrence du même
   oubli.** Mesurés avant correction sur la page des anciennes sorties : le champ de recherche
   faisait 21 px de haut et le sélecteur d'année 19 px, tous deux en 13,3 px — la taille par
   défaut du navigateur, qui n'existe nulle part dans l'échelle de `:root`. Trois règles du
   projet tombaient ensemble : les 16 px d'iOS, l'échelle typographique, et les 24 x 24 px de
   cible tactile (WCAG 2.2).

   **La liste énumérée a été retournée le 22/08/2026, à la quatrième occurrence** (`D6` de
   `docs/revue-etats-2026-08-21.md`) : `number` manquait à son tour, et les **quatre** champs
   d'année du projet rendaient 13,33 px et 21 px de haut à côté de voisins à 16 px et 36 px
   **dans le même formulaire**. Le commentaire ci-dessus énonçait déjà le contrôle à faire —
   « c'est le `type=` qui décide » — et il n'a servi à rien, parce qu'un contrôle qui demande
   qu'on s'en souvienne ne se rejoue pas. **On énumère donc ce qu'il ne faut PAS habiller**,
   qui est une liste fermée et stable, au lieu de ce qu'il faut habiller, qui ne l'est pas.

   **`:where()` et non `:not()` nu** : `:where()` ne contribue **rien** à la spécificité, donc
   ce sélecteur vaut `(0,0,1)` — plus **bas** que les `(0,1,1)` d'avant. Toutes les règles de
   la feuille qui annulent quelque chose sur un champ gagnent donc **plus confortablement**
   qu'avant, au lieu de se faire battre.

   **La première rédaction de ce commentaire disait qu'un `:not()` nu aurait monté la
   spécificité à `(0,10,1)`. C'est faux, et c'était l'argument principal.** La spécificité
   d'un `:not()` est celle de son argument le plus spécifique — ici un sélecteur d'attribut,
   donc `(0,1,0)` — et `input:not(…)` vaut `(0,1,1)`, exactement ce que valait l'énumération
   qu'il remplace. Mesuré au navigateur le 22/08/2026 en opposant les deux formes à un
   sélecteur de deux classes. **La raison de `:where()` est donc l'inverse de celle qui était
   écrite** : on ne fuit pas une inflation qui n'existe pas, on descend **délibérément** sous
   les règles à une seule classe. Le mode d'échec s'inverse du même coup : un champ oublié est
   désormais **habillé**, et c'est une erreur qui se voit, là où l'oubli précédent était muet.

   **Le prix de cette descente, à connaître avant d'y toucher** : `.carte-empilee > *`
   (0,1,0) passe désormais **devant** cette règle. Un champ posé en enfant direct d'une
   `.carte-empilee` y perdrait ses marges verticales. Aucun ne l'est aujourd'hui.

   **Ça couvre aussi ce qu'aucun `grep` n'atteint** : `admin.js` fabrique son champ d'année
   avec `entree.type = type`, où le type est un argument de fonction. Une énumération dans la
   feuille ne pouvait pas le voir. */
input:where(:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"],
                  [type="submit"], [type="reset"], [type="button"], [type="image"],
                  [type="range"], [type="color"])),
select,
textarea {
  width: 100%;
  padding: var(--espace-petit);
  border-radius: 8px;
  border: 1px solid var(--couleur-texte-discret);
  background-color: var(--couleur-surface-encastree);
  color: var(--couleur-texte);
  font-size: var(--texte-champ);
  /* **Deux champs qui se suivent sont à 16 px**, et il faut deux valeurs pour l'obtenir
     (22/08/2026, `D14`). **Un `<input>` est `inline-block`** : ses marges ne fusionnent donc
     **jamais** avec celles de son voisin, contrairement à celles de deux `<p>`. La paire
     `0.25rem / 1rem` s'additionnait en **20 px** — hors échelle, dans les huit formulaires de
     l'application et depuis toujours.

     **Aucune sonde ne pouvait le voir, et c'est la leçon** : `ecartsHorsEchelle()` mesure ce qui
     est rendu, or un formulaire replié ne rend rien. La revue du 21/08 a mesuré onze pages sans
     déplier un seul formulaire. Le même angle mort que le sixième cas de `D7`.

     Le haut reste petit — il sépare le champ de son intitulé, pas de son voisin — et c'est le bas
     qui porte le rythme : 12 + 4 = 16. */
  margin: var(--espace-minuscule) 0 var(--espace-moyen);
  box-sizing: border-box;
}

textarea {
  font-family: inherit;
  resize: vertical;
  min-height: 4rem;
}

/* **La seule chose qu'un menu déroulant ne partage pas avec un champ de saisie** : il se
   dimensionne sur son contenu, là où un champ prend la largeur offerte. Le reste de son
   habillage vient de la déclaration commune ci-dessus, jamais recopié (voir les Conventions
   de CLAUDE.md). La flèche native est conservée — pas d'`appearance: none`, qui obligerait à
   la redessiner pour rien. */
select {
  width: auto;
  max-width: 100%;
}

/* **Un champ éteint doit se voir éteint** (04/09/2026), et c'est la déclaration ci-dessus qui
   l'en empêchait : en posant `color` et `background-color` en dur, elle **neutralise le grisé
   natif** du navigateur. Il ne restait qu'un `opacity: 0.7` — mesuré — sur un fond déjà très
   pâle, donc invisible à l'œil.
   **Trouvé sur le rappel d'une sortie libre**, dont le nombre s'éteint quand l'unité vaut « Pas
   de rappel » : `rappel.js` le désactive bien, avec un commentaire qui explique pourquoi — « 3 »
   à côté de « Pas de rappel » se lit comme un réglage qui n'a pas pris. L'intention était donc
   juste et écrite depuis le 29/08 ; c'est son rendu qui manquait, et le réglage paraissait brut
   pour cette raison.
   **La règle est générale et pas locale au rappel**, puisque la cause est générale : tout champ
   désactivé du projet était dans ce cas.

   **Le fond n'est délibérément pas touché**, et le premier jet le changeait — pour
   `var(--couleur-surface)`, **qui n'existe pas** : les trois surfaces du projet sont `-teintee`,
   `-encastree` et `-neutre`. Une variable inconnue ne lève rien, la déclaration est simplement
   ignorée, et le fond était ressorti **transparent** ; c'est la mesure du rendu qui l'a dit, pas
   la relecture. Les deux surfaces candidates étant de toute façon à un point l'une de l'autre en
   thème clair (`#E8EEF5` contre `#E5EDF5`), elles n'auraient rien distingué.
   Reste donc ce qui suffit : le texte passe en discret — le couple que le projet emploie partout
   pour ce qui ne demande pas l'attention — et le curseur dit que le champ ne se saisit pas. */
input:disabled,
select:disabled,
textarea:disabled {
  color: var(--couleur-texte-discret);
  opacity: 1;
  cursor: not-allowed;
}

/* **Le texte d'invite peut être plus petit que le champ, et c'est ce qui règle le problème
   sans toucher à la règle des 16 px** : iOS Safari décide de zoomer d'après le `font-size`
   du champ, pas d'après celui de son `::placeholder`. Le texte saisi reste donc à
   `--texte-champ`, obligatoire (voir Typographie), tandis que « T'as un truc à dire ? »
   cesse d'être le texte le plus gros de la carte alors qu'il n'annonce qu'un champ vide.
   Deux sélecteurs et non un seul : `::placeholder` n'est pas hérité, et une liste séparée
   par des virgules est invalidée en entier par un sélecteur inconnu du navigateur. */
input::placeholder {
  font-size: var(--texte-courant);
}

textarea::placeholder {
  font-size: var(--texte-courant);
}

/* Une case à cocher et son libellé, cliquables ensemble puisque le libellé est un
   `<label>`. **Trois emplois, une seule déclaration** : la case « visiteur » de
   l'administration, celle du bikepark au moment de proposer une sortie pas du mardi, et la
   même dans la ligne d'administration de ces sorties. La première vivait
   sans aucune règle — la case et son texte ne partageaient donc pas leur ligne de base —
   et l'arrivée de la seconde installait deux apparences pour un même contrôle si on les
   écrivait chacune de son côté (voir Conventions dans CLAUDE.md).

   **La marge basse aligne la case sur le rythme des champs**, et vaut aux trois emplois
   pour la même raison : chacun vit entre un champ de saisie — qui porte `margin: 0.25rem 0
   1rem` — et une rangée de boutons qui n'a pas de marge haute. Sans elle, la case flotte à
   1 rem sous le champ et se colle au bouton, alors qu'elle appartient au formulaire et non
   à l'action. */
.option-cochable {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  font-size: var(--texte-courant);
  margin-bottom: var(--espace-grand);
}

/* La case au gabarit du navigateur fait 13 px, soit nettement moins haut que le texte
   qu'elle accompagne — elle se lisait comme une décoration plutôt que comme le contrôle
   qu'on vient cocher. `accent-color` la fait virer à la couleur du thème une fois cochée,
   au lieu du bleu système qui ne ressemble à rien d'autre sur l'écran.

   **Une dimension de contrôle, pas une taille de police** : la règle de l'échelle de `:root`
   ne s'applique donc pas ici, et il n'existe pas de rôle à réutiliser — les variables
   d'icônes désignent des glyphes. */
.option-cochable input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  flex-shrink: 0;
  accent-color: var(--couleur-accent);
}

#selecteur-sortie-admin {
  margin-top: var(--espace-bloc);
  margin-bottom: var(--espace-bloc);
}

/* **La taille est posée explicitement bien qu'elle ne « change rien » à première vue**,
   et c'est justement le cas que la passe typographique décrit : sans `font-size`, ce
   message héritait du 1 rem du `body`, donc du palier des mises en avant. Dans la carte
   du suivi en direct, « Ce lien ne ressemble pas à un LiveTrack Garmin. » se retrouvait
   ainsi plus gros que le bouton et que la note qu'il surplombe — un refus qui pèse plus
   lourd que l'action. C'est un texte libre, donc `--texte-courant` ; seule la couleur le
   distingue, et elle est déjà là (voir Typographie dans docs/interface.md). */
.message-erreur {
  color: var(--couleur-danger);
  font-size: var(--texte-courant);
  margin: var(--espace-petit) 0 0;
}

/* Bandeau global d'échec réseau : en haut, au-dessus du contenu, sans passer sous les deux
   boutons `fixed` du haut — le menu à gauche, la cloche à droite, 40 x 40 posés à 16 px du bord
   et en `z-index: 1000`, donc peints **par-dessus** ce bandeau qui est à 800.

   **Le retrait est symétrique depuis le 22/08/2026** (`D11` de
   `docs/revue-etats-2026-08-21.md`), et c'est exactement la correction que `.bandeau-visiteur` a
   reçue la veille pour la même raison : il ne dégageait que la gauche, alors que la cloche occupe
   la même place à droite. **Le déficit ne dépendait pas de la largeur du téléphone** — 40 px de
   bouton contre 16 px de retrait, soit **24 px de texte toujours recouvrables**, et sur les
   **trois premières lignes** puisqu'une ligne fait 20 px et le bouton 40. (Au-delà d'environ
   592 px de fenêtre il disparaît de lui-même : le corps est une colonne de 480 px centrée,
   alors que les deux boutons sont `fixed` au **viewport** — ils sortent alors de la colonne et
   ne recouvrent plus rien. Le retrait y est du confort perdu, pas un défaut réparé.) Neuf des trente-cinq
   messages du projet les franchissaient, dont « Quelque chose s'est mal passé à l'affichage »,
   qui existe sur les onze pages.

   **Ce que ça coûte, et pourquoi on le paie quand même** : 40 px de largeur de texte. Quand la
   lisibilité du bandeau et l'atteignabilité des boutons s'opposent, **les boutons gagnent** — le
   bandeau apparaît justement quand le réseau flanche, c'est-à-dire au moment où l'on veut le menu
   pour aller voir ailleurs ; un bandeau amputé de deux mots reste lisible, un menu inatteignable
   ne se contourne pas. Et `signalerErreur()` n'a aucune minuterie, contrairement à
   `signalerInfo()` : un mot recouvert le serait jusqu'au rechargement.

   **Le dégagement ne vaut que là où il y a quelque chose à dégager**, et il a fallu le dire :
   **cinq des onze pages portent ce bandeau sans les deux boutons** — l'administration, les trois
   pages de dépôt et le partage LiveTrack. Elles réservaient 56 px de chaque côté pour contourner
   des boutons absents. Sur un écran de 390 elles y réservaient **112 px**, dont **80 récupérables**
   — le retrait de 1 rem de chaque côté n'étant pas négociable, c'est lui qui reste. C'est
   précisément sur les pages de dépôt qu'un message d'erreur a besoin d'être lu en entier. Le
   retrait large est donc porté par `body:has(#bouton-menu)`, qui dit exactement la condition
   plutôt que d'énumérer les pages — une liste de pages serait la cinquième du projet à s'oublier. */
.bandeau-erreur {
  position: sticky;
  top: 0;
  z-index: 800;
  margin: 0 0 var(--espace-petit);
  padding: var(--espace-petit) var(--espace-grand);
  background-color: var(--couleur-danger);
  color: var(--couleur-texte-bouton-danger, white);
  font-size: var(--texte-courant);
  text-align: left;
  border-radius: 0 0 10px 10px;
}

.bandeau-erreur[hidden] {
  display: none;
}

/* Les neuf pages à menu, et elles seules : c'est la présence du bouton qui décide, pas le nom du
   fichier. `:has()` est déjà employé par la feuille (voir `.ligne-hote`), donc rien de neuf. */
body:has(#bouton-menu) .bandeau-erreur {
  padding-left: var(--espace-degagement);
  padding-right: var(--espace-degagement);
}

/* Variante du même bandeau pour une confirmation — seule la couleur change, le gabarit
   et le placement restent ceux de l'échec : deux messages qui doivent se ressembler
   partagent leur déclaration au lieu de la recopier (voir la convention dans CLAUDE.md).
   La paire accent/texte est celle de `.bouton-principal`, pour que « ça a marché » se
   dise partout de la même couleur. */
.bandeau-erreur.bandeau-info {
  background-color: var(--couleur-accent);
  color: var(--couleur-texte-bouton-principal, white);
}

/* Le pied de la carte du mardi, qui ne porte que le bouton de partage. **Il existe parce
   que `.ligne-hote` ne pouvait pas l'accueillir**, et c'est une leçon à ne pas défaire :
   cette rangée porte un `flex-wrap` posé exprès (voir plus haut), l'intitulé et
   « Je reçois » n'y tenant déjà pas côte à côte avec un libellé long. Le partage y ajoutait
   une quarantaine de pixels, ce qui faisait replier le **cas courant** et jetait « En fait
   non... » seul sur une ligne entière — constaté à l'écran, pas au code.

   Il porte de toute façon sur la carte entière et non sur l'hôte : son pied est sa place,
   même montage que `.pied-trace` pour le bloc de la trace. */
/* **Centré, et le bouton porte un libellé** (20/08/2026, deuxième tour). Il a été aligné à
   droite le matin même, au motif qu'un glyphe seul au centre d'un pied se lit comme posé là
   faute de place. **C'était troquer un défaut contre un autre** : toutes les autres rangées de
   la carte sont soit centrées (les compteurs, les boutons du sondage), soit tenues par deux
   masses (l'hôte et son bouton, la température et le coucher du soleil). Une seule petite
   masse jetée à droite cassait cet axe — signalé à l'écran.

   **La vraie réponse était le libellé, pas la position** : « Partager » à côté de l'icône fait
   une action assumée au lieu d'un signe isolé, et le centre redevient la bonne place.

   **Pourquoi il diffère du partage de `.pied-trace`, qui reste une icône nue** : là-bas le
   bouton est équilibré par « Déposée par X » à l'autre bout de la rangée. Ici il est seul
   dans son pied — ce n'est pas la même situation, donc pas la même forme. */
.pied-sortie {
  display: flex;
  justify-content: center;
}

/* Le libellé prend la taille du texte courant, l'icône gardant la sienne : `.bouton-nu` pose
   un `font-size` d'icône que le texte hériterait sinon. La marge négative de `.bouton-nu`
   sert à recoller un bouton au bord droit d'une rangée — centré, elle le décalerait. */
#partager-sortie {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  margin-right: 0;
}

/* **La seule règle `display` du projet qui n'avait pas son `[hidden]`** (28/08/2026,
   `RC-UX-032`). L'invariant est explicite — une règle d'auteur l'emporte sur le
   `[hidden] { display: none }` du navigateur à spécificité égale, et ici c'est un sélecteur
   d'identifiant, donc elle l'emporte largement. `sortie.js` pose bien
   `partager-sortie.hidden = idSortieSelectionnee === null`, et ça ne masquait rien.

   Trouvé par criblage : sur les **36 identifiants** que les modules JS basculent par
   `getElementById(…).hidden`, 35 avaient leur règle `[hidden]` — souvent avec un commentaire
   nommant le piège — et celui-ci était le seul sans. La branche n'est pas atteignable en
   fonctionnement normal (une sortie est toujours sélectionnée sur cet écran), d'où la gravité
   info ; mais un invariant qui souffre une exception muette n'en est plus un. */
#partager-sortie[hidden] {
  display: none;
}

#partager-sortie span {
  font-size: var(--texte-courant);
}

/* Le partage posé dans l'angle haut-droit d'une carte. **Sorti du flux**, sans quoi il
   pousserait le premier bloc de la carte vers le bas — ici les compteurs, qui sont
   centrés et se retrouveraient décalés d'une ligne.

   La marge négative de `.bouton-nu` n'a plus de sens sur un élément positionné : elle est
   annulée, et c'est `right` qui aligne le glyphe sur le bord. */
.bouton-coin-carte {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  margin-right: 0;
}

#carte-detail-sortie {
  position: relative;
}

/* Le groupe d'actions au bout d'une ligne d'en-tête, employé par les cartes des sorties
   pas du mardi : leur en-tête est en `space-between` à deux blocs — la date à gauche, ce
   qu'on peut en faire à droite — où un troisième enfant direct les écarterait tous les
   trois. Ces en-têtes-là ne portent que des boutons-icônes, ils n'ont donc pas le problème
   de place de `.ligne-hote`. */
.actions-carte {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

/* `.bouton-nu` porte une marge négative qui aligne optiquement son glyphe sur le bord de
   la carte : elle n'a de sens que pour le dernier du groupe. Sur les précédents, elle
   grignoterait l'écart avec le suivant jusqu'à faire se chevaucher les deux surfaces
   tactiles — deux cibles au doigt qui se recouvrent, sur un écran de téléphone. */
.actions-carte > .bouton-nu:not(:last-child) {
  margin-right: 0;
}

/* `.carte-visee` a été retirée le 22/08/2026 : elle cerclait la carte désignée par un lien
   partagé, et **plus rien ne la posait depuis le 20/08**, jour où le lien a été changé pour
   ouvrir directement la vue détail au lieu de faire défiler jusqu'à la carte. Dix lignes de
   commentaire justifiaient un choix de conception (`outline` et non `border`, pour ne pas
   décaler la carte de ses voisines) sur un élément qui ne se peignait jamais. */

/* ---------- Mode visiteur : voir sans écrire ---------- */

/* Un visiteur est quelqu'un d'extérieur à qui l'on montre l'application. Il voit tout et ne
   peut rien poser. `membre.js` bascule la classe sur `<body>` (voir `estVisiteur`).

   **Tout est masqué ici et non par une condition dans chaque module**, pour une raison
   pratique : ces commandes vivent sur cinq pages et **la moitié est construite en JS après
   coup** — les pouces sous chaque commentaire et chaque photo, les croix de retrait, la
   tuile d'ajout. Un sélecteur les attrape toutes, y compris celles qui n'existent pas encore
   au chargement ; une condition par module en oublierait forcément une, et l'oubli ne se
   verrait que le jour où un visiteur clique.

   **`display: none` et pas une simple transparence** : l'élément sort de l'arbre, donc il
   n'est ni cliquable ni atteignable au clavier.

   **Ce n'est pas une protection**, c'est une interface. Le projet ne vérifie aucune identité
   côté serveur (voir les invariants de CLAUDE.md) : un `POST` direct passerait. Même modèle
   de confiance que les boutons d'hôte et d'invité, à ne pas confondre avec un verrou. */
body.visiteur #sondage-reponses,
body.visiteur #bouton-je-recois,
body.visiteur #bouton-ouvrir-ajout-invite,
body.visiteur #formulaire-ajout-invite,
body.visiteur .action-invite,
body.visiteur .formulaire-livetrack,
body.visiteur #bouton-retirer-livetrack,
body.visiteur #depot-photos,
body.visiteur .tuile-ajout-photo,
body.visiteur #depot-trace,
body.visiteur #bouton-corriger,
body.visiteur #detail-correction,
/* **`#bloc-edition` a disparu le 21/08/2026**, éclaté en deux gestes : corriger l'édition
   affichée (le crayon et son formulaire, dans la carte du voyage) et en lancer une autre (le
   bouton et son formulaire, dans la zone des éditions). Les quatre sont listés ici, là où une
   seule ligne suffisait — c'est le prix de la séparation, et il fallait le payer : le bloc unique
   ne se comprenait pas (voir `docs/voyage.md`). **Les déclencheurs autant que les formulaires**,
   sans quoi un visiteur ouvre un panneau vide. */
body.visiteur #bouton-editer-voyage,
body.visiteur #formulaire-voyage,
body.visiteur #bouton-creer-voyage,
body.visiteur #formulaire-creation-voyage,
body.visiteur .boutons-sortie-libre,
body.visiteur #formulaire-sortie,
/* **Le bouton qui déplie autant que le formulaire qu'il déplie.** `#formulaire-sortie` seul
   laissait un visiteur ouvrir une carte vide : le contenu est masqué, pas son déclencheur. */
body.visiteur #bouton-ouvrir-formulaire,
/* **`.formulaire-commentaire` et `.bouton-reaction` ont quitté cette liste le 20/08/2026** : un
   visiteur peut désormais commenter et réagir. C'était une demande explicite, et la règle
   d'origine — « il voit tout et n'écrit rien » — est donc devenue « il voit tout, il peut dire ce
   qu'il en pense, mais il n'organise pas ». Ce qui reste masqué relève de l'organisation : le
   sondage, l'hôte, les invités, les photos, la trace, la correction de l'historique.

   **Ne pas les remettre sans regarder ce qui en dépend** : son pseudo apparaît maintenant sous les
   commentaires, dans les pseudos de réactions, et dans le fil des dernières infos. */
body.visiteur #retirer-photo,
body.visiteur .pied-trace .bouton-nu[aria-label^="Retirer"],
/* **Ce que la règle n'avait pas suivi jusqu'au 13/09/2026** (`RC-UX-085` de l'audit par le cycle de
   vie). Le Tricount, les rappels posés, le calendrier, l'atelier et l'invité d'une sortie libre sont
   arrivés après elle sans y entrer : un visiteur pouvait poser un rappel qui fait sonner tout le
   groupe, remplacer le lien où l'on règle les comptes, noter et déplacer au calendrier. **Le bouton
   d'invité d'une sortie libre est fabriqué en JS sans identifiant**, d'où la classe qu'il partage avec
   celui du mardi.

   **Sans Tricount, la carte entière** : son intitulé resterait seul, une carte vide. **Le
   glisser-déposer et la sélection du calendrier n'ont pas de bouton à masquer** : ils se coupent dans
   `calendrier.js`, qui rend aussi les champs d'une note inertes. */
body.visiteur .bouton-icone-invite,
body.visiteur .ligne-ajout-invite,
body.visiteur #detail-libre-rappel,
body.visiteur #modifier-tricount,
body.visiteur #formulaire-tricount,
body.visiteur #bouton-retirer-tricount,
body.visiteur #detail-libre-tricount:has(#rangee-tricount[hidden]),
body.visiteur #ajouter-velo,
body.visiteur #noter-ce-jour,
body.visiteur #proposer-ce-jour,
body.visiteur #popup-evenement .champ-rappel,
body.visiteur #enregistrer-evenement,
body.visiteur #evenement-en-sortie,
body.visiteur #supprimer-evenement {
  display: none;
}

/* Le mot de l'hôte reste **lisible** mais cesse d'être une zone cliquable : il bascule en
   édition au clic pour le seul hôte, et un visiteur ne peut pas l'être — la règle est là
   par principe, pour que rien de cette zone ne réagisse au doigt. */
body.visiteur #mot-hote-sortie {
  pointer-events: none;
}

/* Le bandeau qui dit ce qu'on est. Sans lui, un visiteur croit l'application cassée : les
   boutons qu'on lui a décrits n'y sont pas. Il porte le ton du reste — on l'accueille, on
   ne lui notifie pas un niveau d'habilitation. */
.bandeau-visiteur {
  display: none;
  margin: 0 0 var(--espace-moyen);
  /* **Le retrait est symétrique, et c'est ce qui le centre** (21/08/2026). Il valait
     `3.5rem` à gauche — pour dégager le bouton de menu, `fixed` dans ce coin — et `0.75rem`
     à droite, où la cloche occupe pourtant la même place. Le texte étant centré **dans sa
     boîte**, il tombait **22 px à droite** du centre de l'écran : exactement la moitié de
     l'écart entre les deux retraits, mesuré. Deux bords à dégager, deux retraits égaux. */
  padding: var(--espace-petit) var(--espace-degagement);
  border-radius: 10px;
  background-color: var(--couleur-surface-neutre);
  color: var(--couleur-texte-discret);
  font-size: var(--texte-etiquette);
}

body.visiteur .bandeau-visiteur {
  display: block;
}


/* Onglets soulignés, et non « dossiers » comme ceux de la carte de sortie : ici
   le panneau contient lui-même des cartes teintées, un onglet actif au même fond
   s'y confondrait. Le `margin-bottom: -1px` fait passer le trait de 2px par
   dessus la bordure du conteneur au lieu de s'y ajouter. */
.onglets-admin {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-bottom: 1px solid var(--couleur-bordure);
  margin: var(--espace-bloc) 0 var(--espace-grand);
}

.onglet-admin {
  border: none;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  background-color: transparent;
  color: var(--couleur-texte-discret);
  padding: var(--espace-petit) var(--espace-minuscule);
  font-size: var(--texte-bouton-adaptatif);
}

.onglet-admin:hover {
  background-color: transparent;
}

.onglet-admin.actif {
  border-bottom-color: var(--couleur-accent);
  color: var(--couleur-texte);
}

/* Règle explicite par précaution : une propriété `display` posée un jour sur
   `.panneau-admin` l'emporterait sur le `[hidden]` du navigateur. */
.panneau-admin[hidden] {
  display: none;
}

.carte-membre-admin {
  background-color: var(--couleur-surface-teintee);
  border-radius: 12px;
  padding: var(--espace-petit) var(--espace-grand);
  margin: var(--espace-petit) 0;
}

/* En-tête cliquable : il faut neutraliser le style générique des boutons
   (contour, arrondi, fond au survol), qui n'a pas de sens pour une ligne de
   liste occupant toute la largeur de sa carte. */
.entete-membre-admin {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  width: 100%;
  padding: var(--espace-minuscule) 0;
  border: none;
  border-radius: 0;
  background-color: transparent;
  color: var(--couleur-texte);
  font-size: var(--texte-fort);
  text-align: left;
}

.entete-membre-admin:hover {
  background-color: transparent;
}

/* **Le chevron de ce qui se déplie, partagé** (03/09/2026). Il vivait ici pour la seule
   administration ; la météo et le récap du sondage l'ont rejoint plutôt que d'en recopier un —
   trois chevrons qui se ressembleraient sans partager leur déclaration finiraient par diverger.

   **Ce qu'il corrige** : ces deux zones-là n'avaient que `cursor: pointer` pour dire qu'elles
   s'ouvrent, c'est-à-dire **rien du tout au doigt**, qui est pourtant le contexte principal.
   L'accessibilité était là depuis toujours (`role`, `tabindex`, `aria-expanded`) ; c'est
   l'affordance visuelle qui manquait.

   **Le sélecteur d'état vise l'attribut et non une classe** : celui qui annonce l'ouverture au
   lecteur d'écran est celui qui fait pivoter le chevron, donc les deux ne peuvent pas se
   contredire. Sa portée reste étroite — il faut porter `.chevron-deplier` pour être concerné. */
.chevron-membre-admin,
.chevron-deplier {
  color: var(--couleur-texte-discret);
  transition: transform 0.2s ease;
}

.entete-membre-admin[aria-expanded="true"] .chevron-membre-admin,
[aria-expanded="true"] > .chevron-deplier {
  transform: rotate(180deg);
}

.corps-membre-admin {
  margin-top: var(--espace-moyen);
}

.corps-membre-admin[hidden] {
  display: none;
}

.carte-membre-admin.inactif {
  opacity: 0.5;
}

/* **Un anneau qui s'efface, et non plus un fond qui se décolore** (27/08/2026). L'animation
   partait de `--couleur-accent` pour finir sur `--couleur-surface-teintee` : elle **supposait
   donc que la cible soit une carte** de cette couleur-là, ce qui était vrai du seul usage
   d'alors — les cartes membres de l'administration.

   Depuis que les lignes du fil et les notifications désignent une zone **ou un objet**, la
   cible peut être un commentaire sans fond propre, une ligne d'hôte, un lien Tricount. Sur
   eux, l'ancienne forme finissait par teinter en gris un élément transparent, puis retombait
   d'un coup au retrait de la classe.

   `box-shadow` ne touche pas au fond de l'élément, donc **elle marche sur n'importe quoi** —
   et une seule déclaration sert les deux usages, comme le veut la convention. */
/* **Deux battements puis une disparition, sur 1,8 s** (27/08/2026). La première forme durait
   0,6 s et ne faisait qu'un fondu : elle suffisait à son seul usage d'alors — confirmer un
   geste qu'on venait de faire soi-même dans l'administration — mais pas au nouveau, où l'on
   arrive d'ailleurs et où **il faut trouver l'objet dans un écran qu'on découvre**. Signalé à
   l'usage : « il faut que ça se voie ».

   **Un anneau *et* un halo** : l'anneau dessine le contour, le halo attire l'œil de plus loin.
   Ni l'un ni l'autre ne touche au fond de l'élément, ce qui les rend utilisables sur un
   commentaire sans fond propre comme sur une carte teintée.

   **Le palier à 70 %** tient la surbrillance pleine pendant 1,25 s avant de la relâcher : un
   fondu qui commence tout de suite est déjà à moitié éteint quand le regard arrive. */
@keyframes flash-carte {
  0%,
  50%,
  70% {
    box-shadow: 0 0 0 3px var(--couleur-accent),
                0 0 16px 5px color-mix(in srgb, var(--couleur-accent) 45%, transparent);
  }
  25% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--couleur-accent) 35%, transparent),
                0 0 16px 5px transparent;
  }
  100% {
    box-shadow: 0 0 0 3px transparent, 0 0 16px 5px transparent;
  }
}

.carte-flash {
  animation: flash-carte 1.8s ease-out;
  /* Sans lui, l'anneau d'un élément à angles vifs colle au texte. Les cartes qui ont déjà le
     leur ne sont pas touchées : une valeur propre l'emporte sur celle-ci. */
  border-radius: 10px;
}

/* Devenu un <span> dans l'en-tête cliquable : plus de marge, elle n'aurait de
   toute façon aucun effet sur un élément en ligne. */
.pseudo-membre-admin {
  font-weight: 500;
}

.ligne-action-sortie {
  background-color: var(--couleur-surface-teintee);
  border-radius: 12px;
  padding: var(--espace-grand) 1.25rem;
  margin: var(--espace-grand) 0;
  text-align: left;
}

.nom-action-sortie {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  font-weight: 500;
  margin: 0 0 var(--espace-petit);
}

.badge-hote-admin {
  background-color: var(--couleur-accent);
  color: var(--couleur-texte-bouton-principal, white);
  border-radius: 20px;
  padding: var(--espace-minime) var(--espace-petit);
  font-size: var(--texte-etiquette);
  font-weight: 600;
}

.reponse-manquante-admin {
  font-style: italic;
  font-weight: 400;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.lien-membre-admin {
  color: var(--couleur-texte-discret);
  font-size: var(--texte-courant);
  word-break: break-all;
  margin: 0 0 var(--espace-moyen);
}

/* **La dernière visite d'un membre** (20/08/2026, chantier `S`). Même couleur et même taille
   que le lien juste au-dessus, dont elle est la voisine immédiate dans le corps de la carte :
   deux faits en retrait sur la même personne, ils n'ont aucune raison de peser différemment
   (la règle est dans les Conventions de CLAUDE.md). L'italique la distingue du lien, qui est
   une donnée à copier là où celle-ci est une information à lire.

   Pas de `word-break` : une date ne dépasse pas, contrairement à un lien personnel. */
.visite-membre-admin {
  color: var(--couleur-texte-discret);
  font-size: var(--texte-courant);
  font-style: italic;
  margin: 0 0 var(--espace-moyen);
}

/* **Le passage à la ligne est arrivé avec le sixième bouton** (« Tester la notif »,
   19/08/2026). La rangée tenait sur une seule ligne à cinq, en serrant ; à six, le partage
   descendait sous la largeur d'un mot et `overflow-wrap` coupait au milieu — « Régén / érer »,
   « Désact / iver ». La base de 6rem est la largeur en dessous de laquelle un libellé ne tient
   plus : les boutons s'y refusent et passent à la ligne, où ils se réétalent. */
.actions-membre-admin {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-petit);
}

.actions-membre-admin button {
  flex: 1 1 6rem;
  min-width: 0;
  padding: clamp(0.35rem, 1.5vw, 0.6rem) clamp(0.2rem, 1vw, 1rem);
  font-size: var(--texte-bouton-adaptatif);
  overflow-wrap: break-word;
}

.reponses-admin {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--espace-petit);
  margin-top: var(--espace-moyen);
}

.bouton-reponse-admin {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-minuscule);
  font-size: var(--texte-bouton-adaptatif);
  padding: clamp(0.35rem, 1.5vw, 0.5rem) clamp(0.2rem, 1vw, 0.4rem);
  overflow-wrap: break-word;
}

.ligne-journal {
  font-size: var(--texte-courant);
  color: var(--couleur-texte-discret);
  margin: 0;
  padding: var(--espace-petit) 0;
  border-bottom: 1px solid var(--couleur-bordure);
}

.ligne-journal:last-child {
  border-bottom: none;
}

/* **La taille est posée, et c'est tout l'intérêt de la poser.** Sans elle, ces sept titres
   héritaient du 2em par défaut du navigateur, soit **32 px — une valeur qui n'existe nulle
   part dans l'échelle de `:root`**, entre `--chiffre` (28,8) et `--chiffre-principal`
   (35,2). C'est le piège que la fiche typographie décrit : une taille héritée est
   invisible à la lecture de la feuille, donc on ne peut pas raisonner dessus. Sixième cas
   du genre, trouvé au navigateur le 17/08/2026 après deux passes qui l'avaient manqué.

   Posée sur la classe et **pas sur `h1`** : le `<h1>` de la carte du mardi ne contient
   qu'une image, et l'espace entre le logo et le titre est son `margin-top` par défaut de
   0.67**em** — donc calculé sur ce même `font-size`. Le régler sur l'élément aurait
   resserré cet espace de deux pixels sur un écran qui n'a rien demandé (voir Images et
   icônes dans `docs/interface.md`). */
.titre-statistiques,
.titre-compte {
  font-family: var(--police-affichage);
  font-style: italic;
  font-size: var(--chiffre);
  /* **La marge haute est posée, elle ne l'était pas** (21/08/2026). Sans elle, le titre
     prenait le `0.67em` par défaut du navigateur sur un `<h1>` — soit **19 px** ici et **20**
     sur l'écran principal, dont la police diffère. Un écart qu'aucune règle n'expliquait, et
     le même piège que le `1em` des `<p>` sortis d'un conteneur, déjà payé deux fois.
     `--espace-bloc` : le logo et le titre sont deux zones. */
  margin: var(--espace-bloc) 0 var(--espace-minuscule);
}

/* **`.salut-compte` partage cette déclaration plutôt que d'en recopier une** (21/08/2026) :
   c'est le même objet — la ligne discrète posée sous le titre d'une page, qui dit de quoi la
   page parle avant la première carte. Les statistiques et le compte se suivent dans le menu,
   donc un écart de taille ou de marge se verrait en passant de l'une à l'autre. */
.periode-stats,
.salut-compte {
  margin: 0 0 var(--espace-bloc);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.salut-compte[hidden] {
  display: none;
}

/* **Une rangée qui défile horizontalement plutôt qu'une grille**, contrairement aux
   onglets de sortie qui sont quatre pour toujours : les années s'ajoutent d'une par an, et
   sept pastilles réparties en colonnes égales seraient déjà illisibles sur 320 px.
   `flex-shrink: 0` sur les pastilles est ce qui fait défiler au lieu d'écraser. */
/* **Les deux défilements horizontaux du projet partagent leur comportement**, ils ne le
   recopient pas (voir les Conventions de CLAUDE.md) : les pastilles d'année et les prévisions
   horaires sont le même objet — une rangée plus large que son cadre, qu'on pousse au doigt.

   **La barre de défilement native est masquée depuis le 20/08/2026**, sur signalement : sous
   Windows elle est peinte en permanence, et elle apparaissait comme un trait gris sous les
   années **à l'arrivée sur la page**, sans qu'on ait rien fait. Sur un téléphone elle
   s'efface d'elle-même — c'est donc un artefact de bureau, mais visible par tout le monde
   depuis un poste.

   **Ce qu'elle disait est repris par le dégradé** : le bord droit s'estompe, ce qui annonce
   qu'il y a une suite, et le `scroll-snap` fait qu'on s'arrête sur un élément entier plutôt
   qu'à moitié. Masquer sans remplacer aurait supprimé l'affordance au lieu de la déplacer.

   `mask-image` et non un calque posé par-dessus : un pseudo-élément devrait connaître la
   couleur du fond, donc être redéfini dans les trois thèmes ; le masque efface, il ne peint
   pas, et ne dépend d'aucune couleur. */
.pastilles-annees,
.previsions-horaires {
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  mask-image: linear-gradient(to right, #000 calc(100% - 1.75rem), transparent);
}

.pastilles-annees::-webkit-scrollbar,
.previsions-horaires::-webkit-scrollbar {
  display: none;
}

.pastille-annee,
.carte-heure {
  scroll-snap-align: start;
}

/* Le sélecteur de période et la ligne qui la nomme forment un groupe : `--espace-petit` les
   tient serrés, là où `0.75rem` en faisait deux blocs distincts (12 px, une valeur de plus pour
   la même relation). Le `gap` entre pastilles suit le même palier. */
.pastilles-annees {
  display: flex;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-petit);
}

.pastille-annee {
  flex-shrink: 0;
  border: 1px solid var(--couleur-bordure);
  border-radius: 999px;
  padding: var(--espace-minuscule) var(--espace-moyen);
  font-size: var(--texte-etiquette);
  background-color: transparent;
  color: var(--couleur-texte-discret);
}

/* Même couple accent + `--couleur-texte-bouton-principal` que `.bouton-principal`, avec le
   même repli sur `white` : deux éléments qui doivent se ressembler partagent leur
   déclaration plutôt que d'inventer chacun la leur (voir Conventions dans CLAUDE.md). */
.pastille-annee.active {
  border-color: transparent;
  background-color: var(--couleur-accent);
  color: var(--couleur-texte-bouton-principal, white);
}

/* Colonnes égales, même correctif de débordement que .boutons-sondage :
   `minmax(0, 1fr)` et non `1fr`, sans quoi une colonne ne peut jamais rétrécir
   sous la largeur de son contenu et déborde sur un écran de 320px.

   **Autant de colonnes que de chiffres affichés, et non trois en dur** : depuis le
   19/08/2026 deux d'entre eux disparaissent sur une période d'avant le sondage, et une
   grille figée à trois laissait le survivant collé dans la première colonne, à gauche d'un
   grand vide. `grid-auto-flow: column` en crée exactement autant qu'il en reste. */
.bandeau-chiffres {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--espace-petit);
  margin-bottom: var(--espace-grand);
}

.chiffre-cle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--espace-minime);
}

/* **Quatrième occurrence du même piège** (après `.ligne-record`, `.mention-annulee` et
   `.panneau-admin`) : `display: flex` l'emporte sur le `[hidden]` du navigateur, si bien
   que masquer un chiffre-clé depuis le JS ne masquait rien du tout. Repéré à l'écran le
   19/08/2026 — la page affichait « 0 assiette » sur une année d'avant le sondage, ce que
   le masquage existait précisément pour éviter. */
.chiffre-cle[hidden] {
  display: none;
}

.chiffre {
  font-family: var(--police-affichage);
  font-style: italic;
  font-size: var(--chiffre);
  font-weight: bold;
  color: var(--couleur-accent-texte);
  line-height: 1;
}

.chiffre-libelle {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  overflow-wrap: break-word;
}

.note-echantillon {
  margin: 0 0 var(--espace-grand);
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

/* Même habillage que .carte-sortie, mais arrondie sur ses quatre coins : aucune
   rangée d'onglets ne vient se poser dessus ici. */
/* **`.carte-compte` partage cette déclaration plutôt que de la recopier** (20/08/2026) : les
   deux pages empilent des blocs de même nature sur le même gabarit secondaire, et deux
   habillages de carte dans la même application se seraient vus au premier aller-retour entre
   elles. C'est la règle du projet, celle qui a valu l'écart de `.etiquette-bouton`. */
.carte-stats,
.carte-compte {
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-radius: 14px;
  padding: var(--espace-grand) 1.25rem;
  margin: 0 0 var(--espace-grand);
}

/* Règle explicite : si une propriété `display` s'ajoutait un jour sur
   `.carte-stats`, elle l'emporterait sur le `[hidden] { display: none }` du
   navigateur à spécificité égale (même piège que .previsions-horaires). */
.carte-stats[hidden],
.carte-compte[hidden] {
  display: none;
}

/* **Un rythme vertical explicite pour les cartes qui empilent des lignes de nature
   différente**, et c'est un défaut de fond qu'elle corrige, pas un réglage à l'œil.

   Ces cartes assemblent des classes venues chacune d'un autre écran : `.chez-ancienne` et
   `.signature-trace` **ne déclarent aucune marge**, donc prennent celle du navigateur
   (1 em), tandis que `.texte-mot-hote`, `.noms-presence` et `.label-stats` sont à
   `margin: 0`. D'où ce qu'on voit sur un vrai téléphone : un grand vide à un endroit, trois
   lignes collées juste en dessous. Même famille que le piège des tailles héritées (voir
   Typographie dans docs/interface.md) — une marge qui vient du navigateur est invisible à
   la lecture de la feuille, on ne peut pas raisonner dessus.

   La gouttière est donc portée par le conteneur et les marges des enfants sont annulées :
   une seule valeur décide, quelles que soient les classes empilées demain.

   `.carte-stats[hidden]` reste plus spécifique que cette classe, l'attribut `hidden`
   continue donc de masquer la carte. */
.carte-empilee {
  display: flex;
  flex-direction: column;
  gap: var(--espace-petit);
}

/* Les enfants directs, **et le paragraphe niché dans l'en-tête** : la date y vit dans un
   `.entete-presences`, donc hors de portée d'un sélecteur d'enfant direct. Elle gardait à
   elle seule la marge du navigateur, ce qui laissait un vide sous elle alors que tout le
   reste était resserré — la moitié du défaut, invisible tant qu'on ne regarde pas la
   capture. */
.carte-empilee > *,
.carte-empilee .entete-presences > p {
  margin-top: 0;
  margin-bottom: 0;
}

.label-stats {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  margin: 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  /* Voir `.label-commentaires` plus bas : neutralise le gras d'un `<h2>` (`RC-UX-049`). */
  font-weight: normal;
}

.stat-principale {
  font-family: var(--police-affichage);
  font-style: italic;
  margin: var(--espace-minuscule) 0 0;
  font-size: var(--chiffre-principal);
  font-weight: bold;
  line-height: 1;
}

.stat-detail {
  margin: var(--espace-minuscule) 0 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* **Une phrase, donc `--texte-courant` et non `--chiffre-principal`** : « 26,7 km, 811 m de
   dénivelé, 2 h 46 en selle et 18,4° » sur une carte n'est pas un chiffre mis en avant,
   c'est une ligne à lire. Elle garde en revanche la police d'affichage et l'italique de
   `.stat-principale`, qui sont la signature de ces cartes. */
.stat-phrase {
  font-family: var(--police-affichage);
  font-style: italic;
  margin: var(--espace-minuscule) 0 0;
  font-size: var(--texte-courant);
  font-weight: bold;
  line-height: 1.3;
}

/* Le titre qui ouvre une section de la page des statistiques — « Moi » sous le collectif.
   **`2rem` en haut valait 32 px**, une neuvième valeur pour l'écart « deux zones qui se
   suivent » ; c'est bien une rupture de zone, mais elle se dit avec le palier qui existe pour
   ça. En bas, `--espace-moyen` : le titre et ce qu'il annonce forment un groupe. */
.titre-section-stats {
  font-family: var(--police-affichage);
  font-style: italic;
  margin: var(--espace-bloc) 0 var(--espace-moyen);
  font-size: var(--titre-section);
}

.ligne-record {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  margin: var(--espace-petit) 0;
  font-size: var(--texte-courant);
}

/* **Règle obligatoire dès qu'un sélecteur pose `display`** : à spécificité égale, la
   classe l'emporte sur le `[hidden] { display: none }` du navigateur, quel que soit
   l'ordre dans le fichier. Sans elle, la ligne du record de pluie resterait affichée sur
   un historique entièrement sec. Même piège que `.previsions-horaires[hidden]` et
   `#liste-reponses[hidden]`, payé deux fois avant celui-ci (voir Carte de sortie). */
.ligne-record[hidden] {
  display: none;
}

.mardi-graphe {
  margin-bottom: var(--espace-grand);
}

.mardi-graphe:last-child {
  margin-bottom: 0;
}

.date-mardi-graphe {
  margin: 0 0 var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  text-align: left;
  color: var(--couleur-texte-discret);
}

/* L'hôte ressort de la date, qui reste en retrait : c'est lui qu'on cherche en
   parcourant le graphe, la date ne sert qu'à se repérer. */
.hote-mardi {
  color: var(--couleur-texte);
}

.ligne-barre {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin: var(--espace-minime) 0;
}

/* La piste occupe la largeur restante ; `min-width: 0` l'autorise à rétrécir
   sous la largeur de son contenu, même piège que .boutons-sondage. */
.piste-barre {
  flex: 1;
  min-width: 0;
  height: 0.55rem;
  border-radius: 4px;
  background-color: var(--couleur-surface-neutre);
  overflow: hidden;
}

/* Un mardi annulé n'a pas de tablée à dessiner : la piste reste vide, en
   pointillés, plutôt qu'une barre à zéro qu'on lirait comme « personne ». */
.piste-annulee {
  background-color: transparent;
  border: 1px dashed var(--couleur-bordure);
}

/* `--taille` est la seule valeur venue du JS (la proportion mesurée). Une
   transition ne suffirait pas ici : l'élément est créé avec sa largeur
   finale déjà posée (--taille), rien ne changerait donc après coup pour la
   déclencher. D'où des @keyframes, qui jouent dès l'insertion dans le DOM. */
@keyframes pousse-barre {
  from { width: 0; }
  to { width: var(--taille, 0); }
}

.barre-graphe {
  width: var(--taille, 0);
  height: 100%;
  border-radius: 4px;
  animation: pousse-barre 0.6s ease-out backwards;
}

/* Décalage progressif : haut en bas. Écrit explicitement pour chacune des 12
   lignes possibles (StatistiquesEndpoints.MardisAffiches) — pas de variable
   d'index venue du JS, seulement des sélecteurs nth-child et un pas fixe. */
.mardi-graphe:nth-child(1)  .barre-graphe { animation-delay: 0.00s; }
.mardi-graphe:nth-child(2)  .barre-graphe { animation-delay: 0.05s; }
.mardi-graphe:nth-child(3)  .barre-graphe { animation-delay: 0.10s; }
.mardi-graphe:nth-child(4)  .barre-graphe { animation-delay: 0.15s; }
.mardi-graphe:nth-child(5)  .barre-graphe { animation-delay: 0.20s; }
.mardi-graphe:nth-child(6)  .barre-graphe { animation-delay: 0.25s; }
.mardi-graphe:nth-child(7)  .barre-graphe { animation-delay: 0.30s; }
.mardi-graphe:nth-child(8)  .barre-graphe { animation-delay: 0.35s; }
.mardi-graphe:nth-child(9)  .barre-graphe { animation-delay: 0.40s; }
.mardi-graphe:nth-child(10) .barre-graphe { animation-delay: 0.45s; }
.mardi-graphe:nth-child(11) .barre-graphe { animation-delay: 0.50s; }
.mardi-graphe:nth-child(12) .barre-graphe { animation-delay: 0.55s; }

.barre-velo {
  background-color: var(--couleur-serie-velo);
}

.barre-table {
  background-color: var(--couleur-serie-table);
}

/* **Le profil par mois : douze colonnes de largeur égale**, `minmax(0, 1fr)` pour la même
   raison que partout ailleurs — sans lui une colonne ne peut pas rétrécir sous son contenu
   et le graphe déborde sur 320 px. */
#profil-par-mois {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--espace-minuscule);
  align-items: end;
}

.colonne-mois {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--espace-minuscule);
}

/* La piste tient toute la hauteur et la barre monte depuis le bas : c'est
   `justify-content: flex-end` qui la fait pousser vers le haut plutôt que pendre. */
.piste-mois {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  width: 100%;
  height: 3.5rem;
  border-radius: 4px;
  background-color: var(--couleur-surface-neutre);
  overflow: hidden;
}

.barre-mois {
  height: var(--taille);
  border-radius: 4px;
  background-color: var(--couleur-serie-velo);
}

/* **Le rôle « mention discrète de second rang », déclaré une fois pour sept sélecteurs**
   (28/08/2026, `RC-CODE-046`). Il l'était **sept fois**, à l'identique — c'est la convention de
   `CLAUDE.md` qui avait dérivé le plus loin : « deux éléments qui doivent se ressembler
   partagent leur déclaration, ils ne la recopient pas ».

   **Ce n'est pas une ressemblance de hasard, et c'est ce qui les réunit** : ces éléments se
   suivent à l'écran. La signature et le libellé d'un chiffre sont dans la **même carte de
   trace** ; la date d'un commentaire et les pseudos sous une réaction sont dans le **même
   commentaire**. Une dérive se verrait aussitôt contre sa voisine — exactement le cas fondateur
   de la règle (`.etiquette-bouton`, 0,9 rem contre 1 rem).

   **C'est une mention, pas une donnée** : elle accompagne ce qu'on lit sans lui disputer la
   lecture. (Cette phrase vient du commentaire de `.noms-reaction`, qui disait déjà « même palier
   que les autres mentions discrètes du projet » — il nommait le rôle sans que le rôle existe.)

   **Regroupé sans risque de cascade, et c'était à vérifier avant** : chacune des sept classes
   n'apparaît qu'une seule fois dans la feuille, donc aucune autre règle ne dispute `font-size`
   ni `color` sur ces éléments, et l'ordre de déclaration est indifférent.

   Couvre l'étiquette d'un mois du graphe, la date d'une ligne d'historique, celle d'un
   commentaire, le libellé d'un chiffre de trace, la signature d'une trace, l'intitulé du
   sélecteur d'année du voyage et les pseudos sous une réaction. */
.etiquette-mois,
.date-ancienne,
.date-commentaire,
.libelle-trace,
.signature-trace,
.choix-annee-voyage label,
.noms-reaction {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* **La frise : une marque par mardi, qui passe à la ligne toute seule.** Une année tient
   ainsi sur deux rangs sur un téléphone et sur un seul sur un écran large, sans média
   query — c'est le `flex-wrap` qui décide, pas une largeur devinée. */
#frise-mardis {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-minuscule);
  justify-content: center;
}

/* Le mardi sans hôte garde sa place et son contour : c'est un mardi qui a existé, dont on
   ne sait rien — le vider entièrement le ferait disparaître de la frise, ce qui dirait
   autre chose. */
.marque-mardi {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 3px;
  border: 1px solid var(--couleur-bordure);
}

.marque-mardi.tenu {
  border-color: transparent;
  background-color: var(--couleur-serie-velo);
}

/* Largeur fixe : sans elle, les pistes ne s'alignent pas d'une ligne à l'autre
   dès qu'un nombre passe à deux chiffres. */
.valeur-barre {
  min-width: 2.5rem;
  text-align: right;
  font-size: var(--texte-etiquette);
}

.ligne-moi {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  margin: var(--espace-petit) 0;
}

.ligne-moi[hidden] {
  display: none;
}

/* Les notes sont des phrases, pas des lignes de compteur. Le trait est posé sur
   le conteneur et non sur chaque phrase : sinon une note masquée emporterait le
   séparateur avec elle, ou en ferait apparaître deux. */
.notes-moi {
  margin-top: var(--espace-moyen);
  padding-top: var(--espace-moyen);
  border-top: 0.5px solid var(--couleur-bordure);
}

.note-moi {
  margin: var(--espace-minuscule) 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.note-moi[hidden] {
  display: none;
}

.note-serie {
  color: var(--couleur-accent-texte);
}

/* --- Les anciennes sorties ---------------------------------------------- */

/* Recherche et filtre d'année sur une ligne, le champ prenant la place restante.
   Le `<select>` garde sa largeur naturelle : une année est courte, l'étirer
   n'apporterait rien et volerait de la place au champ de recherche. */
.filtres-anciennes {
  display: flex;
  gap: var(--espace-petit);
  align-items: center;
  margin-bottom: var(--espace-grand);
}

.filtres-anciennes[hidden] {
  display: none;
}

/* **L'annulation de la marge doit battre la déclaration générique, et une classe seule n'y
   suffit pas.** `input[type="search"]` vaut (0,1,1) — un élément plus un attribut — quand
   `.champ-recherche` ne vaut que (0,1,0) : la marge générique gagnait.

   **Ça ne se voyait pas avant le 20/08/2026**, parce que la déclaration générique ne visait
   alors ni `search` ni `select` : la classe n'avait rien à combattre, et son commentaire
   annonçait déjà une annulation qui n'annulait rien. Habiller les deux contrôles a réveillé
   la marge, et le champ s'est retrouvé **6 px plus haut que le sélecteur d'année**.

   **Les deux contrôles partagent donc cette déclaration** (voir les Conventions de CLAUDE.md) :
   ils vivent sur la même rangée, leur alignement est la seule chose qui compte, et deux règles
   séparées se seraient désaccordées au premier ajustement — c'est exactement ce qui vient
   d'arriver, le sélecteur ayant sa propre règle et pas le champ. */
.filtres-anciennes .champ-recherche,
.filtres-anciennes select {
  margin: 0;
}

.champ-recherche {
  flex: 1;
  min-width: 0;
}

/* Séparateur d'année, posé sur l'en-tête qui *suit* comme tous les séparateurs du
   projet. Le premier de la liste n'en a pas besoin. */
/* **Deux années qui se suivent sont deux zones**, d'où `--espace-bloc` (22/08/2026, `D8`) :
   1,25 rem rendait 20 px, qui n'est pas un palier. */
.entete-annee {
  margin: var(--espace-bloc) 0 var(--espace-petit);
  padding-top: var(--espace-moyen);
  border-top: 0.5px solid var(--couleur-bordure);
  font-size: var(--texte-etiquette);
  text-align: left;
  color: var(--couleur-texte-discret);
}

.entete-annee:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

/* Une ligne par mardi. Même habillage que .carte-stats, en plus compact : c'est
   une liste qu'on parcourt, pas une carte qu'on lit. Les lignes cliquables sont
   de vrais <button>, d'où la neutralisation du style générique des boutons. */
.ligne-ancienne {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--espace-petit);
  width: 100%;
  /* Indispensable, et pas décoratif : les lignes cliquables sont des <button>
     (donc `border-box` par défaut, comme tout contrôle de formulaire) alors que
     la ligne d'un mardi annulé est un <div> (`content-box`). Sans cette
     déclaration, `width: 100%` + padding + bordure s'additionnent sur le seul
     <div>, qui débordait de l'écran quand ses voisines tenaient. */
  box-sizing: border-box;
  margin-bottom: var(--espace-petit);
  padding: var(--espace-petit) var(--espace-moyen);
  border: 1px solid var(--couleur-bordure);
  border-radius: 10px;
  background-color: var(--couleur-surface-teintee);
  color: var(--couleur-texte);
  font-size: var(--texte-courant);
  text-align: left;
  cursor: pointer;
}

.ligne-ancienne:hover {
  border-color: var(--couleur-accent);
}

/* Un mardi annulé garde son allure en retrait — bordure pointillée, texte discret —
   pour se lire comme un creux dans la chronologie. Il est en revanche **cliquable
   comme les autres** depuis qu'il peut porter un souvenir (pourquoi on a annulé,
   photos, météo), d'où le curseur et le survol laissés à la règle commune. */
.ligne-ancienne.annulee {
  background-color: transparent;
  border-style: dashed;
  color: var(--couleur-texte-discret);
}

.ancienne-gauche {
  display: flex;
  flex-direction: column;
  gap: var(--espace-minime);
  min-width: 0;
}


/* Le « chez X », dans la liste **et** dans le détail d'un mardi : une seule déclaration pour
   la même information, au lieu du traitement de mention discrète qu'elle avait hérité en
   partageant la classe des périodes de statistiques. */
/* Le seul écart entre les deux emplois : dans le détail, il vit sous le titre du mardi et a
   besoin de la gouttière que `.periode-stats` lui donnait. */
.hote-detail {
  margin: 0 0 var(--espace-bloc);
}

/* **La ligne d'hôte se comporte en bouton quand elle est éditable** — le curseur le dit, même
   idiome que le mot de l'hôte et la zone météo de la carte du mardi, où la zone elle-même est
   le contrôle. Le sélecteur porte sur `[role="button"]` et non sur une classe : c'est le rôle
   posé par le JS qui décide, il n'y a donc qu'une source. */
#detail-hote[role="button"] {
  cursor: pointer;
}

/* Les pseudos proposés pour l'hôte d'une ancienne sortie. **Une rangée qui se replie, pas une
   colonne** : treize pseudos courts empilés dépasseraient la hauteur de la popup, qui est
   plafonnée à `calc(100% - 2rem)`. L'espacement entre ce bloc et le reste vient du `gap` de
   `.popup[open]`, il n'y a donc aucune marge à poser ici. */
.choix-hote {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--espace-petit);
  overflow-y: auto;
}

/* **Une seule déclaration depuis le 16/08/2026**, alors que la classe en avait deux à
   six cents lignes d'écart : celle-ci, et une autre dans la zone du graphe qui posait
   l'italique et la couleur en retrait. Les deux s'appliquaient aux deux emplois, chacune
   n'en décrivant qu'un — la mention du graphe héritait donc d'un `display: flex` dont elle
   n'avait que faire, et celle de l'historique d'un style écrit pour sa voisine. Le graphe
   ayant cessé d'employer la classe (sa mention est passée dans l'en-tête), la seconde
   déclaration est venue ici, à l'identique : le rendu de l'historique ne bouge pas. */
/* **`.mention-sortie-libre` partage cette déclaration plutôt que d'en recopier une**
   (20/08/2026) : c'est le même objet — un état de la sortie, dit en retrait sous son titre —
   et les deux se lisent sur des écrans qui se suivent, la liste des sorties libres et
   l'historique. La règle du projet est explicite là-dessus : deux éléments qui doivent se
   ressembler partagent leur déclaration, ils ne la recopient pas.

   **Elle n'hérite pas du nom pour autant, et c'est délibéré** : une sortie close n'est pas une
   sortie annulée. Réemployer `.mention-annulee` telle quelle aurait fait dire au balisage
   l'inverse de ce qui s'est passé. */
.mention-annulee,
.mention-sortie-libre {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
  font-size: var(--texte-courant);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

/* `flex-wrap` depuis que les compteurs disent ce qu'ils comptent : « 12 ont
   roulé » et « 14 ont mangé » côte à côte dépassent 288px, la largeur utile la
   plus étroite du projet. */
.compteurs-ancienne {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-moyen);
  align-items: center;
  flex-shrink: 0;
}

/* Indispensable et pas décoratif, même piège que `.panneau-admin` plus haut : le
   `display: flex` de la classe l'emporterait sur le `[hidden]` du navigateur, et les
   compteurs resteraient visibles sur un mardi annulé, où ils ne veulent rien dire. */
.compteurs-ancienne[hidden] {
  display: none;
}

.compteurs-ancienne .ti-tools-kitchen-2 {
  color: var(--couleur-accent-secondaire);
}

/* Le temps qu'il faisait ce mardi-là. `flex-wrap` parce que la ligne complète
   (emoji, description, températures, cumul de pluie) dépasse 288px — la largeur
   utile la plus étroite du projet — sur un « Brouillard · 12.4° / 19.8° · 💧 3.2mm ».
   Pas de `justify-content: space-between` comme #meteo-sortie sur la carte du
   mardi : là-bas deux groupes s'opposent aux deux bords, ici c'est une phrase. */
.meteo-ancienne {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-petit);
  font-size: var(--texte-courant);
  margin-top: var(--espace-petit);
}

/* Troisième garde de la même famille (21/08/2026) : `display: flex` l'emporte sur le
   `[hidden]` du navigateur. Sans elle, la ligne météo masquée laissait sa `margin-top` sous le
   bloc précédent — un écart de 8 px sur toute sortie sans relevé. */
.meteo-ancienne[hidden] {
  display: none;
}

.meteo-ancienne-temperatures,
.meteo-ancienne-pluie {
  color: var(--couleur-texte-discret);
}

/* Retour à la liste : un lien déguisé en bouton, sans le contour générique — il
   n'est pas une action sur les données, juste une navigation. Le `margin-top`
   remplace celui que le <h1> apporte sur les autres pages : ici c'est ce bouton
   qui suit le logo, et sans lui il s'y collait. */
/* La barre de tête de la vue détail : le retour à gauche, le partage à droite. Les marges
   verticales sont **passées du bouton à la rangée** en même temps que le partage est
   arrivé — laissées sur un enfant, elles auraient donné l'espacement du bloc depuis
   l'intérieur, et l'alignement des deux boutons aurait dépendu de la marge de l'un d'eux.
   `.bouton-retour` n'existe qu'ici, ce déplacement ne touche donc rien d'autre. */
.entete-detail {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* **`--espace-bloc` et non 1,25 rem** (22/08/2026, `D8`) : c'est le logo et la première zone
     de la page, exactement la relation que la vue **liste** des deux mêmes pages ouvre à 24 px.
     Une seule et même relation valait donc 24 en liste et 20 en détail — la revue des
     espacements du 21/08 avait corrigé les listes sans ouvrir les détails. */
  margin-top: var(--espace-bloc);
  margin-bottom: var(--espace-moyen);
}

.bouton-retour {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  padding: 0;
  border: none;
  background-color: transparent;
  color: var(--couleur-texte-discret);
  font-size: var(--texte-courant);
  cursor: pointer;
}

.bouton-retour:hover {
  background-color: transparent;
  color: var(--couleur-texte);
}

/* **Le mot rejoint l'en-tête au lieu d'occuper une carte à lui** (20/08/2026). Il portait
   `carte-stats` dans le HTML, donc un cadre entier pour une ligne de 0,9 rem : le texte le
   plus faible de la page pour la chose la plus personnelle qu'elle contienne. Il se range
   maintenant avec la date et « chez X », dont il fait partie — c'est l'identité de la sortie,
   pas une statistique.

   **Centré et sans filet, contrairement au mardi** : l'en-tête de cette page est centré, la
   raison est écrite avec la déclaration partagée plus haut. Pas de `font-size` ici, et c'est
   volontaire — il valait `--texte-courant`, et comme les deux sélecteurs pèsent le même id,
   celui-ci étant plus bas dans le fichier il aurait **gagné en silence** contre le palier de
   titre partagé. */
#detail-mot-hote {
  font-size: var(--texte-fort);
  text-align: center;
  margin: var(--espace-petit) 0 var(--espace-grand);
}

/* **Le mot d'une sortie libre passée s'édite sur place** (13/09/2026) : la zone devient un bouton pour
   ceux qui y étaient, et s'ouvre en champ au toucher. Le crayon, l'invitation et les boutons gardent
   les classes du mot de l'hôte sur la carte du mardi — rien n'est redéclaré ici que la mise en page. */
#detail-mot-hote[role="button"] {
  cursor: pointer;
}

/* Le paragraphe du mot suit l'en-tête centré : `.texte-mot-hote` porte un `text-align: left` fait pour
   une carte, même correction que `#detail-libre-mot`. */
#detail-mot-hote .texte-mot-hote {
  text-align: center;
}

#detail-mot-hote.mot-hote-edition {
  display: flex;
  flex-direction: column;
  gap: var(--espace-petit);
}

#detail-mot-hote[hidden] {
  display: none;
}

/* Le champ écrit à gauche dans un en-tête centré, et l'écart entre lui et les boutons est celui du
   `gap` : sa marge de formulaire l'aurait doublé. */
#detail-mot-hote textarea {
  margin: 0;
  text-align: left;
}

#detail-mot-hote .boutons-mot-hote {
  justify-content: center;
}

/* **Le programme, dans l'en-tête du détail d'une sortie libre.** Centré comme la date et le
   nom qu'il suit : sur cette page l'en-tête est centré, et `.texte-mot-hote` porte un
   `text-align: left` fait pour une carte alignée à gauche — il tranchait sur tout le reste.

   **Pas de filet d'identité ici, contrairement au mot d'un mardi** : il n'y a pas d'hôte sur
   ce type de sortie, donc personne à qui rattacher une voix. C'est un programme — où on va, à
   quelle heure — et non ce que quelqu'un a écrit au groupe. Même raisonnement que pour la
   carte de la liste, qui partage déjà la classe sans partager le traitement. */
/* `--espace-grand` en bas depuis le 22/08/2026 (`D8`) : 1,25 rem rendait 20 px, hors échelle. */
#detail-libre-mot {
  margin: var(--espace-moyen) 0 var(--espace-grand);
}

#detail-libre-mot .texte-mot-hote {
  text-align: center;
}

/* La signature se colle au badge qu'elle suit, et les deux au nom de la sortie : c'est le même
   groupe de métadonnées que sur la carte de la liste, seulement empilé faute de largeur
   contrainte ici. */
/* **Le `margin-bottom` est posé, il ne l'était pas** (22/08/2026). Cette règle ne déclarait que
   la marge haute, donc le `<p>` gardait le `1em` par défaut du navigateur en bas — et à
   `--texte-etiquette` (0,8 rem) ce `1em` vaut **12,8 px**, qui fusionnaient avec les 4 px de la
   météo en dessous pour rendre **13 px**, hors échelle. Relevé par l'audit du 21/08 et resté
   sans cause jusqu'ici : ce n'était pas un des sept `1.25rem`.

   **C'est le piège du `1em` que la feuille dit avoir déjà payé deux fois** — sur la date de
   l'en-tête et sur la rangée de métadonnées. Il se rejoue chaque fois qu'une règle pose *une*
   marge sur un `<p>` : l'autre reste celle du navigateur, et elle est invisible à la lecture.

   `--espace-moyen` plutôt que zéro : la signature ferme le groupe de l'en-tête, la météo ouvre
   le bloc suivant. Douze pixels au lieu de treize, donc l'œil ne voit rien changer. */
#detail-libre-signature {
  margin: 0;
}

/* **La rangée des métadonnées du détail** (04/09/2026) : elle porte le centrage propre à cet
   écran et l'espacement que ses deux mentions portaient chacune de leur côté. Les valeurs sont
   celles d'avant — collée au nom au-dessus, `--espace-moyen` en dessous parce qu'elle ferme
   l'en-tête quand le bloc suivant en ouvre un autre —, si bien que le rythme vertical ne change
   pas : ce qui change est que deux lignes deviennent un groupe. */
.meta-detail-libre {
  justify-content: center;
  margin: var(--espace-minuscule) 0 var(--espace-bloc);
}

/* **Les métadonnées se collent au nom, comme dans la carte de la liste** (04/09/2026) : elles le
   complètent, elles ne le suivent pas. `.hote-detail` pose `--espace-bloc` en bas — la gouttière
   dont ce titre a besoin dans le détail d'un mardi passé, où il est la **dernière** ligne de
   l'en-tête — et cette valeur l'emportait ici par fusion, séparant l'identité de ses propres
   métadonnées par 24 px.

   **Sur l'identifiant et non sur `.hote-detail`**, qui sert aussi le détail de l'historique : une
   classe partagée se corrige chez celui qui diverge. La coupure de 24 px n'est pas perdue, elle
   descend d'un cran — c'est la rangée ci-dessus qui la porte désormais, entre l'en-tête et le
   programme. Rapport obtenu : 4 px dans le groupe, 24 px pour en sortir. */
/* **Le nom mène, la date accompagne** (06/09/2026), comme sur la carte de la liste depuis le
   04/09. L'écart mesuré avant : le nom à 16 px et la date à 28,8 px ici, contre 19,2 et 12,8 dans
   la liste — le même objet s'annonçait dans deux ordres opposés, et le nom qu'on avait suivi
   devenait une petite ligne en arrivant.

   **La déclaration est celle de `.accroche-sortie-libre`**, et c'est voulu : c'est le même objet
   qui s'annonce, à deux endroits. Elle est recopiée plutôt que partagée par un sélecteur commun
   parce que les deux vivent dans des contextes de marge différents — la liste dans une carte
   `flex`, le détail dans un bloc où les marges fusionnent — et que le 04/09 a précisément séparé
   cette accroche du « chez X » pour cette raison. Un troisième partage la ramènerait dans une
   famille dont elle vient de sortir.

   **La marge haute est celle que portait le `<h1>`** : le nom est désormais la première ligne de
   l'en-tête, il reprend donc la gouttière qui l'en séparait. */
#detail-libre-titre {
  font-family: var(--police-affichage);
  font-style: italic;
  font-weight: 600;
  font-size: var(--titre-section);
  color: var(--couleur-accent-secondaire-texte);
  line-height: 1.25;
  margin: var(--espace-bloc) 0 0;
}

/* **La date passe en appui quand il y a un nom, et reprend la tête sinon.** Sélecteur
   d'adjacence et non `:has()` : le projet emploie déjà exactement cet idiome
   (`#detail-bikepark:not([hidden]) + #detail-mot-hote`), et il suffit ici puisque le nom précède
   la date dans le DOM. **`:not([hidden])` est indispensable** — le nom est écrit en dur et
   seulement masqué, donc sans lui la règle s'appliquerait aussi aux sorties sans nom, où la date
   doit rester l'accroche.

   **La déclaration est celle de `.date-appui-sortie-libre`**, pour la raison ci-dessus. Les
   propriétés d'affichage du `<h1>` sont toutes reprises, `.titre-statistiques` posant une police,
   un style et une taille qu'il faut défaire une à une. */
#detail-libre-titre:not([hidden]) + #detail-libre-date {
  /* `inherit` et non une variable : le projet n'en a qu'une pour la police d'affichage, le texte
     courant venant du `body`. C'est aussi ce que fait `.date-appui-sortie-libre`, qui ne déclare
     simplement aucune famille. */
  font-family: inherit;
  font-style: normal;
  font-weight: 400;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  line-height: 1.35;
  margin: var(--espace-minuscule) 0;
}

/* Le bouton d'annulation vit **en bas et seul**, hors de toute carte : c'est la seule action
   destructrice de l'écran, et la mettre à côté du sondage la rendrait voisine d'un geste
   qu'on fait sans réfléchir. La confirmation reste posée par la popup partagée. */
#detail-libre-supprimer {
  display: block;
  margin: var(--espace-bloc) auto 0;
}

#detail-libre-supprimer[hidden] {
  display: none;
}

/* **La météo du détail d'une sortie libre.** Elle ne partage pas la déclaration de
   `#meteo-sortie`, et c'est justifié : là-bas c'est une rangée `space-between` dans une carte
   alignée à gauche, ici c'est une ligne de l'en-tête **centré** de la page. Les deux groupes
   se toucheraient aux deux bords de l'écran pour rien. Ce qu'ils ont en commun — l'allure des
   groupes eux-mêmes — est déjà partagé par `.meteo-jour` et `.meteo-coucher`.

   `cursor: pointer` parce que la zone déplie les prévisions horaires ; sur un téléphone c'est
   l'apparition de la bande d'heures qui l'apprend, faute de survol. */
#detail-libre-meteo {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--espace-grand);
  margin: var(--espace-minuscule) 0 var(--espace-moyen);
  font-size: var(--texte-courant);
  cursor: pointer;
}

#detail-libre-previsions {
  margin-bottom: var(--espace-grand);
}

/* Bloc des invités du panneau de correction, séparé des lignes de membres par le
   même filet que celles-ci utilisent entre elles. */
.bloc-invites-correction {
  text-align: left;
  margin-top: var(--espace-moyen);
  padding-top: var(--espace-moyen);
  border-top: 0.5px solid var(--couleur-bordure);
}

.invites-correction {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-petit);
  margin: var(--espace-petit) 0;
}

/* Titre de section et crayon de correction sur une même ligne. Le crayon est
   ancré ici et non au coin de la carte : celle-ci porte aussi les compteurs et la
   météo, et une icône flottant en haut à droite aurait laissé croire qu'on édite
   tout le bloc. Le titre réutilise `.label-stats` plutôt que de recopier sa taille
   et sa couleur — c'est le même objet, un intitulé de section. */
.entete-presences {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  margin-top: var(--espace-grand);
}

/* Les groupes de présence sont une liste qu'on lit : alignés à gauche, contre le
   centrage par défaut de body. */
.groupe-presence {
  text-align: left;
  margin-top: var(--espace-grand);
}

.titre-groupe-presence {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
  margin: 0 0 var(--espace-minime);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* **Donnée-réponse** : elle répond directement à l'intitulé posé juste au-dessus (« Ont
   roulé et mangé »), d'où la graisse en plus de la taille et de la couleur. Les 1,6 px qui
   séparent `--texte-etiquette` de `--texte-courant` ne suffisaient pas à faire trancher les
   deux, la couleur portant presque seule la distinction (voir Typographie). */
.noms-presence {
  margin: 0;
  font-size: var(--texte-courant);
  font-weight: 600;
}

/* Une ligne de correction par membre : pseudo au-dessus, ses trois choix en
   dessous. Empilé plutôt que côte à côte — trois libellés et un pseudo sur la
   même ligne ne tiennent pas sur 320px. */
.ligne-correction {
  text-align: left;
  margin-top: var(--espace-moyen);
  padding-top: var(--espace-moyen);
  border-top: 0.5px solid var(--couleur-bordure);
}

.pseudo-correction {
  display: block;
  margin-bottom: var(--espace-minuscule);
  font-size: var(--texte-courant);
  font-weight: 600;
}

/* Grille 2×2 pour les quatre choix, et non une seule rangée de quatre : sur
   320px, quatre colonnes écraseraient « Roulé et mangé » (même problème que
   .reponses-admin et ses quatre boutons). `auto-fit` a été essayé puis écarté —
   il donne 3 colonnes vers 390px, largeur de téléphone très courante, ce qui
   laisse le quatrième bouton orphelin sur une seconde ligne. 2×2 est toujours
   équilibré, et `body` étant plafonné à 480px la largeur disponible ne varie
   que de 288 à 408px : un point de rupture n'apporterait rien (le projet n'en a
   aucun en largeur). `minmax(0, 1fr)` et non `1fr` : sans ça une colonne ne peut
   pas rétrécir sous la largeur de son contenu et déborde. */
.boutons-correction {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--espace-petit);
}

.boutons-correction button {
  min-width: 0;
  padding: clamp(0.35rem, 1.5vw, 0.5rem) clamp(0.2rem, 1vw, 0.5rem);
  font-size: var(--texte-bouton-adaptatif);
  overflow-wrap: break-word;
}

/* --- Suivi en direct (LiveTrack) ----------------------------------------- */

/* La classe des « boutons » qui sont des liens, née pour le suivi en direct : il mène chez
   Garmin, donc c'est un <a>. **Le Tricount d'une sortie libre la partage depuis le 12/09/2026**,
   pour la même raison. L'habillage vient de `.bouton-principal`, posé en seconde classe dans le HTML
   plutôt que recopié ici — mais un <a> n'hérite d'aucune règle de l'élément `button`,
   d'où ce que celui-ci donnait gratuitement : taille de texte, padding horizontal
   (`.bouton-principal` ne pose que le vertical), curseur, et le retrait du soulignement
   propre aux liens. */
.bouton-livetrack {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  padding-left: var(--espace-grand);
  padding-right: var(--espace-grand);
  font-size: var(--texte-courant);
  text-decoration: none;
  cursor: pointer;
}

/* Un `display` posé sur l'élément bat le `hidden` natif : même piège de spécificité que
   les popups et les panneaux repliables (voir docs/interface.md). */
.bouton-livetrack[hidden] {
  display: none;
}

/* **Le formulaire du Tricount partage cette declaration** (22/08/2026) : c'est le meme objet —
   un champ de lien et son bouton sur une ligne — et deux copies auraient diverge au premier
   ajustement (voir les Conventions de `CLAUDE.md`). Le `[hidden]` est pose pour les deux du
   meme geste juste en dessous, ce qui ferme d'avance le piege du masque qui ne masque pas.
   `.formulaire-livetrack` garde son nom : `livetrack.js` le cherche par `querySelector`. */
.formulaire-livetrack,
.formulaire-tricount {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
}

.formulaire-livetrack[hidden],
.formulaire-tricount[hidden] {
  display: none;
}

/* `min-width: 0` parce qu'un élément flex refuse par défaut de descendre sous la largeur
   de son contenu : sans lui, une URL longue pousserait « Partager » hors de la carte.
   Le `margin: 0` annule la gouttière verticale de la règle générique des champs, qui n'a
   pas lieu d'être sur une ligne. */
.formulaire-livetrack input,
.formulaire-tricount input {
  flex: 1;
  min-width: 0;
  margin: 0;
}

/* `.bouton-nu` est taillé pour un glyphe seul (`--icone-bouton`, 1.15 rem) ; celui-ci
   porte un libellé. À cette taille, « Retirer le suivi » serait le texte le plus gros de
   la carte, devant le lien qu'il accompagne. Seule la taille revient au palier des
   contrôles — le reste de l'habillage nu est exactement ce qu'on veut ici. */
/* **Le retrait du Tricount partage cette déclaration depuis le 12/09/2026** : même objet, un bouton
   nu qui porte un libellé. Il ne la partageait pas, et sortait donc à 18,4 px — le texte le plus
   gros de sa carte, exactement le défaut décrit juste au-dessus. */
#bouton-retirer-livetrack,
#bouton-retirer-tricount {
  font-size: var(--texte-courant);
}

/* --- Commentaires (carte de sortie et anciennes sorties) ----------------- */

/* Même habillage que .carte-stats : c'est un bloc qu'on lit, aligné à gauche
   contre le centrage par défaut de body.

   **Les quatre zones en carte partagent cette déclaration au lieu de la
   recopier** (voir la convention dans CLAUDE.md) : suivi en direct, commentaires, photos
   et trace se suivent dans la même colonne, une dérive sur l'une se verrait aussitôt
   contre ses voisines. Elles n'ont d'ailleurs jamais rien eu de propre ici — les deux
   premières portaient déjà deux copies identiques, la troisième en aurait fait trois.
   `.carte-livetrack` est la seule des quatre à être une zone d'action et non de lecture :
   c'est le bouton qu'elle contient qui porte l'emphase, pas sa boîte. */
.carte-commentaires,
.carte-photos,
.carte-trace,
.carte-livetrack {
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-radius: 14px;
  padding: var(--espace-grand) 1.25rem;
  /* **Le même écart que toutes les autres cartes** depuis le 21/08/2026. Ces quatre-là
     portaient `1.5rem` en haut, censé marquer la rupture entre la sortie et ce qui vient
     après — mais posé sur les **quatre**, il s'appliquait aussi *entre* elles : photos et
     commentaires étaient séparés comme deux zones alors qu'ils sont deux cartes du même
     groupe. Mesuré, ça donnait 16 ou 24 px selon la voisine, sans qu'aucune intention ne
     décide laquelle.

     **La règle « serré dans un groupe, large entre deux groupes » n'est pas abandonnée pour
     autant** : elle demande une rupture *choisie*, et c'est justement ce que cette marge
     n'était pas. Quand on voudra la marquer, ce sera sur la première carte du groupe et sur
     elle seule. */
  margin: 0 0 var(--espace-grand);
  text-align: left;
}

.carte-commentaires[hidden],
.carte-photos[hidden],
.carte-trace[hidden],
.carte-livetrack[hidden] {
  display: none;
}

/* Même remarque pour les trois intitulés, qui se lisent en colonne l'un sous
   l'autre : « Commentaires », « Photos », « La trace ». */
/* `.label-tricount` les rejoint le 22/08/2026 : c'est le même objet, l'intitulé d'une zone de
   la vue détail, une icône et un mot. Il a été un `<a>` jusqu'au 12/09/2026 ; redevenu un titre,
   il n'a plus à lui que son masquage (voir `.label-tricount[hidden]`). */
/* `.label-rappel` les rejoint le 04/09/2026, pour la raison qui vaut pour `.label-tricount` :
   c'est le même objet, l'intitulé d'une zone de la vue détail. Il ne recopie donc rien — et sa
   carte se rend désormais comme celle du Tricount, qu'elle touche et dont elle partage le geste. */
.label-commentaires,
.label-photos,
.label-trace,
.label-tricount,
.label-rappel {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-petit);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  /* **Posé explicitement depuis le 29/08/2026** (`RC-UX-049`) : ces intitulés sont passés
     de `<p>` à `<h2>` pour donner un plan de document aux lecteurs d'écran, et un titre
     hérite du gras du navigateur. La hiérarchie visuelle a été dessinée et mesurée ; c'est
     la sémantique qui manquait, pas le poids. */
  font-weight: normal;
}

.commentaire {
  margin-bottom: var(--espace-moyen);
  padding-bottom: var(--espace-moyen);
  border-bottom: 0.5px solid var(--couleur-bordure);
}

/* Le dernier ne porte pas de trait : le formulaire en dessous en a déjà un. */
.commentaire:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
}

.entete-commentaire {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-minime);
}


/* Repoussé à droite, pour ne pas coller à la date. Réutilise .action-invite pour
   la forme du bouton, seule la position change. */
.retirer-commentaire {
  margin-left: auto;
}

.texte-commentaire {
  margin: 0;
  font-size: var(--texte-courant);
  overflow-wrap: break-word;
}

/* **Les quatre notes d'état vide partagent cette déclaration** (voir la convention dans
   CLAUDE.md), au même titre que les cartes qui les contiennent, juste au-dessus :
   « Pas de commentaire », « Pas de photo », l'invite de dépôt d'une trace et celle du
   suivi en direct disent toutes la même chose — cette zone n'a rien à montrer — et
   doivent donc se lire pareil (l'état vide est le quatrième traitement typographique,
   voir docs/interface.md). Elles en étaient à trois copies identiques, la quatrième en
   aurait fait quatre. Seule la gouttière basse les distingue, d'où l'exception qui suit
   plutôt qu'un retour à quatre déclarations complètes. */
.note-commentaires,
.note-photos,
.note-trace,
.note-livetrack {
  margin: 0;
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

/* Ces deux-là annoncent un formulaire posé dessous ; les deux autres terminent leur zone. */
.note-trace,
.note-livetrack {
  margin-bottom: var(--espace-petit);
}

/* **Un emoji ne s'incline pas**, et c'est une règle générique, pas un correctif local : il
   n'existe aucune variante italique d'un glyphe emoji, le moteur se contente donc de le
   pencher en le déformant. Partout où un emoji vit dans un texte en italique — les états
   vides, aujourd'hui — il faut le sortir de la pente. À réutiliser telle quelle si le cas
   se présente ailleurs. */
.emoji {
  font-style: normal;
}

.note-commentaires[hidden],
.note-photos[hidden],
.note-trace[hidden],
.note-livetrack[hidden] {
  display: none;
}

/* **Le même traitement, pour les états vides qui *sont* une carte** et non une note au
   fond d'une carte : voyage sans édition, aucune sortie proposée, dépôt expiré, partage
   sans lien. Ils ne portaient que `.carte-stats`, donc héritaient du 1 rem du `body` en
   couleur pleine — le texte le plus gros de l'écran pour annoncer qu'il n'y a rien à voir,
   exactement l'inverse de ce que dit la règle (voir Typographie dans docs/interface.md).
   **Pas de `margin` ici**, à la différence des notes ci-dessus : sur ces éléments la marge
   est celle de la carte, l'écraser collerait les blocs entre eux. */
/* `.resultat-sonde` a été retirée le 17/08/2026 (`A23`) : c'était le reliquat de la sonde de
   codecs, dont le retrait est noté comme fait au chantier F de docs/roadmap.md — le JS et le
   HTML étaient partis, la règle était restée. Son commentaire s'était en prime intercalé
   entre celui de `.reaction-passive` et sa règle, si bien qu'il documentait le mauvais
   sélecteur. Les deux sont recollés ci-dessous. */

/* Le pouce de son propre contenu : même allure, mais ce n'est plus un bouton — c'est un
   `<span>`, donc il lui faut la mise en ligne que l'élément `button` donnait, et surtout
   pas le curseur qui promettrait un geste (voir reactions.js). */
.reaction-passive {
  display: inline-flex;
  align-items: center;
  cursor: default;
}


/* Le champ au-dessus, le bouton en dessous aligné à droite : côte à côte, un
   textarea de deux lignes et un bouton ne s'alignent pas proprement. Le trait
   sépare la saisie de la lecture, posé sur l'élément qui suit comme partout. */
/* `position: relative` pour ancrer le bouton d'envoi **dans** le champ, langage de
   toutes les messageries. Le bouton plein qui vivait dessous pesait autant que le
   commentaire qu'il sert, sur un écran large de 480px. */
.formulaire-commentaire {
  position: relative;
  margin-top: var(--espace-moyen);
  padding-top: var(--espace-moyen);
  border-top: 0.5px solid var(--couleur-bordure);
}

/* Annule le margin vertical de la règle générique des champs. Le `padding-right`
   réserve la place du bouton : sans lui le texte passerait dessous.

   **Pas de `font-size` ici, et c'est la règle des 16px** : ce champ était à
   `0.9rem`, soit 14.4px, ce qui fait zoomer iOS Safari à chaque mise au point —
   exactement ce que le commentaire de la règle `button` plus haut met en garde de
   ne pas faire. Il hérite donc du `1rem` générique des `textarea`. Un bouton n'a
   pas cette contrainte, un champ de saisie oui. */
.formulaire-commentaire textarea {
  margin: 0;
  min-height: 3rem;
  padding-right: 3rem;
}

/* **Sur le conteneur et non sur l'identifiant** (27/08/2026) : la visionneuse porte un second
   formulaire de commentaire depuis les commentaires de photo, et une règle par id en aurait
   fait deux copies — celle qui aurait dérivé la première, comme l'a fait `.etiquette-bouton`
   avant elle. `.formulaire-commentaire` ne contient qu'un champ et ce bouton. */
.formulaire-commentaire button {
  position: absolute;
  right: 0.4rem;
  bottom: 0.4rem;
}

/* ---------- Photos de sortie ---------- */

/* L'habillage de `.carte-photos` et de `.label-photos` vit avec celui des
   commentaires et de la trace, en une seule déclaration partagée (voir la section
   Commentaires ci-dessus). */

/* `auto-fill` avec un minimum de 5rem : trois colonnes sur les 288px les plus
   étroits, quatre dès 390px. Contrairement aux boutons de correction, un orphelin
   sur la dernière ligne n'a rien de gênant ici — une galerie n'a pas à être
   équilibrée, elle a le nombre de photos qu'elle a. */
/* `margin-bottom` depuis que la tuile « + » vit dans la grille : celle-ci ne se
   replie plus jamais (la règle `:empty` qui s'en chargeait est devenue caduque),
   et rien ne la séparait donc du texte d'état juste en dessous. Même gouttière que
   `.label-photos` au-dessus, pour garder le rythme de la carte. */
.grille-photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
  gap: var(--espace-petit);
  margin-bottom: var(--espace-petit);
}

/* L'intertitre d'une journée dans la galerie fusionnée d'un voyage. **Il traverse la grille
   entière** plutôt que d'occuper une case : `1 / -1` va de la première ligne de colonne à la
   dernière, quel que soit le nombre de colonnes que `auto-fill` a décidé. C'est ce qui permet
   de grouper sans découper la galerie en sous-grilles — les vignettes gardent le même gabarit
   d'un groupe à l'autre.

   `margin-top` sur les suivants seulement : le premier groupe suit déjà l'intitulé « Photos ».
   Reprend `--texte-etiquette` et la couleur discrète des autres intitulés de section. */
/* **Aucune marge basse**, et c'est mesuré : la grille pose déjà son `gap` de 8 px sous
   l'intertitre. Les 2 px qu'il portait s'y ajoutaient et donnaient **10 px**, une valeur hors
   échelle — trouvée par la sonde des écarts, pas à l'œil. L'intertitre et ses photos sont un
   groupe : le `gap` seul les tient. */
.intertitre-journee {
  grid-column: 1 / -1;
  margin: var(--espace-petit) 0 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.intertitre-journee:first-child {
  margin-top: 0;
}

.intertitre-journee a {
  color: inherit;
  text-decoration: underline;
}

/* Vignettes carrées : les photos ont des rapports variables, et une grille aux
   hauteurs inégales serait illisible. `object-fit: cover` rogne au centre plutôt
   que de déformer. Les dimensions réelles restent portées par les attributs
   width/height de l'<img>, dont la visionneuse se sert.

   La tuile d'ajout **partage ce gabarit au lieu de le recopier** : elle vit dans
   la même grille, un carré de travers s'y verrait immédiatement, et deux règles
   séparées finiraient par diverger (voir la convention dans CLAUDE.md). Ne restent
   propres à chacune que la façon de se remplir. */
.vignette-photo,
.tuile-ajout-photo {
  border-radius: 8px;
  aspect-ratio: 1;
  cursor: pointer;
}

.vignette-photo {
  padding: 0;
  border: none;
  /* Ancre la pastille de réaction dans le coin de la case, pas dans celui de la grille. */
  position: relative;
  overflow: hidden;
  background-color: var(--couleur-surface-neutre);
}

/* Bordure tiretée : elle dit « emplacement à remplir » plutôt que « photo », ce
   qu'un cadre plein ne distinguerait pas au milieu des vignettes. Le centrage est
   explicite parce qu'un <label> n'a rien du comportement d'un bouton. */
.tuile-ajout-photo {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px dashed var(--couleur-accent);
  color: var(--couleur-texte-discret);
}

.tuile-ajout-photo .ti {
  font-size: var(--icone-grande);
}

.vignette-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.depot-photos {
  margin-top: var(--espace-moyen);
}

.depot-photos[hidden] {
  display: none;
}

/* Plein écran plutôt que `.popup`, qui est plafonné à 320 px : une photo a besoin de toute
   la place.

   **Un `<dialog>` depuis le 15/08/2026**, comme les popups de confirmation (voir
   `popups.js`). Plus de `z-index` : un dialogue modal vit dans la couche supérieure, donc
   la confirmation de retrait — modale elle aussi et ouverte après — s'empile correctement
   par-dessus sans qu'aucun palier soit à choisir.

   `background: transparent` est indispensable : le navigateur peint un `dialog` en
   `canvas`, ce qui masquerait le `::backdrop` posé juste dessous et donnerait un aplat
   opaque à la place du fond sombre. Même famille que `border`, `margin` et `color`, qui
   annulent tous les trois la feuille de style du navigateur. */
.visionneuse-photo {
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  border: none;
  padding: var(--espace-grand);
  background: transparent;
  color: inherit;
  /* La photo sort de l'écran quand on la fait glisser : sans coupure, ce débordement
     ouvrirait un défilement horizontal sur la page. `clip` et non `hidden` — c'est ce qui
     permet de ne couper **que** cet axe, là où `hidden` forcerait l'autre à devenir
     défilable et enfermerait une photo portrait trop haute dans une zone à faire défiler. */
  overflow-x: clip;
}

/* `display` sous `[open]` **et pas ailleurs** : le navigateur masque un `<dialog>` fermé
   par `display: none`, qu'une valeur écrite hors de ce sélecteur remplacerait — la
   visionneuse serait alors visible en permanence. Même piège que les popups. */
.visionneuse-photo[open] {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--espace-moyen);
}

/* Reprend exactement le voile supprimé : même couleur, même opacité. */
.visionneuse-photo::backdrop {
  background-color: rgba(0, 0, 0, 0.4);
}

/* Cadre qui épouse la photo, pour que la croix se pose au coin de **l'image**. Il
   remplace l'image comme enfant flex de la visionneuse, d'où le `min-height: 0`
   qui migre ici pour la même raison que ci-dessous : sans lui, un enfant flex ne
   descend pas sous sa taille de contenu et une photo portrait déborderait par le
   bas. `display: flex` supprime l'espace sous l'image, qu'un conteneur en ligne
   réserverait pour la ligne de base. */
.cadre-photo {
  position: relative;
  display: flex;
  min-height: 0;
}

/* `min-height: 0` : sans lui, un enfant flex ne descend pas sous sa taille de contenu
   et une photo portrait déborderait de l'écran par le bas.

   **La vidéo partage cette déclaration, elle n'en a pas une copie** — c'est la convention du
   projet (voir CLAUDE.md), et son absence est précisément le défaut corrigé le 15/08/2026 :
   la règle ne visait que `img`, si bien que le `<video>` s'affichait à la taille brute de ses
   attributs `width`/`height`, que `photos.js` pose aux dimensions réelles du fichier. Une
   vidéo de 720×1280 débordait donc de l'écran — rognée en hauteur, décentrée en largeur — et
   ce dans les deux orientations, puisque aucun plafond ne s'y appliquait.

   `width: auto` et `height: auto` sont ce qui neutralise ces attributs ; les garder sur
   l'élément reste utile, ils donnent le rapport d'aspect avant que les métadonnées arrivent
   et évitent que la mise en page saute.

   **Le sélecteur vise le cadre et non la visionneuse entière depuis le 27/08/2026**, et c'est
   un défaut signalé à l'usage qui l'a exigé : « si j'ouvre la dernière photo, mon avatar
   apparaît en taille d'origine ». La visionneuse porte le fil des commentaires depuis la
   tranche 14, donc `.visionneuse-photo img` attrapait les **têtes des auteurs** — 0-1-1 contre
   0-1-0 pour `.avatar`, si bien que `width: auto` gagnait et qu'un avatar s'affichait à ses
   256 px. La leçon est celle de tout sélecteur écrit sur une balise : **il attrape ce qui
   n'existait pas le jour où on l'a écrit**. `.cadre-photo` ne contient que le média regardé. */
.cadre-photo > img,
.cadre-photo > video {
  max-width: 100%;
  min-height: 0;
  /* **Le `100%` est le filet, et c'est lui qui rend le débordement impossible** (20/09/2026,
     signalé à l'usage une seconde fois : « la photo rogne sur les réactions / commentaires »).
     `80vh` est un plafond, jamais une place libre — `photos.js` calcule la place réelle, mais il
     la calculait **avant** que le fil de commentaires ait la sienne, si bien que l'image, dont la
     hauteur est explicite, sortait de son propre cadre par le bas. Mesuré : 78 px sur la barre
     avec un seul commentaire.
     Le cadre, lui, était déjà juste — c'est un enfant flex à `min-height: 0`, donc flexbox le
     comprimait correctement. Borner l'image **à son cadre** suffit donc à fermer la famille
     entière, y compris le chemin qu'on oubliera demain. Vérifié : le pourcentage se résout bien
     contre la hauteur d'un flex item comprimé, sans hauteur explicite sur le cadre.
     **Conséquence à connaître avant d'y toucher** : `getComputedStyle` rend alors la chaîne
     `min(732px, 100%)` et non un nombre, que `espaceDisponible` lit — la fonction le dit et s'en
     accommode, c'est écrit là-bas. La règle du plein écran juste en dessous écrase ce `min()` par
     une valeur simple, et n'a rien à border : elle masque la barre et le fil. */
  max-height: min(80vh, 100%);
  /* **La taille vient de `photos.js`, calculée depuis les dimensions en base** (03/09/2026), et
     le repli `auto` est ce qui vaut hors visionneuse. Avec `auto`, la taille venait de ce qui
     était **chargé** : la vignette de secours ne fait que quelques centaines de pixels, donc la
     photo s'affichait petite puis grandissait d'un coup à l'arrivée du plein format — un saut
     qui, en feuilletant, tombait pendant la transition et la hachait. */
  width: var(--largeur-rendue, auto);
  height: var(--hauteur-rendue, auto);
  border-radius: 10px;
  object-fit: contain;
  /* **Le média suit le doigt**, et c'est `photos.js` qui pose la valeur — jamais la règle,
     comme `stats.js` pour la hauteur d'une barre. La valeur par défaut compte autant que la
     déclaration : sans elle, une photo affichée avant le premier glissement n'aurait aucune
     transformation et sauterait à zéro au premier geste.

     **Deux axes depuis le 02/09/2026** : l'horizontal feuillette, le vertical ferme. Le geste
     retenu est celui d'Instagram et de Google Photos, et `closeOnVerticalDrag` est actif par
     défaut dans PhotoSwipe — c'est un réflexe acquis, qu'on essayait ici sans réponse.

     **La rotation est en bout de chaîne, et c'est tout ce qui la rend sûre** (03/09/2026) :
     les transformations s'appliquent de droite à gauche, donc `rotate` couche l'image sur
     elle-même **avant** que `translate` ne la déplace. Le glissement du feuilletage reste
     ainsi dans le repère de l'**écran** — vers la gauche fait sortir la photo par la gauche,
     couchée ou non. Écrite en tête, elle aurait fait partir la photo vers le haut, et le
     geste le plus fréquent de la visionneuse serait devenu incompréhensible une fois sur
     deux. `--pivot-echelle` est le facteur qui remplit l'écran une fois l'image tournée :
     il est séparé de `--zoom` parce que le pincement doit pouvoir s'y ajouter sans l'écraser.

     **`--zoom-x` et `--zoom-y` sont au décalage du zoom ce que `--pivot-echelle` est à son
     facteur : une variable de plus pour un rôle de plus** (04/09/2026). Le déplacement dans une
     image agrandie s'écrivait dans `--glissement`, qui déplace **la bande entière** — les deux
     voisines la lisent aussi, à `∓ 100vw`. Examiner un détail vers un bord faisait donc entrer
     la photo d'à côté dans l'écran, dès le **premier pixel** de décalage : la voisine a son bord
     à `centre − demi-largeur + --glissement`. Signalé à l'usage — « on se retrouve à zoomé sur
     2 bouts de photos différentes en même temps ».

     **Elles ne sont déclarées que sur la photo courante**, et c'est tout ce qui rend la
     séparation sûre : une voisine n'a plus aucun moyen de lire le décalage du zoom. Posées
     **avant** le `scale`, donc en pixels d'écran et non multipliées par le facteur — ce que
     `bornerDecalage` suppose pour calculer ses butées. */
  transform: translate(var(--glissement, 0px), var(--glissement-y, 0px))
             translate(var(--zoom-x, 0px), var(--zoom-y, 0px)) scale(var(--zoom, 1))
             rotate(var(--pivot, 0deg)) scale(var(--pivot-echelle, 1));
}

/* **La transition ne vit que sous cette classe, et c'est tout le mécanisme** : pendant le
   glissement elle est retirée, pour que la photo colle au doigt sans retard ; au relâchement
   elle est remise, et c'est elle qui termine le mouvement ou ramène la photo en place quand
   le geste est abandonné.

   **La courbe et la durée ont été reprises le 03/09/2026**, après trois essais à l'écran : « le
   relâché est encore assez violent, ça manque de rondeur ». **La durée n'est plus fixe** :
   `photos.js` la rapporte à ce qu'il reste à parcourir, sans quoi une photo lâchée à mi-course
   mettrait autant de temps qu'une autre à peine poussée — et c'est ce décalage entre la vitesse
   du doigt et celle de l'animation qui se sent. Le trajet complet vit dans `DUREE_BASCULE_MAX`,
   seul nombre à toucher pour régler la sensation.

   **La borne du quart de seconde a sauté, et il fallait qu'elle saute** : elle datait du temps où
   changer de photo était un remplacement sec, où toute durée était effectivement une attente. Un
   mouvement qui prolonge le geste se lit autrement — l'essentiel du trajet se fait dans les
   premières fractions, et la fin qui se pose doucement est justement ce qu'on appelle de la
   rondeur. La courbe est une décélération exponentielle, la plus marquée des familles usuelles.

   **Un premier réglage à 0,34 s était passé inaperçu** : cinquante millisecondes d'écart sur un
   geste courant ne se sentent pas. Un réglage de confort se change franchement ou pas du tout. */
.cadre-photo-anime > img,
.cadre-photo-anime > video {
  transition: transform var(--duree-bascule, 0.4s) cubic-bezier(0.16, 1, 0.3, 1);
}

/* **Oubli rattrapé le jour même** : la barre de chargement respectait la préférence de
   mouvement réduit, pas ce glissement — le geste le plus fréquent de la visionneuse. Sans
   transition, la photo se pose directement là où elle doit être : rien n'est perdu de
   l'information, seul le trajet disparaît. */
@media (prefers-reduced-motion: reduce) {
  .cadre-photo-anime > img,
  .cadre-photo-anime > video {
    transition: none;
  }
}

/* **Les deux voisines du feuilletage** (03/09/2026). Elles sont sœurs de la photo courante, donc
   elles héritent de son gabarit — c'est voulu : les trois doivent se rendre **exactement** de la
   même façon, puisque chacune deviendra la courante à son tour. On ne surcharge que ce qui
   distingue une voisine : elle sort du flux, se centre sur le cadre, et se rejette d'une largeur
   d'écran.

   **Le sélecteur est qualifié par le cadre** pour passer devant `.cadre-photo > img` (0-2-1
   contre 0-1-1), qui porte le placement et le `transform` de la photo qu'on regarde.

   **Elles coupent l'héritage du pivot** : `--pivot` est posé sur le cadre pour la photo courante,
   or chaque voisine a sa propre orientation. Redéclarer les valeurs neutres ici fait qu'un oubli
   du script laisse une voisine droite — jamais couchée par erreur. */
.cadre-photo > img.voisine-photo {
  --pivot: 0deg;
  --pivot-echelle: 1;
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  /* Rien à viser : les gestes appartiennent à la photo courante. */
  pointer-events: none;
}

.cadre-photo > img.voisine-avant {
  transform: translateX(calc(var(--glissement, 0px) - 100vw)) rotate(var(--pivot)) scale(var(--pivot-echelle));
}

.cadre-photo > img.voisine-apres {
  transform: translateX(calc(var(--glissement, 0px) + 100vw)) rotate(var(--pivot)) scale(var(--pivot-echelle));
}


/* **`none` depuis que le zoom est à nous** (02/09/2026), et c'est un échange assumé. Le
   pincement natif marchait, mais il agrandissait la **page** : la barre, le compteur et le fil
   grossissaient avec la photo, et sur iOS un élément `position: fixed` s'y comporte mal. Le
   reprendre impose de tout reprendre — `touch-action` étant une liste blanche, on ne peut pas
   garder le pincement du navigateur et lui retirer le reste.

   **Ce qu'on doit désormais à l'utilisateur** : le pincement, le double-appui, le déplacement
   dans l'image agrandie. Ce sont les trois gestes que `photos.js` implémente, et c'est
   exactement ce que fait PhotoSwipe pour la même raison.

   Sur la vidéo, rien de tout ça : ses commandes natives gardent leurs gestes. */
.cadre-photo > img {
  touch-action: none;
}

/* **L'histoire de cette ligne mérite d'être gardée, elle s'est retournée deux fois en un
   jour.** Le pincement natif fonctionnait depuis toujours, le `meta viewport` ne l'ayant
   jamais bridé. Un `touch-action: pan-y` ajouté pour fiabiliser le feuilletage l'a supprimé
   sans rien signaler — **`touch-action` est une liste blanche**, ce qui n'y figure pas est
   refusé au navigateur. Rétabli en `pan-y pinch-zoom` le jour même, puis remplacé par `none`
   quand le zoom est devenu le nôtre. **Une propriété ajoutée pour gagner un geste peut en
   retirer un autre, sans erreur et sans trace.** */


/* Fond propre plutôt que de compter sur le voile : celui-ci n'est qu'à 40 %
   d'opacité, donc la page reste visible derrière — un texte posé dessus serait
   lisible en thème sombre et se perdrait en thème clair. Avec sa propre surface, la
   barre tient dans les trois thèmes sans rien supposer de ce qu'il y a dessous. */
.barre-visionneuse {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  padding: var(--espace-petit) var(--espace-moyen);
  border-radius: 20px;
  background-color: var(--couleur-surface-teintee);
}

/* Le fil d'une photo, sous la barre (27/08/2026). **Son fond et son arrondi sont ceux de la
   barre, et c'est délibéré** : les deux flottent au-dessus de la même image et doivent se
   lire comme un seul bloc d'interface, pas comme deux surfaces différentes.

   `max-height` **en `vh` et non en `rem`** : ce qu'il faut borner ici est la part d'écran
   prise à la photo, pas une hauteur de texte. `min-height: 0` laisse le flex le comprimer
   davantage sur un écran court — sans lui, un enfant flex ne descend pas sous sa taille de
   contenu et le fil pousserait l'image hors du cadre. */
.commentaires-photo {
  width: min(100%, 34rem);
  min-height: 0;
  /* **Deux plafonds, et le second est un plancher pour la photo** (20/09/2026). `30vh` borne le
     fil dans l'absolu ; `--plafond-fil`, posé par `photos.js`, le borne à ce que la photo lui
     laisse une fois sa moitié de hauteur réservée. Sans lui, la garantie « la photo ne descend
     pas sous la moitié de l'écran » ne tiendrait qu'à une coïncidence numérique entre `30vh` et
     la hauteur de la barre — vraie aujourd'hui sur les cinq tailles mesurées, et fausse le jour
     où l'un des deux nombres change. Le repli vaut `30vh` : hors visionneuse ouverte, rien ne
     pose la variable, et le fil retrouve son seul plafond absolu. */
  max-height: min(30vh, var(--plafond-fil, 30vh));
  /* **Sa hauteur est figée le temps qu'on change de photo** (03/09/2026), et le repli `auto` est
     l'état normal. Les trois blocs de la visionneuse sont centrés verticalement ensemble : quand
     ce fil est vidé puis rechargé, sa hauteur tombe et remonte, et le groupe entier se recentre —
     donc **la photo se déplace**, deux fois, la seconde à l'arrivée de la réponse du serveur.
     C'est ce qui rendait le feuilletage rugueux bien après que le glissement fut devenu fluide.
     Voir `figerHauteurFil` dans `photos.js`.

     **`box-sizing` est indispensable ici, et son absence gonflait le fil à chaque changement**
     (03/09/2026, signalé à l'usage : « une bande plus épaisse en dessous, puis ça bascule à la
     bonne dimension »). Le projet **n'a pas de `box-sizing` global** — trois autres règles s'y
     déclarent déjà explicitement, pour cette raison exacte. `figerHauteurFil` mesure avec
     `offsetHeight`, qui compte **bordure et `padding` compris** ; en `content-box`, réinjecter
     ce nombre dans `height` **rajoute le `padding` par-dessus**. Mesuré : 80 px naturels
     deviennent 96 px une fois figés, et le dégel — qui remesure puis refige — en faisait 112.
     Le fil enflait donc de 32 px à chaque photo avant de revenir, **même entre deux photos sans
     le moindre commentaire**, là où le mécanisme est justement censé ne rien faire.

     **La leçon dépasse cette règle** : dès qu'un script mesure une hauteur pour la réécrire
     dans `height`, les deux bouts doivent parler de la même boîte. Sans `box-sizing` global,
     ce n'est jamais le cas par défaut, et l'écart vaut exactement le `padding` — donc il
     grandit à chaque aller-retour au lieu de se voir d'un coup. */
  box-sizing: border-box;
  height: var(--hauteur-fil, auto);
  overflow-y: auto;
  /* Le fil est le seul conteneur qui défile dans la visionneuse, donc le seul par où le
     geste pouvait s'échapper : arrivé au bout des commentaires, le navigateur poursuit le
     défilement sur l'ancêtre — c'est le *scroll chaining*, et c'est exactement ce que les
     membres ont vu bouger derrière la photo. `contain` arrête la propagation sans interdire
     le défilement du fil lui-même, et supprime au passage le rebond élastique iOS en bout
     de course. Ceinture et bretelles avec le verrouillage de `body` : celui-ci ne dépend
     d'aucune prise en charge de `:has()`. */
  overscroll-behavior: contain;
  padding: var(--espace-petit) var(--espace-moyen);
  border-radius: 20px;
  background-color: var(--couleur-surface-teintee);
  /* **`body` centre tout**, et c'est `.carte-commentaires` qui rétablit l'alignement du fil
     d'une sortie. Sans cette ligne, les mêmes commentaires se lisaient centrés ici et à
     gauche là-bas — deux rendus pour un même bloc. Vu à l'écran, invisible dans le code. */
  text-align: left;
}

/* **Le trait de séparation change de côté ici, et ce n'est pas cosmétique** : sur une carte
   de sortie il est porté par le formulaire, qui suit toujours quelque chose. Dans la
   visionneuse le fil est vide la plupart du temps, et ce trait dessinait alors une ligne
   au-dessus du champ sans rien au-dessus d'elle.

   **Porté par la liste et conditionné à `:not(:empty)`**, il n'apparaît donc que quand il a
   quelque chose à séparer. Le porter par le formulaire aurait demandé `:has()`, la liste
   restant un élément vide et non un élément absent. */
/* Le dégel : la hauteur retrouvée est rendue en glissant plutôt qu'en sautant, une fois les
   nouveaux commentaires arrivés — donc hors du moment critique. La classe n'est portée que le
   temps de cette transition. */
.fil-degel {
  transition: height 0.2s ease-out;
}

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

.commentaires-photo .formulaire-commentaire {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

#liste-commentaires-photo:not(:empty) {
  margin-bottom: var(--espace-petit);
  padding-bottom: var(--espace-petit);
  border-bottom: 0.5px solid var(--couleur-bordure);
}

.signature-photo {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  margin-right: var(--espace-moyen);
}

/* La journée d'où vient un média, dans la galerie fusionnée d'un voyage. **Elle vit dans la
   signature** et hérite donc de sa taille : `.barre-visionneuse` porte déjà quatre enfants,
   un cinquième s'y étalerait.

   Souligné, contrairement aux liens habillés en boutons du projet : c'est un lien **dans une
   phrase**, où rien d'autre ne dirait qu'il est cliquable. La couleur d'accent seule ne suffit
   pas — c'est le critère WCAG 1.4.1, l'information ne doit pas reposer sur la couleur. */
.lien-journee-photo {
  color: var(--couleur-accent);
  text-decoration: underline;
}

/* Boutons réduits à leur icône : `line-height: 0` supprime le talon que la boîte
   de ligne du texte laisserait sous le glyphe, sans quoi la pastille paraît
   décentrée verticalement alors que le padding est symétrique. */
.bouton-icone {
  padding: var(--espace-petit) var(--espace-petit);
  line-height: 0;
}

.bouton-icone .ti {
  font-size: var(--icone-bouton);
}

/* La croix se pose au coin haut-droit **de la photo**, dans `.cadre-photo` qui
   l'épouse — elle était auparavant calée sur le coin de l'écran, où elle flottait
   loin de l'image dès qu'une photo était étroite ou peu haute. Elle reste sortie du
   flux, sans quoi elle s'empilerait au-dessus de l'image au lieu de se poser
   dessus. Elle porte son propre fond pour la même raison que la barre du bas — le
   voile n'étant qu'à 40 % d'opacité, la page reste visible derrière et un contour
   seul se perdrait selon le thème.
   Elle ne reste qu'un rappel : le bouton Retour du téléphone, la touche Échap et un appui
   à côté de la photo sortent aussi. **Ce commentaire annonçait « le voile » et « un
   glissement », qui n'existaient ni l'un ni l'autre** — le voile a disparu en migrant vers
   `<dialog>`, et rien n'a jamais fermé au glissement. Corrigé le 02/09/2026, en même temps
   qu'a été ajouté l'appui à côté, qui est ce que le voile promettait. */
.fermer-visionneuse {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  padding: var(--espace-petit);
  line-height: 0;
  border-radius: 50%;
  background-color: var(--couleur-surface-teintee);
}

/* Posé à droite au-dessus de la grille, à la manière d'une action de section : le geste est
   rare — on prend l'album d'un mardi une fois — et il ne doit pas concurrencer du regard la
   tuile d'ajout, qui est, elle, le geste courant. */
.archive-photos {
  display: block;
  margin: 0 0 var(--espace-petit) auto;
}

/* **Le quatrième faux masqué du projet** (04/09/2026, signalé à l'usage : « le bouton pour
   enregistrer des photos qui apparaît alors qu'il y a aucune photo »). `photos.js` posait bien
   `archive.hidden = photos.length === 0`, et le bouton restait là : le `display: block`
   ci-dessus l'emporte sur le `[hidden] { display: none }` du navigateur — **à spécificité
   égale**, les deux valant (0,1,0), et la feuille de l'auteur passe devant celle de l'agent.
   Mesuré : `hidden=true`, `display: block`, 34 × 34 px bien visibles.

   **C'est l'invariant que trois cas avaient déjà illustré le 21/08/2026** — dont un qui coûtait
   40 px au milieu d'une carte : toute règle qui pose un `display` sur quelque chose de masquable
   doit poser aussi son `[hidden]`. Celui-ci a vécu six semaines de plus parce que le contrôle
   qui l'attrape se collait à la main dans une console ; il est désormais rejouable
   (`outils/faux-masques.mjs`), et le balayage des dix pages dit que c'était **le dernier**. */
.archive-photos[hidden] {
  display: none;
}

/* **La barre de chargement, et pourquoi elle est indéterminée.** Signalée à l'usage :
   « on a l'impression que rien ne se passe alors que la photo est simplement en train de
   charger ». Une `<img>` ne renseigne aucune progression — il faudrait passer par `fetch` et
   lire le flux octet par octet, donc renoncer au chargement natif de l'image pour un chiffre.
   Le va-et-vient dit ce qui compte : ça travaille.

   Elle est **au-dessus de la photo** et non au centre : un indicateur posé au milieu masque ce
   qu'on attend, et il y a déjà la vignette à regarder pendant ce temps.

   `translate` séparé, pour la même raison que les chevrons : le `transform` reste libre pour
   l'animation. `prefers-reduced-motion` arrête le va-et-vient et laisse une barre pleine —
   l'information demeure, le mouvement disparaît. */
.barre-chargement {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  overflow: hidden;
  width: 100%;
  height: 3px;
  border-radius: 10px 10px 0 0;
  background-color: var(--couleur-surface-teintee);
}

.barre-chargement::after {
  content: "";
  display: block;
  width: 40%;
  height: 100%;
  background-color: var(--couleur-accent);
  animation: va-et-vient 1.1s ease-in-out infinite;
}

@keyframes va-et-vient {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(250%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .barre-chargement::after {
    width: 100%;
    animation: none;
  }
}

/* **Le plein écran, par un appui sur la photo** — geste universel des applis photo, et il
   répond à un vrai calcul de place : l'image plafonne à `80vh`, le fil en réserve `30vh` en
   permanence même vide puisque le formulaire y est toujours, et la barre prend jusqu'à trois
   lignes selon les réactions. Sur un téléphone, la photo occupait donc à peine plus de la
   moitié de l'écran.

   **Pas `requestFullscreen()`** : Safari iOS ne l'offre que pour les vidéos, jamais pour un
   élément quelconque. Le plein écran se fait donc en CSS, dans un `<dialog>` qui couvre déjà
   tout — exactement comme la carte de trace.

   **`100dvh` et non `100vh`** : `vh` ignore la barre d'outils rétractable des navigateurs
   mobiles, donc la photo déborderait du bas de l'écran de sa hauteur — le piège classique.

   **Tout s'efface, la croix et les chevrons compris** — signalé à l'usage, et l'objection qui
   les avait gardés ne tient plus. Elle était juste quand elle a été écrite : sans eux il n'y
   avait plus de « à côté » où appuyer pour sortir. Le glissement vers le bas, arrivé depuis et
   valable à la souris comme au doigt, a rouvert la question ; un appui sur la photo ramène
   tout, et Échap n'a jamais cessé de fonctionner.

   **Une justification vraie le jour où on l'écrit peut cesser de l'être sans qu'on y
   revienne** — c'est le mode d'échec des commentaires qui défendent une absence, et le projet
   l'a déjà payé sur le `Hash` d'une photo.

   La barre de chargement, elle, reste : elle ne décore pas, elle informe. */
.visionneuse-photo.immersive {
  padding: 0;
}

.visionneuse-photo.immersive .barre-visionneuse,
.visionneuse-photo.immersive .commentaires-photo,
.visionneuse-photo.immersive .compteur-photo,
.visionneuse-photo.immersive .fermer-visionneuse {
  display: none;
}

.visionneuse-photo.immersive .cadre-photo > img,
.visionneuse-photo.immersive .cadre-photo > video {
  max-height: 100dvh;
  border-radius: 0;
}

/* **Où l'on en est dans la galerie**, au coin opposé à la croix et sur le même fond : les
   deux se lisent comme la même couche d'interface posée sur la photo. Il manquait, et son
   absence tenait à la même cause que tout le reste — la visionneuse montrait *une* photo au
   lieu de faire parcourir *une série*.

   Masqué par `photos.js` quand la galerie n'en contient qu'une : « 1 / 1 » n'apprend rien et
   ajoute une pastille sur une photo qui n'a rien à parcourir. */
/* **Couchée, la photo déborde de son cadre**, et tout ce qui s'ancre au cadre se retrouve alors
   au milieu de l'image — la barre de chargement s'y affichait en travers, signalé à l'usage. Elle
   se rattache donc à l'écran dans ce cas : `fixed` la résout contre la fenêtre et non contre
   l'ancêtre positionné, sans rien changer au cas courant. */
.photo-couchee .barre-chargement {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  width: auto;
}

.compteur-photo {
  position: absolute;
  top: 0.4rem;
  left: 0.4rem;
  padding: 0.15rem var(--espace-petit);
  border-radius: 20px;
  background-color: var(--couleur-surface-teintee);
  font-size: var(--texte-mention);
}

/* **Une vidéo ne se distinguait pas d'une photo dans la grille** : on ne savait ce qu'on
   ouvrait qu'après l'avoir ouvert. Le serveur envoyait pourtant déjà le type, et son
   commentaire annonçait cette pastille — le front ne l'avait jamais posée.

   Au centre et non au coin, contrairement aux pastilles de réaction : celles-ci comptent
   quelque chose et se rangent, celle-ci dit ce qu'est la tuile et doit se voir d'un coup
   d'œil sur une grille entière. */
.pastille-lecture {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  padding: var(--espace-petit);
  line-height: 0;
  border-radius: 50%;
  background-color: var(--couleur-surface-teintee);
}

.pastille-lecture .ti {
  font-size: var(--icone-bouton);
}

.fermer-visionneuse .ti {
  font-size: var(--icone-bouton);
}

/* ---------- Trace de la sortie ---------- */

/* Le SVG ne porte **ni largeur ni hauteur** : son `viewBox` — l'une des rares choses
   que trace.js écrive, avec l'attribut `d` et les `transform` — donne le rapport
   d'aspect, et le navigateur en déduit la hauteur à partir de la largeur disponible.
   En plein écran, ce même `viewBox` reprend les dimensions réelles de la scène pour que
   les tuiles s'affichent à leur taille propre. */
.trace-dessin {
  display: block;
  width: 100%;
  height: auto;
}

/* La vignette est un `<button>` : il faut donc désarmer l'habillage de contrôle que la
   règle générique `button` lui donne, et rétablir un curseur de main. `overflow: hidden`
   avec l'arrondi pour que le fond de carte ne dépasse pas des coins. `position: relative`
   ancre la mention légale dans l'angle — sans elle, elle se caserait sur la carte de
   sortie entière. */
.vignette-carte {
  display: block;
  position: relative;
  width: 100%;
  padding: 0;
  margin-bottom: var(--espace-petit);
  border: none;
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  line-height: 0;
}

/* Exigée par la licence des données, et visible au même endroit que la carte — mais dans
   l'angle, en `--texte-mention` et sur un fond discret, comme toute carte le fait. En
   pleine ligne et à la taille des étiquettes, elle pesait autant que les chiffres de la
   sortie. Le fond est nécessaire : un texte gris posé directement sur des tuiles se perd
   dès qu'il tombe sur une forêt ou une zone urbaine — même raison que la barre de la
   visionneuse de photos. */
/* **Allégée le 15/08/2026 : le cartouche pesait plus que ce qu'il annonce.** Fond opaque et
   texte plein en faisaient un bloc solide dans le coin de la carte, à côté d'une trace qu'on
   voyait moins bien que lui.
   **Ce qui ne peut pas bouger** : la mention doit rester lisible, c'est la condition
   d'utilisation des tuiles OpenStreetMap — on n'a pas le droit de la faire disparaître, ni de
   la rendre illisible. La taille reste donc `--texte-mention`, le palier prévu pour ça, et
   c'est le **poids** qui baisse : fond translucide plutôt qu'opaque, et texte discret plutôt
   que plein. Le contraste reste largement suffisant pour la lire. */
.attribution-carte {
  position: absolute;
  right: 0.15rem;
  bottom: 0.15rem;
  margin: 0;
  padding: 0 var(--espace-minuscule);
  border-radius: 3px;
  background-color: color-mix(in srgb, var(--couleur-surface-teintee) 55%, transparent);
  color: var(--couleur-texte-discret);
  font-size: var(--texte-mention);
  line-height: 1.4;
}

/* Le fond de carte. **Aucune inversion de couleurs en thème sombre**, piste écartée
   volontairement : inverser une carte OSM rend ses étiquettes illisibles et ses forêts
   violettes. Une carte reste une carte — on se contente de la retenir un peu pour
   qu'elle n'éblouisse pas au milieu d'une interface sombre. */
:root[data-theme="sombre"] .trace-tuile {
  opacity: 0.75;
}

/* Le halo posé sous le tracé, et **ce n'est pas un ornement** : c'est lui qui garantit
   la lisibilité depuis qu'il y a un fond de carte, laquelle porte toutes les couleurs
   à la fois — aucun trait ne peut assurer seul son contraste contre elle.

   Sa couleur ne dépend donc **pas du thème** mais du fond de carte, identique dans les
   trois : d'où sa place dans `:root`, seule couleur du projet à y vivre et pour cette
   raison précise (voir Palette). */
/* **Épaissis le 15/08/2026, après avoir regardé une vraie sortie sur un fond
   topographique** : à 2 px pour le trait et 5 px pour son liseré, la trace avait exactement
   le poids visuel des routes de la carte, qui en dessine des dizaines — elle s'y noyait au
   lieu de s'en détacher. Ce n'est pas une question de couleur mais d'épaisseur : sur un fond
   chargé, ce qui distingue une surcharge du dessin qu'elle surcharge, c'est d'être plus
   grosse que lui.

   **Ces valeurs sont des unités de `viewBox` et non des pixels d'écran** — voir la règle
   suivante, qui raconte pourquoi `non-scaling-stroke` a été retiré le 15/08/2026 et
   pourquoi les épaisseurs ont baissé du même coup. */
.trace-halo {
  fill: none;
  stroke: var(--couleur-liseret-trace);
  stroke-width: 7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* **`vector-effect: non-scaling-stroke` a été retiré le 15/08/2026, et il ne faut pas le
   remettre.** Il était là depuis 5b-1, où le `viewBox` épousait la trace : sans lui,
   l'épaisseur subissait la mise à l'échelle du cadre et une sortie étalée était dessinée au
   fil quand une sortie ramassée l'était au gros feutre. **Cet argument est mort le jour où
   le fond de carte a rendu le cadre fixe** — 400×300 pour la vignette, la taille réelle en
   pixels CSS pour le plein écran — et le commentaire ne l'avait pas suivi. Une épaisseur en
   unités de `viewBox` est désormais constante d'une trace à l'autre par construction.

   **Ce qu'il coûtait, lui, était bien réel** : les deux moteurs ne s'accordent pas sur ce
   que « ne subit aucune transformation » recouvre. Gecko neutralise la mise à l'échelle du
   `viewBox` et s'arrête là, donc `stroke-width: 6` vaut 6 pixels CSS ; Blink neutralise en
   plus la transformation de densité d'écran, donc les mêmes 6 valent 6 pixels **physiques**,
   soit 2,2 pixels CSS sur un téléphone à 2,75×. Mesuré sur deux captures de la même trace,
   en prenant la largeur de la carte comme étalon : le trait occupait 1,7 % de la carte sur
   un S25 contre 3,4 % dans Firefox, quand « 53,1 km » en occupait 16,2 et 15,9 % — les
   polices, en `rem`, tombaient d'accord ; le trait, seul, changeait de règle. **Le symptôme
   à reconnaître, c'est un réglage d'épaisseur qui ne converge pas** : régler sur le
   téléphone donne un trait énorme sur l'ordinateur, et l'inverse.

   **Les épaisseurs ont donc baissé en même temps** (6 sur 11 → 4 sur 7) : sans la propriété,
   les deux appareils convergent vers ce que montrait le rendu Firefox, qui était le plus
   lourd des deux. Unifier sans redescendre aurait figé la mauvaise valeur.

   Contrepartie assumée : `appliquerGeste()` met le groupe à l'échelle pendant un pincement
   et ne redessine qu'au relâchement, le trait grossit donc avec la carte le temps du geste.
   C'est ce que fait Leaflet, et c'est plus cohérent qu'un trait figé pendant que le fond
   enfle.

   Le rapport avec le liseré est conservé : le trait occupe un peu moins de la moitié, ce qui
   laisse de chaque côté une bordure claire assez large pour le décoller du fond.

   La couleur reprend `--couleur-serie-velo` plutôt qu'une variable propre : c'est
   l'accent du projet dédié aux éléments graphiques, dont le contraste est déjà mesuré
   dans les trois thèmes (voir Contrastes). **Depuis le fond de carte, ce n'est plus lui
   qui porte la lisibilité** — les 3:1 mesurés valaient contre la surface de la carte de
   sortie, pas contre des tuiles : c'est le halo ci-dessus qui s'en charge, et la couleur
   ne sert plus qu'à identifier le tracé. */
.trace-chemin {
  fill: none;
  stroke: var(--couleur-serie-velo);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* De deux à sept chiffres, selon ce que le GPX portait : `flex-wrap` avec `space-around`
   plutôt qu'une grille à colonnes fixes, et c'est ce qui rend le nombre variable
   supportable — une ligne incomplète se **centre** d'elle-même au lieu de laisser un vide
   à droite, qu'il manque la durée ou les trois vitesses. Même raison qui avait fait écarter
   `auto-fit` pour les boutons de correction, où l'orphelin restait plaqué à gauche. */
.chiffres-trace {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: var(--espace-petit) var(--espace-petit);
  margin-bottom: var(--espace-petit);
}

/* `flex-basis` à un peu moins du tiers : trois par ligne sur les 288 px les plus étroits
   comme sur les 480 du maximum, donc un rythme stable plutôt qu'un nombre de colonnes qui
   change avec le téléphone. */
.chiffre-trace {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 0 1 28%;
}

.valeur-trace {
  font-size: var(--texte-fort);
  font-weight: 600;
}


/* Signature à gauche, retrait à droite : même partage que la barre de la visionneuse
   de photos, et le bouton est `.bouton-nu` pour la même raison que le crayon des
   présences — une bordure d'accent pèserait bien plus que l'action, à côté d'une
   mention grise. */
.pied-trace {
  display: flex;
  align-items: center;
  justify-content: space-between;
}


/* Les quatre classes de pente, posées par trace.js sur les segments du tracé **et** sur
   la ligne du profil : une seule échelle pour les deux écrans, donc une seule légende, et
   le même rouge désigne le même mur partout. Le JS n'y met qu'un nom de classe.

   **Sélecteurs combinés, et c'est une précaution délibérée** : `.trace-chemin` porte déjà
   un `stroke`, donc une règle `.pente-plate` seule aurait la même spécificité et ne
   gagnerait que par sa position dans le fichier. Déplacer cette section suffirait à
   repeindre toute la trace en cyan, **sans erreur ni symptôme ailleurs**. Le projet a déjà
   payé deux fois ce genre de dépendance à l'ordre (voir Carte de sortie). */
.trace-chemin.pente-montee-forte,
.profil-ligne.pente-montee-forte { stroke: var(--couleur-pente-montee-forte); }

.trace-chemin.pente-montee-moyenne,
.profil-ligne.pente-montee-moyenne { stroke: var(--couleur-pente-montee-moyenne); }

.trace-chemin.pente-montee-douce,
.profil-ligne.pente-montee-douce { stroke: var(--couleur-pente-montee-douce); }

.trace-chemin.pente-plat,
.profil-ligne.pente-plat { stroke: var(--couleur-pente-plat); }

.trace-chemin.pente-descente-douce,
.profil-ligne.pente-descente-douce { stroke: var(--couleur-pente-descente-douce); }

.trace-chemin.pente-descente-forte,
.profil-ligne.pente-descente-forte { stroke: var(--couleur-pente-descente-forte); }

/* Les chevrons de sens : **remplis, sombres, et cerclés de clair**. Leur première version
   était un « > » tracé dans le blanc du halo, posé sur un trait déjà bordé de blanc : il
   disparaissait. Un triangle plein contraste avec le trait qu'il chevauche quelle que soit
   la couleur de pente de celui-ci, et le liseré clair le détache du fond de carte. */
.chevron-trace {
  fill: var(--couleur-chevron-trace);
  stroke: var(--couleur-halo-trace);
  stroke-width: 1;
  stroke-linejoin: round;
}

/* Les repères de départ et d'arrivée. **Ils partagent le remplissage et le liseré des
   chevrons au lieu de les recopier** (voir la convention dans CLAUDE.md) : ils ont le même
   problème à résoudre — se détacher d'un tracé qui porte six teintes, posé sur un fond de
   carte dont on ne sait rien — et ils se côtoient sur le même dessin, où deux valeurs
   voisines mais distinctes se verraient.

   Le liseré est un peu plus épais que celui des chevrons (1,5 contre 1) : un disque de
   4,5 unités de rayon offre plus de surface qu'un triangle, donc plus de contour à
   traverser avant que la forme ne se lise. C'est le seul écart, et il est délibéré.

   **La distinction départ/arrivée est une affaire de forme, pas de couleur**, d'où
   l'absence de toute règle de teinte ici : voir `formeRepere` dans `trace.js`. */
.repere-trace {
  fill: var(--couleur-chevron-trace);
  stroke: var(--couleur-halo-trace);
  stroke-width: 1.5;
  stroke-linejoin: round;
}

/* ---------- Profil altimétrique ---------- */

.profil-dessin {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--espace-minuscule);
}

/* L'aire garde une teinte neutre : la couleur vit sur la ligne de crête, où elle est
   lisible. Une aire coloriée par pente donnerait quatre grands aplats qui écraseraient
   la silhouette. */
.profil-aire {
  fill: var(--couleur-surface-neutre);
  stroke: none;
}

/* **Épaissie et affranchie de `non-scaling-stroke` le 15/08/2026** (voir `.trace-chemin`,
   où le mécanisme est raconté). C'est ici qu'il faisait le plus de dégâts : à 2 avec la
   propriété, la crête tombait à **0,7 pixel CSS sur un téléphone à 2,75×**, donc un filet
   sous-pixel que l'antialiasing délavait — la silhouette était grise et molle là où elle
   doit se lire d'un coup d'œil. À 3,5 sans elle, elle vaut ~2,7 px sur téléphone et ~3,8 sur
   ordinateur, et c'est le même trait partout.

   **3,5 et non 3** : relevé à l'œil le 15/08/2026 une fois le trait enfin identique sur les
   deux appareils — c'est justement ce que l'unification permet, un réglage jugé une seule
   fois. Elle reste plus fine que le tracé de la carte (4), ce qui est voulu : le profil est
   une silhouette, pas la trace.

   Aucune contrepartie ici, contrairement au tracé : le profil a un `viewBox` fixe (400×70)
   et ne subit aucun geste. */
.profil-ligne {
  fill: none;
  stroke-width: 3.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* `flex-wrap` : cinq éléments sur 288 px ne tiennent pas d'affilée, l'amplitude passera
   à la ligne plutôt que d'écraser les seuils. */
.legende-pente {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-minime) var(--espace-petit);
  margin-bottom: var(--espace-petit);
  font-size: var(--texte-mention);
  color: var(--couleur-texte-discret);
}

.item-legende {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

/* La pastille reprend la couleur de la classe en **fond** là où le tracé l'utilise en
   contour : d'où `background-color` ici et `stroke` là-bas, une même variable pour deux
   propriétés. */
.pastille-pente {
  width: 0.7rem;
  height: 0.2rem;
  border-radius: 2px;
}

.pastille-pente.pente-montee-forte { background-color: var(--couleur-pente-montee-forte); }
.pastille-pente.pente-montee-moyenne { background-color: var(--couleur-pente-montee-moyenne); }
.pastille-pente.pente-montee-douce { background-color: var(--couleur-pente-montee-douce); }
.pastille-pente.pente-plat { background-color: var(--couleur-pente-plat); }
.pastille-pente.pente-descente-douce { background-color: var(--couleur-pente-descente-douce); }
.pastille-pente.pente-descente-forte { background-color: var(--couleur-pente-descente-forte); }

.amplitude-profil {
  margin-left: auto;
}

/* ---------- Voyage de fin d'année ---------- */

/* Le choix de l'édition, au-dessus de la carte : c'est une navigation, pas une donnée du
   voyage. Masqué tant qu'il n'y a qu'une édition — d'où la règle `[hidden]`, obligatoire
   dès qu'un sélecteur pose `display` (voir Composants et mise en page). */
/* **La zone qui parle des éditions** (21/08/2026) : laquelle j'ouvre, et j'en lance une autre.
   Elle porte le sélecteur — qui se masque seul quand il n'y a qu'une édition — et le bouton de
   création, qui, lui, reste toujours offert. C'est la marge basse qui déménage ici : elle
   appartenait au sélecteur, or c'est la zone entière qui doit se détacher de ce qui suit. */
.zone-editions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--espace-petit);
  margin-bottom: var(--espace-grand);
}

/* Le formulaire de création. **Deux champs seulement** — l'année et la destination si on la
   connaît déjà. Bordé en largeur : sur un écran large, trois contrôles étirés sur toute la page
   se liraient comme un formulaire d'administration, alors que c'est un geste de deux secondes. */
/* **Le `gap` est retiré, pas la marge des champs** (22/08/2026). Ce conteneur est le seul
   formulaire du projet en `flex` : son `gap` de 8 px s'ajoutait à la marge basse des champs au
   lieu de fusionner avec, ce qui donnait **28 px** entre l'année et la destination contre 20
   partout ailleurs. Le `gap` à zéro le ramène au comportement commun.

   **Les 20 px restants ne sont pas propres à ce formulaire**, et c'est un constat à part
   (`D14`) : un `<input>` est `inline-block`, donc ses marges **ne fusionnent jamais** avec
   celles de son voisin — la marge basse de 1 rem et la marge haute de 0,25 rem s'ajoutent dans
   **tous** les formulaires de l'application. À traiter d'un seul geste sur la règle générique
   des champs, pas ici. */
#formulaire-creation-voyage {
  display: flex;
  flex-direction: column;
  gap: 0;
  width: 100%;
  max-width: 22rem;
}

#formulaire-creation-voyage[hidden] {
  display: none;
}

/* Les deux boutons nus de l'en-tête de la carte du voyage — corriger et partager. Sans cette
   rangée, `.entete-presences` les écarterait l'un de l'autre par son `space-between`, le titre
   d'un côté et les deux boutons éparpillés de l'autre. */
.actions-entete-voyage {
  display: flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

.choix-annee-voyage {
  display: flex;
  gap: var(--espace-petit);
  align-items: center;
  justify-content: center;
}

.choix-annee-voyage[hidden] {
  display: none;
}


.choix-annee-voyage select {
  margin: 0;
}

/* Les liens sortants — logement, Tricount, photos — côte à côte et cliquables au pouce.
   `flex-wrap` parce que trois libellés ne tiennent jamais sur 288 px. */
/* **Plus de marge propre depuis le 28/08/2026** (`RC-UX-056`) : la carte du voyage est une
   `.carte-empilee`, donc une colonne flex à `gap: 8px` dont la règle `> *` annule les marges de
   ses enfants directs — et celle-ci en reposait une. **En flexbox, `gap` et `margin`
   s'additionnent**, les marges n'y fusionnant pas : mesuré, **20 px** rendus au-dessus comme en
   dessous là où la carte en veut 8, et 20 n'est aucun palier de l'échelle
   (2 · 4 · 8 · 12 · 16 · 24 · 56).

   **Le défaut était écrit dans cette feuille comme une prédiction**, quelques dizaines de lignes
   plus bas, à propos du cas voisin : « le même piège vaut pour la rangée de liens de la carte du
   voyage — **invisible aujourd'hui, ses trois liens étant nuls** ». L'auteur avait vu le piège,
   l'avait nommé, et avait constaté qu'aucun voyage de la base ne portait de lien pour le
   révéler. Il a suffi d'en poser deux. */
.liens-voyage {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-petit);
}

/* **Sans ça, la rangée masquée occupe quand même la place** : `display: flex` l'emporte sur le
   `[hidden] { display: none }` du navigateur, à spécificité égale. C'est le piège que
   `.carte-stats` et `.previsions-horaires` documentent déjà — il n'était pas gardé ici.

   **Il coûtait 40 px au milieu de la carte du voyage**, mesuré : les deux `gap` de la colonne
   plus les deux marges de la rangée vide, sur une édition sans logement ni Tricount ni album —
   c'est-à-dire toute édition qu'on vient de lancer. Trouvé en cherchant une anomalie
   d'espacement ; ce n'en était pas une. */
.liens-voyage[hidden] {
  display: none;
}

/* Habillés comme des boutons secondaires sans en être : ce sont de vrais liens, qui
   doivent garder leur menu contextuel et leur ouverture en nouvel onglet. */
/* **Le lien Tricount d'une sortie reprend cette classe** (22/08/2026) : c'est le même objet, un
   lien vers un service externe posé dans une carte. Son `[hidden]` est déclaré ici même —
   cette classe ne pose aucun `display`, donc le défaut du navigateur suffirait, mais l'écrire
   protège du jour où elle en posera un (trois cas payés le 21/08/2026). */
.lien-voyage {
  padding: var(--espace-petit) var(--espace-moyen);
  border: 1px solid var(--couleur-bordure);
  border-radius: 8px;
  font-size: var(--texte-courant);
  color: var(--couleur-texte);
  text-decoration: none;
}
.lien-voyage[hidden] {
  display: none;
}

/* **L'intitulé du Tricount se masque quand un Tricount existe** (12/09/2026) : le bouton qui
   l'ouvre nomme alors la zone. Sans cette règle, le `display: flex` de la déclaration partagée
   des intitulés l'emporterait sur le `[hidden]` du navigateur, à spécificité égale.

   **Il a été le lien lui-même du 22/08 au 12/09/2026**, souligné sur le seul mot quand il avait
   un `href`. Ce que ce soulignement avait appris — `text-decoration` se propage aux descendants
   au lieu de s'hériter, donc un enfant ne peut pas l'annuler — est consigné dans
   `docs/sorties-pas-du-mardi.md`. */
.label-tricount[hidden] {
  display: none;
}

/* La zone du Tricount empile ses lignes avec un écart régulier : l'intitulé ou la rangée
   d'ouverture, le formulaire, le retrait. */
#detail-libre-tricount {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--espace-petit);
}

/* **Masquable depuis le 13/09/2026** : l'historique porte cette carte pour ses sorties libres et la
   masque sur un mardi. Le sélecteur d'identifiant ci-dessus l'emporterait sur tout `[hidden]` de
   classe — celui de `.carte-stats` compris. */
#detail-libre-tricount[hidden] {
  display: none;
}

/* **La rangée d'ouverture** (12/09/2026) : le bouton qui ouvre, le partage, le crayon. Le bouton
   prend la place, les deux icônes gardent la leur — `min-width: 0`, pour qu'un libellé ne pousse
   jamais une icône hors de la carte.

   **`align-self: stretch`, et il le faut** : la carte aligne ses lignes au début, donc une ligne y
   prend la largeur de son contenu. Celle-ci doit courir sur toute la carte. */
.rangee-tricount {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: var(--espace-petit);
}

.rangee-tricount[hidden] {
  display: none;
}

.rangee-tricount .bouton-livetrack {
  flex: 1;
  min-width: 0;
}

/* Les journées du séjour reprennent le gabarit des liens sortants ci-dessus — même objet,
   un lien habillé en bouton — mais **pas leur marge basse**. Celle-ci est écrite pour la
   carte du voyage, où la rangée a des lignes au-dessus et en dessous ; ici elle ferme sa
   propre carte, et s'ajoutait donc au padding : 17 px en haut contre 29 en bas, mesuré.

   **Corrigé chez celui qui diverge et non sur la classe partagée** (voir Conventions dans
   CLAUDE.md) : `.liens-voyage` sert les deux cartes, et lui retirer sa marge changerait
   l'espacement des liens du logement et du Tricount, qu'on ne touche pas ici.

   **`.carte-empilee` avait été essayé d'abord, et ne corrige rien** : sa règle
   `.carte-empilee > *` a la même spécificité que `.liens-voyage`, déclarée plus bas dans
   cette feuille, qui l'emporte donc à égalité. Le `gap` s'ajoutait à la marge au lieu de la
   remplacer. Le même piège vaut pour la rangée de liens de la carte du voyage — invisible
   aujourd'hui, ses trois liens étant nuls. */
#liste-sorties-voyage {
  margin: var(--espace-moyen) 0 0;
}

/* Les champs du formulaire s'empilent, le bouton prend toute la largeur — même
   disposition que la proposition d'une sortie pas du mardi. */
#formulaire-voyage button {
  width: 100%;
}

/* ---------- Sorties pas du mardi ---------- */

/* Une ligne de sortie pas du mardi dans l'historique. Elle garde le gabarit des autres —
   même hauteur, même alignement — et ne se distingue que par sa marque : un liseré ou un
   fond en ferait un bloc à part, alors que la page promet une chronologie continue.

   **Les deux marques partagent la déclaration** parce qu'elles se posent **sur la même
   ligne**, côte à côte, quand une sortie pas du mardi est aussi un bikepark : deux
   déclarations séparées auraient dérivé d'un demi-point de taille, et l'écart se verrait
   ici plus qu'ailleurs (voir Conventions dans CLAUDE.md). */
.marque-pas-du-mardi,
.marque-bikepark {
  /* Une icône seule : c'est une taille d'**objet**, pas de texte, donc `--icone-texte` et
     non un palier de l'échelle typographique (voir Typographie). `.ti` se dimensionnant en
     `em`, c'est bien `font-size` qui la règle. */
  font-size: var(--icone-texte);
  color: var(--couleur-texte-discret);
  /* Aligne le calendrier sur la ligne de base de la date qui le précède : sans ça il
     flotte d'un pixel ou deux au-dessus, ce qui se voit sur une liste de dix lignes. */
  display: inline-flex;
  align-items: center;
}


/* La marque « bikepark ou shuttle » **là où il y a la place pour des mots** : la carte
   d'une sortie à venir et le détail d'une sortie passée. Dans la liste de l'historique,
   large de quelques mots, c'est `.marque-bikepark` ci-dessus qui sert — une icône seule
   doublée d'un texte pour lecteur d'écran, exactement comme la marque du calendrier à
   côté de laquelle elle se pose.

   **Une seule déclaration pour les deux écrans** : ils se suivent (on regarde la sortie
   avant, on la relit après), et une pastille qui changerait de taille entre les deux se
   remarquerait. */
/* **Ses marges verticales sont annulées** (22/08/2026), et le piège était double. C'est un `<p>`,
   donc il gardait le `1em` du navigateur en haut comme en bas — soit 12,8 px à
   `--texte-etiquette`. Et il est `inline-flex` : les marges d'une boîte de niveau ligne **ne
   fusionnent pas** avec celles de ses voisines de bloc, elles **s'ajoutent**. Mesuré sur la vue
   détail : le titre laissait ses 24 px voulus **plus** 12,8, soit 37 px ; et 12,8 + 4 faisaient
   17 px avant la signature. Aucun des deux n'est un palier.

   La même marge fantôme gonflait la rangée `.meta-sortie` de la carte de liste, où le badge est
   un enfant flex : ses marges y étaient honorées et s'ajoutaient au `gap`.

   **Annulé ici, posé au site** : c'est la convention du projet, et c'est ce qui évite qu'un
   emploi paie l'espacement voulu par l'autre. Le détail pose sa marge basse juste en dessous. */
.badge-bikepark {
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  align-self: flex-start;
  padding: var(--espace-minime) var(--espace-petit);
  border-radius: 999px;
  background-color: var(--couleur-surface-neutre);
  color: var(--couleur-texte-discret);
  font-size: var(--texte-etiquette);
}

/* **Obligatoire dès qu'une classe pose un `display`** : il l'emporte sur le `[hidden]` du
   navigateur, et le badge du détail — écrit en dur dans la page, masqué par défaut —
   s'afficherait sur chaque sortie. Quatrième occurrence de ce piège dans le projet. */
.badge-bikepark[hidden] {
  display: none;
}

/* Sur la vue détail le badge est un bloc de métadonnées entre le titre et la signature : il
   ferme son groupe, d'où `--espace-moyen` en bas. Rien en haut, les 24 px de `.hote-detail`
   au-dessus suffisent — et toute valeur non nulle s'y **ajouterait** au lieu de fusionner. */
/* **Les marges sont passées à la rangée le 04/09/2026.** Le badge et la signature sont désormais
   groupés dans `.meta-detail-libre`, et deux marges verticales dans un conteneur flex ne
   fusionnent pas : laissées ici, elles auraient décalé les deux mentions l'une par rapport à
   l'autre. C'est le réflexe que la fiche du projet énonce — annuler les deux marges sur
   l'élément, poser le besoin au site.

   **Le commentaire précédent affirmait faux**, et il vaut d'être corrigé plutôt qu'effacé : il
   disait que toute valeur haute « s'ajouterait au lieu de fusionner ». C'est vrai dans la carte de
   la liste, qui est un conteneur flex, et faux ici — `#vue-detail` est en `display: block`, où les
   marges verticales fusionnent bel et bien. Le raisonnement avait été transposé d'un écran à
   l'autre sans être revérifié. */
#detail-libre-bikepark {
  margin: 0;
}

/* **Le même badge, sur le détail de l'historique** (29/08/2026, signalé à l'usage : « l'étiquette
   est collée au bloc d'en dessous »). Il y était rendu à **24 px au-dessus et 8 px en dessous** :
   il appartenait visuellement au bloc suivant, alors qu'il ferme l'en-tête.

   **La valeur diffère de celle de son jumeau ci-dessus, et c'est le voisin qui l'explique**, pas
   un désaccord : le badge est en `inline-flex`, donc ses marges **s'ajoutent** à celles du bloc
   suivant au lieu de fusionner. À côté, la signature apporte 4 px et 12 suffisent ; ici le mot en
   apporte 8. **La seconde règle annule cette addition** plutôt que de la compenser par une
   soustraction : sans elle il faudrait choisir entre 16 px quand un mot suit et 8 px quand c'est
   la carte, selon un voisin que le badge ne connaît pas.

   **`:not([hidden])` est indispensable** : le badge est écrit en dur dans la page et seulement
   masqué, donc `+` l'atteindrait aussi quand il n'y a pas de bikepark — et le mot perdrait son
   écart sur toutes les autres sorties. Rendu mesuré : 24 px au-dessus, 16 en dessous, dans les
   deux cas. */
#detail-bikepark {
  margin-bottom: var(--espace-grand);
}

#detail-bikepark:not([hidden]) + #detail-mot-hote {
  margin-top: 0;
}

/* Deux boutons et non trois, contrairement au sondage du mardi : sans hôte il n'y a pas de
   repas, donc pas de « je mange ». `1fr` chacun plutôt qu'une grille à trois colonnes —
   deux boutons larges se visent mieux au pouce qu'un alignement conservé par symétrie. */
.boutons-sortie-libre {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--espace-petit);
  margin-top: var(--espace-moyen);
}

/* **Un `gap` de conteneur ne fusionne pas avec la marge d'un enfant, il s'y ajoute** — et
   c'est le piège que la sonde `ecartsHorsEchelle()` a été écrite pour attraper (22/08/2026,
   `D9`). `.carte-empilee` pose un `gap` de 8 px, la valeur générique ci-dessus en ajoute 12 :
   **20 px rendus**, qui n'est pas un palier. Le projet documente déjà que `.carte-empilee`
   doit être neutralisé dès qu'il porte des blocs de natures différentes ; ici la neutralisation
   se fait sur la marge, pour que la somme retombe sur `--espace-grand`.

   **Visible seulement sur une édition sans participant** : dès qu'il y a des noms au-dessus, la
   ligne rendait déjà autre chose. C'est un défaut d'état, trouvé en fabriquant une édition
   qu'on vient de lancer. */
#boutons-participation {
  margin-top: var(--espace-petit);
}

/* **Indispensable et pas décoratif, et le piège s'est refermé exactement comme annoncé** : le
   `display: grid` ci-dessus l'emporte sur le `[hidden]` du navigateur, donc masquer ces boutons
   depuis le JS ne faisait **rien du tout**. Constaté le 17/08/2026 en masquant le sondage d'un
   voyage passé : l'attribut était bien posé — le test le confirmait — et les boutons restaient à
   l'écran. Même famille que `.popup[open]`, `.compteurs-ancienne[hidden]` et
   `.carte-plein-ecran[hidden]` (voir docs/interface.md).

   **Règle** : toute classe qui pose un `display` a besoin de sa règle `[hidden]` explicite, et ça
   ne se voit qu'à l'écran — jamais en lisant le JS, qui a l'air juste. */
.boutons-sortie-libre[hidden] {
  display: none;
}

/* Le bouton qui déplie le formulaire de proposition. Pleine largeur comme la carte qu'il
   ouvre, et posé juste au-dessus d'elle : c'est lui qui en tient l'intitulé.

   **Le « + » tourne de 45° et devient une croix** plutôt que de changer d'icône. Le jeu
   embarqué est un sous-ensemble de Tabler — trente icônes — donc chaque glyphe de plus est
   une extraction à faire, et une classe `ti-` sans règle donne un carré plein et non une
   absence (voir docs/interface.md). Une rotation ne coûte rien et ne peut pas manquer.

   L'état vit sur `aria-expanded`, jamais sur une classe posée en double : l'attribut qui
   annonce l'état au lecteur d'écran est celui qui pilote l'affichage. */
.bouton-deplier {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  width: 100%;
  margin-bottom: var(--espace-grand);
}

.bouton-deplier i {
  transition: transform 0.2s ease;
}

.bouton-deplier[aria-expanded="true"] i {
  transform: rotate(45deg);
}

/* La carte dépliée se colle à son bouton : les deux ne font qu'un objet, et la marge basse
   du bouton les aurait séparés autant que la carte l'est du bloc suivant. */
.bouton-deplier[aria-expanded="true"] {
  margin-bottom: var(--espace-petit);
}

/* **Une carte se lit ligne à ligne, donc elle s'aligne à gauche.** Elle héritait du centrage
   du `body`, que seuls l'en-tête (flex) et le mot contredisaient : la date à gauche, « Proposée
   par X » centré, le mot à gauche, les venants centrés — quatre alignements pour six lignes,
   ce qui donnait un empilement de fragments plutôt qu'une carte. C'est le défaut de
   lisibilité de cet écran, avant même le contenu.

   **L'alignement est posé sur la carte et non sur les classes des lignes** : `.noms-presence`
   et `.signature-trace` servent aussi l'historique et la page voyage, où le centrage est
   voulu. Une classe partagée se corrige chez celui qui diverge, jamais chez tout le monde —
   c'est le pendant exact du piège de spécificité documenté plus bas. */
.carte-sortie-libre {
  text-align: left;
  /* **Le `gap` uniforme de `.carte-empilee` est neutralisé ici, et c'est tout le sujet.**
     Aligner la carte à gauche avait réglé le désordre, mais l'avait remplacé par une
     colonne : six blocs de natures différentes — un titre, deux mentions, un texte, un
     état, une action — séparés par le même intervalle, donc lus comme une liste. L'œil
     n'avait aucun point d'accroche.

     L'espacement est donc décidé groupe par groupe ci-dessous : **serré à l'intérieur d'un
     groupe, large entre deux groupes**. C'est ce contraste, et non l'alignement, qui donne
     sa structure à une carte. */
  gap: 0;
}

/* **Les actions tiennent l'angle, elles ne flottent pas à mi-hauteur.** `.entete-presences`
   centre ses enfants, ce qui convenait tant que le titre tenait sur une ligne : depuis que la
   date et le nom s'empilent, la corbeille et le partage se retrouvaient alignés sur le nom,
   au milieu du bloc. Réglage porté par `.carte-sortie-libre` et non par `.entete-presences`,
   qui sert aussi l'historique et la page voyage — une classe partagée se corrige chez celui
   qui diverge, jamais chez tout le monde (voir `docs/sorties-pas-du-mardi.md`).

   **`center` depuis le 04/09/2026, et `flex-start` avait une raison qui a cessé d'être vraie.**
   L'en-tête aligne le bloc titre et les deux boutons d'action — corbeille, partage —, et ceux-ci
   ne peuvent pas rétrécir sans perdre leur cible tactile : mesuré, ils imposent 36 px là où une
   accroche d'une seule ligne n'en occupe que 24. En `flex-start`, ces **12 px de vide tombaient
   tous sous la date**, ce qui distendait la carte sans titre — la seule des trois où le bloc
   titre est plus court que les boutons. Centré, le vide se répartit 6/6.

   **Ça ne déplace presque rien sur les autres** : quand le titre et sa date font deux lignes,
   c'est le bloc titre qui dicte la hauteur et les boutons descendent de 2,5 px.

   **Une seule déclaration, et c'est le sujet** : la première version du 04/09 avait ajouté un
   second bloc pour le même sélecteur, deux cents lignes plus bas. Il écrasait celui-ci en
   silence, laissant un `flex-start` mort sous un commentaire qui le justifiait encore. */
.carte-sortie-libre > .entete-presences {
  align-items: center;
}

/* **La date et le nom de la sortie, empilés** (20/08/2026). Ils partageaient une ligne
   séparée d'un point médian, qui passait à deux lignes dès qu'un titre existait — avec un
   retour tombant au hasard de la casse, sur une page dont le sujet est justement le *jour*.

   **`min-width: 0` n'est pas décoratif** : sans lui, un titre long pousse les deux boutons
   d'action hors de la carte au lieu de se replier, `.entete-presences` étant une rangée flex
   dont les enfants refusent par défaut de descendre sous leur contenu. */
.titre-sortie-libre {
  min-width: 0;
}

/* **La charge des marges est reprise ici, et c'est obligatoire.** `.carte-empilee
   .entete-presences > p` ne voit que les enfants **directs** de l'en-tête : ces deux
   paragraphes sont devenus des petits-enfants en entrant dans le conteneur ci-dessus, donc
   hors de sa portée, et récupéreraient le `1em` du navigateur — le trou qu'aucune règle
   n'explique, déjà payé deux fois sur cette carte (voir les Conventions de CLAUDE.md). */
.titre-sortie-libre > p {
  margin: 0;
}

/* **La date prend la typographie des identités sans en prendre la couleur.** Famille
   d'affichage, italique, graisse 600, `--texte-fort` : exactement `.chez-ancienne`, qui
   habille juste dessous le nom de la sortie. La couleur, elle, reste celle du texte — le
   second accent est réservé à ce qui identifie quelqu'un (voir `docs/themes-palette.md`), et
   une date n'identifie personne.

   **Les deux pèsent donc pareil**, et c'est voulu : le jour et le nom de la sortie sont deux
   faits de même rang, l'un répondant « quand » et l'autre « quoi ». Même couple que l'hôte et
   son mot sur la carte du mardi, réglé le même jour. */
.date-sortie-libre {
  font-family: var(--police-affichage);
  font-style: italic;
  font-weight: 600;
  font-size: var(--texte-fort);
  line-height: 1.25;
}

/* **La date en appui sous le nom de la sortie** (20/08/2026). Le commentaire ci-dessus décrivait
   deux faits « de même rang », et la mesure a montré ce que ce rang égal produisait : deux lignes
   en `--texte-fort` graisse 600, collées à 0 px, séparées par la seule couleur — donc un bloc
   indifférencié. Le nom mène désormais, la date accompagne.

   **`--texte-etiquette` et `--couleur-texte-discret`**, soit exactement le couple que le projet
   emploie partout pour une mention (voir l'échelle de `docs/interface.md`) : la même que
   `.signature-trace` juste en dessous, avec qui elle forme le second rang de la carte.

   **La règle du dessus n'est pas morte** : la date reprend l'accroche, et donc
   `.date-sortie-libre`, quand la sortie n'a pas de nom — le titre est facultatif. */
.date-appui-sortie-libre {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  line-height: 1.35;
}

/* Les métadonnées se collent au titre : elles le complètent, elles ne le suivent pas. */
.carte-sortie-libre > .meta-sortie {
  margin-top: var(--espace-minuscule);
}

/* Marque et signature côte à côte. `flex-wrap` parce que rien ne garantit qu'elles tiennent
   sur une ligne : un pseudo long sur un écran de 320 px les fait repasser l'une sous
   l'autre, ce qui est exactement l'état d'avant — une dégradation, pas une casse. */
.meta-sortie {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-petit);
}

/* **Indispensable, et le piège est muet** : ces deux `<p>` étaient enfants directs de
   `.carte-empilee`, dont la règle annule les marges. Groupés dans cette rangée ils deviennent
   des petits-enfants, hors de portée d'un sélecteur d'enfant direct, et récupèrent le `1em`
   par défaut du navigateur — soit un trou dans la carte qu'aucune règle n'explique. Même
   cause exactement que la date de l'en-tête, déjà rattrapée par
   `.carte-empilee .entete-presences > p`. */
.meta-sortie > p {
  margin: 0;
}

/* Le badge est conçu pour une colonne, où `align-self` le décolle du bord gauche. En rangée,
   cette valeur le remonterait contre le haut de la ligne au lieu de le centrer sur le texte
   d'à côté. */
.meta-sortie > .badge-bikepark {
  align-self: auto;
}

/* Le corps de la carte. **Il ne respire plus que par le haut** (20/08/2026, au soir) : sa marge
   basse doublait le joint que la règle des présences porte désormais pour de vrai, et le projet
   a déjà payé cette addition une fois sur cette carte — `.carte-empilee` est un conteneur flex,
   où deux marges adjacentes **ne fusionnent pas**. Un joint, une seule règle. */
.carte-sortie-libre > .texte-mot-hote {
  margin: var(--espace-grand) 0 0;
}

/* `.presences-sortie-libre`, `.presences-sortie-libre > p` et `.presence-secondaire` ont été
   retirées le 20/08/2026 au soir : quatre déclarations décrivant un bloc « à trois états » que
   **rien ne construit** — zéro occurrence dans le JS et le HTML, vérifié par comptage. Le détail
   d'une sortie libre bâtit `.groupe-presence` et `.titre-groupe-presence`, qui reprennent le
   balisage de l'historique ; la carte, elle, n'affiche qu'une ligne. C'est du code mort de la
   même famille qu'`A23`, et l'une des quatre portait le correctif d'espacement du 20/08 — d'où
   un correctif mesuré, raisonné, et sans aucun effet (voir la règle des présences plus bas). */

/* **L'accroche de la carte, quelle que soit sa nature** (04/09/2026) — le nom de la sortie, ou
   la date quand il n'y a pas de nom. Une seule classe pour les deux, ce que le commentaire de
   `sorties-pas-du-mardi.js` affirmait déjà sans que ce soit vrai : le nom prenait
   `.chez-ancienne` (rose) et la date de repli `.date-sortie-libre` (bleu foncé), si bien que
   **deux cartes de la même liste s'annonçaient dans deux couleurs différentes**, au gré d'un
   titre facultatif.

   **`--titre-section` et non `--texte-fort`**, et c'est le cœur de la reprise : à 1 rem
   l'accroche pesait exactement autant que le programme posé dessous, qui court sur trois lignes
   et l'emportait donc à l'œil. Signalé à l'usage — « l'accroche est trop faible », « une pile de
   texte ». Le palier existait déjà, il n'y avait pas de nombre à inventer.

   **Elle ne partage plus la déclaration du « chez X »**, alors qu'elle l'avait reprise
   délibérément le 16/08 : les deux ont cessé de vouloir se ressembler — celle-ci domine une
   carte, celle-là tient dans une ligne de liste — et la règle du projet dit qu'une classe
   partagée se corrige chez celui qui diverge. La couleur d'identité est en revanche conservée :
   c'est elle qui accroche l'œil, et son contraste est déjà mesuré sur les trois thèmes. */
.accroche-sortie-libre {
  font-family: var(--police-affichage);
  font-style: italic;
  font-weight: 600;
  font-size: var(--titre-section);
  color: var(--couleur-accent-secondaire-texte);
  line-height: 1.25;
}

/* L'état du sondage se colle au sondage : les deux ne font qu'un groupe. Sans programme,
   la marge haute rétablit l'air que `.texte-mot-hote` aurait apporté.

   **Ces deux valeurs ont été mesurées le 20/08/2026, et elles disaient l'inverse de leur
   intention.** L'écart réel entre l'état et ses boutons valait **17,6 px**, contre **14,4 px**
   entre les métadonnées et l'état : l'espace *à l'intérieur* du groupe était plus large que
   la coupure *entre* les groupes. La carte se lisait donc encore comme une colonne plate,
   malgré la passe du 16/08 qui avait posé la règle « serré dedans, large dehors ».

   **La cause est une addition que personne n'a écrite** : ce `margin-bottom: 0.35rem` et le
   `margin-top: 0.75rem` de `.boutons-sortie-libre` portent le même joint, et
   `.carte-empilee` est un conteneur **flex** — où les marges adjacentes **ne fusionnent
   pas**. Deux règles justes séparément en faisaient une fausse ensemble.

   **Un joint, une seule règle** : la marge basse est remise à zéro ici et le joint est décidé
   plus bas, sur les boutons, à l'intérieur de cette carte. La coupure du dessus passe à
   1,25 rem. Rapport obtenu : 20 px entre les groupes, 5,6 px dedans.

   **Ce correctif n'a jamais rien fait, et c'est le sujet** (20/08/2026, au soir) : il visait
   `.presences-sortie-libre`, un conteneur que **rien ne construit** — la classe apparaissait
   quatre fois dans cette feuille et **zéro fois** dans le JS et le HTML. La carte, elle, ajoute
   directement un `<p class="noms-presence">`, ou `.invites-vide` dans l'état vide.
   La mesure sur le DOM réel donnait donc **0 px** de coupure au lieu des 20 annoncés, et la
   carte se lisait toujours comme une colonne plate — exactement ce que ce commentaire disait
   avoir corrigé.

   **Le défaut ne se voyait que sur une carte sans programme.** Avec un `mot`, ses marges de
   0,9 rem tenaient lieu de coupure des deux côtés et la structure apparaissait ; sans lui elle
   disparaissait avec lui. Or le programme est facultatif par conception, donc la forme sans
   structure était le cas courant.

   **Deux sélecteurs parce que l'état vide remplace la classe** : `creerParticipantsSortie`
   écrit `ligne.className = "invites-vide"`, il ne l'ajoute pas. Ne viser que `.noms-presence`
   ferait perdre la coupure à la carte où personne n'a encore répondu. */
/* **Le filet qui sépare la sortie de la question qu'on me pose** (04/09/2026). La passe du
   20/08 avait identifié cette frontière comme « celle qui compte le plus » et l'avait confiée à
   un écart de 16 px ; il ne suffisait pas, la carte se lisant toujours comme une pile de lignes
   au même x. **Le motif vient de la carte du mardi**, valeurs comprises — `#invites-sortie` y
   pose le même trait pour dire exactement la même chose, « la décision au-dessus, le contexte en
   dessous ». Ici les deux moitiés sont dans l'autre ordre : ce qu'est la sortie, puis ce qu'on me
   demande.

   **Le `margin-top` du dessus est repris ici**, sinon il s'ajouterait au `padding` du filet et la
   coupure vaudrait le double — il passe de `--espace-grand` (posé le 22/08, `D8`) à
   `--espace-bloc`, le palier qui dit une rupture entre deux zones et non deux blocs voisins.
   Les deux sélecteurs sont conservés pour la raison déjà écrite : l'état vide **remplace** la
   classe au lieu de s'y ajouter. */
.carte-sortie-libre > .noms-presence,
.carte-sortie-libre > .invites-vide {
  margin-top: var(--espace-bloc);
  padding-top: var(--espace-grand);
  border-top: 0.5px solid var(--couleur-bordure);
}

/* L'écart entre le pictogramme et les pseudos, le même que celui des compteurs du mardi. */
.carte-sortie-libre > .noms-presence > .ti {
  margin-right: var(--espace-minuscule);
}

/* Le joint « état du sondage → boutons », décidé en un seul endroit. La valeur générique de
   `.boutons-sortie-libre` (0,75 rem) sert le formulaire de proposition, où aucun état ne la
   précède ; ici elle s'ajouterait à celle du dessus.

   **`--espace-petit` depuis le 04/09/2026, signalé à l'usage** — « je trouve les boutons un peu
   collés à la liste des gens ». Le palier précédent, `--espace-minuscule`, est celui d'« une
   icône et son mot » : il décrivait un lien plus serré que celui de deux éléments empilés, et
   `--espace-petit` est justement défini comme « à l'intérieur d'un groupe : boutons d'une
   rangée ». C'est donc le rôle qui tranche, pas le goût.

   **Le rapport reste celui que la carte cherche** — serré dedans, large dehors : 8 px ici contre
   24 px et un filet au-dessus, là où le contraste était de 4 contre 16 avant cette passe. */
.carte-sortie-libre > .boutons-sortie-libre {
  margin-top: var(--espace-petit);
}

/* **Les invités d'une sortie libre**, en groupe à part sous les boutons du sondage
   (20/08/2026). Amener quelqu'un est un autre geste que dire si on vient.

   **La liste de pastilles partage sa déclaration avec celle de la carte du mardi**
   (`#liste-invites`, plus haut) : ce sont les mêmes pastilles, elles ne doivent pas diverger.
   La rangée de saisie partage la sienne avec `.ligne-ajout-invite`, qui existait déjà.

   **La coupure `.carte-sortie-libre > .invites-sortie-libre` a été retirée le 20/08/2026 au
   soir** : les invités sont descendus dans la vue détail le jour même, donc ce sélecteur ne
   trouvait plus rien — la zone vit sous `#detail-libre-invites`, hors de toute carte. Même
   famille de reste que les trois déclarations de présences supprimées plus haut. */

/* Le « + » tourne de 45° et devient une croix, comme le bouton du formulaire de proposition
   de cette page : le jeu d'icônes embarqué est un sous-ensemble de trente glyphes Tabler, et
   chacun de plus est une extraction à faire (voir docs/interface.md). */
.invites-sortie-libre .bouton-icone-invite i {
  transition: transform 0.15s ease;
}

.invites-sortie-libre .bouton-icone-invite[aria-expanded="true"] i {
  transform: rotate(45deg);
}

.invites-sortie-libre .ligne-ajout-invite[hidden] {
  display: none;
}

.invites-sortie-libre .ligne-ajout-invite input {
  flex: 1;
  margin: 0;
}

/* **« Voir le détail », le seul signal qui marche au doigt.** La carte s'ouvre au clic depuis
   le 20/08/2026, mais rien ne l'annonçait : `cursor: pointer` et `:hover` sont les deux
   signaux habituels, et **aucun des deux n'existe sur un téléphone**, qui est l'écran de
   référence du projet. Il fallait donc l'écrire.

   Aligné à droite et discret : c'est une sortie de secours vers le détail, pas une action
   concurrente du sondage juste au-dessus. La couleur d'accent le distingue du texte sans en
   faire un troisième bouton. */
.ouvrir-detail-sortie {
  display: flex;
  align-items: center;
  gap: var(--espace-minime);
  margin: var(--espace-petit) 0 0 auto;
  padding: var(--espace-minuscule) 0;
  background: none;
  border: none;
  color: var(--couleur-accent-texte);
  font-size: var(--texte-courant);
  cursor: pointer;
}

/* Un quart de tour, et le chevron vers le bas devient un chevron vers la droite. Voir la
   raison dans `sorties-pas-du-mardi.js` : chaque glyphe embarqué de plus est une extraction. */
.chevron-detail {
  transform: rotate(-90deg);
}

/* Le survol sur la carte entière, pour les postes fixes. Il redit ce que le bouton dit déjà,
   il ne le remplace pas — même teinte de bordure que les lignes cliquables de l'historique,
   pour que « ceci s'ouvre » se dise partout de la même façon. */
.carte-sortie-libre {
  cursor: pointer;
}

.carte-sortie-libre:hover {
  border-color: var(--couleur-accent);
}

/* Le formulaire de proposition : les champs s'empilent, le bouton prend toute la largeur.
   `.champ-recherche` et consorts portent déjà leur mise en forme, seul l'espacement du
   bloc reste à poser ici.

   **Il y avait ici un `#formulaire-sortie label`, retiré le 16/08/2026 — ne pas le
   remettre.** Il habillait des intitulés visibles qui n'existent pas dans ce bloc : les
   deux champs s'annoncent par un `aria-label` et un texte d'invite. Le seul `<label>` du
   formulaire est donc la case « bikepark ou shuttle » arrivée le même jour, et un sélecteur
   d'id l'emportant sur une classe, cette règle **neutralisait `.option-cochable` en
   entier** — `display: block` l'emportait sur `display: flex`, ce qui rendait du même coup
   `align-items` et `gap` inertes. La case retombait à l'alignement par défaut du navigateur,
   en plus petit et en couleur discrète : l'apparence exacte d'un intitulé, pour un contrôle
   (voir la règle des trois rôles). Un même contrôle avait donc deux apparences selon
   l'écran, ce que la déclaration partagée existe précisément pour empêcher.

   **Des intitulés visibles y sont arrivés le 12/09/2026**, et ce piège dit comment les poser : par
   la classe `.champ-etiquete` de l'atelier, jamais par ce sélecteur d'id, qui rattraperait la case
   à cocher. « Des intitulés visibles qui n'existent pas dans ce bloc » était vrai à sa date. */
#formulaire-sortie button {
  width: 100%;
}

/* ---------- Carte en plein écran ---------- */

/* Même calage que la visionneuse de photos, et **un `<dialog>` comme elle** depuis le
   15/08/2026. Le `z-index` 920 « glissé entre le voile et les popups » a disparu avec le
   voile : la couche supérieure ordonne les modales par ordre d'ouverture, donc la
   confirmation de retrait passe au-dessus sans arbitrage.

   Pas de `background: transparent` ici, contrairement à la visionneuse : la carte est
   opaque par construction, elle couvre son propre fond. Une colonne : la carte prend toute
   la place restante, la barre s'installe en bas, là où le pouce arrive. */
.carte-plein-ecran {
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  border: none;
  padding: 0;
  color: inherit;
  background-color: var(--couleur-fond);
}

/* Voir la visionneuse : `display` ne peut vivre que sous `[open]`. */
.carte-plein-ecran[open] {
  display: flex;
  flex-direction: column;
}

.carte-plein-ecran[hidden] {
  display: none;
}

/* **`touch-action: none` est indispensable, pas une précaution** : sans lui, le
   navigateur traite les glissements comme du défilement de page et le pincement comme un
   zoom de page — il s'approprie les gestes avant que le code les voie. `min-height: 0`
   pour qu'un enfant flex accepte de rétrécir, même raison que l'image de la visionneuse.
   `overflow: hidden` parce que les tuiles dépassent volontairement du cadre.

   **`isolation: isolate` répare un bouton devenu invisible, et c'est le piège que Leaflet
   apporte avec lui.** Leaflet range ses sept panneaux à des `z-index` de 200 à 700, mais
   son conteneur est en `position: relative; z-index: auto` — donc il **ne crée aucun
   contexte d'empilement**, et ces valeurs vont concourir avec les voisins du conteneur au
   lieu de rester chez lui. `.fermer-carte`, posée juste au-dessus sans `z-index`, se
   retrouvait à 0 face aux tuiles à 200 : le bouton restait cliquable — donc les tests au
   clic ne voyaient rien — mais **plus rien ne le peignait**. Constaté au navigateur le
   17/08/2026, dans les deux thèmes.

   `isolation` plutôt qu'un `z-index` sur le bouton : ça enferme les sept panneaux d'un
   coup et **n'ajoute aucun palier** à l'escalier que le passage aux `<dialog>` avait
   justement fait redescendre (voir Composants et mise en page dans `docs/interface.md`).
   Règle générale : tout conteneur de bibliothèque qui pose ses propres `z-index` doit être
   isolé, sans quoi ses valeurs débordent sur la page d'accueil. */
.scene-carte {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  isolation: isolate;
}

/* ---------- Leaflet (carte en plein écran, depuis le 15/08/2026) ---------- */

/* Trois retouches seulement, et aucune n'est cosmétique — elles annulent des choix de la
   feuille de style de Leaflet qui entrent en conflit avec le projet. */

/* Leaflet peint son conteneur en `#ddd`, un gris clair qui se verrait sous les tuiles le
   temps de leur chargement et **au milieu d'une interface sombre**. On reprend le fond du
   thème, comme le faisait la scène SVG. */
.scene-carte.leaflet-container {
  background-color: var(--couleur-fond);
}

/* Les chevrons de sens sont des marqueurs à icône SVG. `L.divIcon` habille sa boîte d'un
   fond blanc et d'une bordure dès qu'on lui laisse sa classe par défaut : on la remplace
   par celle-ci, qui ne fait que porter le SVG. La forme, elle, garde `.chevron-trace` et
   donc exactement les couleurs de la vignette. */
.chevron-leaflet {
  background: none;
  border: none;
}

/* Le halo et le tracé prennent leurs `stroke` et `stroke-width` des règles existantes.
   **Rien à ajouter, et c'est le point intéressant** : Leaflet pose ses propres valeurs en
   *attributs de présentation*, que n'importe quelle règle CSS bat — les classes du projet
   gagnent donc sans qu'on ait à forcer quoi que ce soit. Et comme Leaflet ne met aucun
   `viewBox` à l'échelle, l'épaisseur est en pixels CSS par construction : le défaut de
   densité d'écran corrigé le 15/08/2026 ne peut pas s'y produire. */

/* La croix au coin haut-droit de l'écran : ici la carte occupe tout, donc le coin de
   l'écran **est** le coin de la carte — contrairement à la visionneuse de photos, où il
   fallait une enveloppe épousant l'image. Elle reprend l'habillage de sa jumelle. */
.fermer-carte {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: var(--espace-petit);
  line-height: 0;
  border-radius: 50%;
  background-color: var(--couleur-surface-teintee);
}

.fermer-carte .ti {
  font-size: var(--icone-bouton);
}

/* `flex-wrap` : sur 288 px, les trois calques et les deux boutons de zoom ne tiennent pas
   sur une ligne, et la mention légale passera dessous plutôt que d'écraser le reste. */
.barre-carte {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  padding: var(--espace-petit) var(--espace-moyen);
  background-color: var(--couleur-surface-teintee);
  border-top: 1px solid var(--couleur-bordure);
}

.choix-calques {
  display: flex;
  gap: var(--espace-minuscule);
}

/* Boutons de calque : `.actif` reprend le marquage des onglets de sortie — fond de
   surface et bordure d'accent — plutôt que d'inventer un troisième langage de sélection
   dans le projet. */
.choix-calques button {
  padding: var(--espace-minuscule) var(--espace-petit);
  font-size: var(--texte-etiquette);
  background-color: transparent;
  border: 1px solid var(--couleur-bordure);
}

.choix-calques button.actif {
  border-color: var(--couleur-accent);
  color: var(--couleur-accent-texte);
}

.zoom-carte {
  display: flex;
  gap: var(--espace-minuscule);
}

/* La mention légale du plein écran, elle, **est un lien** — il y a la place, et un lien
   vers la licence est ce que les fournisseurs demandent quand c'est possible. Elle prend
   toute la largeur restante pour passer à la ligne proprement. */
.attribution-plein-ecran {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--texte-mention);
  color: var(--couleur-texte-discret);
}

/* Bordure tiretée reprise de la tuile d'ajout de photo, où elle dit « emplacement à
   remplir » plutôt que « contenu ». Déclaration séparée malgré cette parenté, et
   c'est volontaire : les deux ne se côtoient jamais et n'ont pas le même gabarit —
   un carré dans une grille là, une bande sur toute la largeur ici, puisqu'une sortie
   n'a qu'une trace et qu'il n'y a donc aucune grille où s'insérer. Le centrage est
   explicite parce qu'un <label> n'a rien du comportement d'un bouton. */
.depot-trace {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  padding: var(--espace-grand);
  border: 1.5px dashed var(--couleur-accent);
  border-radius: 8px;
  font-size: var(--texte-courant);
  color: var(--couleur-texte-discret);
  cursor: pointer;
}

/* **Les trois boutons de la suggestion de parcours** (08/09/2026).

   **Le principal seul sur sa ligne, les autres se partageant la suivante**, et ce n'est pas
   une préférence : « Va pour celle-là », « Non, j'ai mon idée » et « Une autre » font 45
   caractères, là où `body` est plafonné à 480 px et que la largeur disponible ne descend qu'à
   288 px. Trois colonnes y auraient donné des boutons de 90 px.

   **`flex` et non la grille de `.boutons-correction`**, parce que le nombre de boutons varie :
   « Une autre » n'apparaît que s'il reste un candidat. Avec `flex: 1 1 0` le seul bouton
   secondaire restant prend toute sa ligne au lieu de laisser une colonne vide — une grille
   aurait demandé un sélecteur sur le rang pour obtenir le même résultat.

   **La police adaptative et `min-width: 0`** viennent de `.boutons-correction`, pour la même
   raison qu'elle : sans le second, une colonne flex ne peut pas rétrécir sous la largeur de son
   contenu et déborde. */
.boutons-suggestion-trace {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-petit);
}

.boutons-suggestion-trace > .bouton-principal {
  flex: 1 0 100%;
}

.boutons-suggestion-trace > .bouton-secondaire {
  flex: 1 1 0;
  min-width: 0;
  /* **Le vertical vient du bouton principal, pas de `.boutons-correction`.** Mesuré à 390 px :
     la valeur empruntée à la grille de correction rendait 28 px de haut contre 44 px au
     principal juste au-dessus — sous la cible tactile confortable, et une rangée de guingois
     puisque les trois boutons se suivent. La grille de correction, elle, n'a que des boutons
     de même gabarit entre eux : la valeur y était cohérente, elle ne l'est pas ici.
     L'horizontal reste serré, c'est lui qui laisse « Non, j'ai mon idée » tenir sur une ligne. */
  padding: var(--espace-moyen) clamp(0.2rem, 1vw, 0.5rem);
  font-size: var(--texte-bouton-adaptatif);
  overflow-wrap: break-word;
}

/* **Le bébé du membre junior** (08/09/2026, chantier `AG`).

   **En `em` et non une taille de l'échelle** : le bébé vit *à l'intérieur* d'un texte dont la
   taille varie selon l'endroit — un pseudo de sondage, une carte d'admin, un nom de présence. Une
   valeur en `em` le fait suivre son voisin, là où un rôle de l'échelle l'aurait figé et rendu
   énorme à côté d'une étiquette. Ce n'est pas un espacement, donc la règle des rôles ne s'y
   applique pas — même raison qui met `.ti` en `em` (voir le bloc des icônes).

   **0,9 em et non 1** : un emoji est dessiné plein, sur toute sa cadratin, là où un pictogramme
   au trait laisse du blanc. À taille égale il pèse donc plus lourd à côté d'un pseudo.

   **La gouttière gauche est un espacement, elle**, donc elle vient de l'échelle.

   **Il apporte sa couleur, et c'est la différence avec un pictogramme du jeu** : ceux-là sont des
   masques peints en `currentColor`, un emoji non. C'est le seul marqueur du projet dans ce cas,
   et c'est assumé — « on le chambre avec bienveillance » demande qu'il se voie. **Conséquence à
   connaître** : la sonde 4 des contrôles d'écran mesure le contraste des *pictogrammes*, elle ne
   verra jamais celui-ci. */
.marque-junior {
  font-size: 0.9em;
  margin-left: var(--espace-minuscule);
  /* Un emoji s'assoit sur la ligne de base et déborde sous elle : on le remonte un peu pour
     qu'il s'aligne sur le corps du texte plutôt que de pendre. */
  vertical-align: -0.08em;
}

/* **Un nom et son bébé ne se séparent jamais.** Les listes de présence se coupent en fin de
   ligne comme du texte — c'est ce qui les garde compactes, mesuré le 20/08/2026 —, et sans ceci
   un bébé se retrouverait seul en tête de ligne suivante, orphelin de son pseudo. */
.nom-liste {
  white-space: nowrap;
}

/* Texte réservé aux lecteurs d'écran : présent dans le document, invisible à l'œil.
   **Premier besoin du projet**, apparu avec la marque des sorties pas du mardi, réduite à
   une icône — une icône seule ne dit **rien** à qui ne la voit pas.

   Pourquoi pas `display: none` ni `visibility: hidden` : les deux retirent l'élément de
   l'arbre d'accessibilité, donc du texte qu'un lecteur d'écran énonce. Pourquoi pas
   `font-size: 0` ni un retrait de 9999 px : le premier laisse parfois le texte
   sélectionnable et le second casse le défilement sur certains navigateurs. Le carré de
   1 px avec `clip-path` est le seul procédé qui masque à l'œil **sans** rien retirer à
   l'accessibilité. */
.texte-lecteur-ecran {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Dépôt d'une trace partagée ---------- */

/* Les sorties candidates s'empilent en pleine largeur : c'est une liste de choix qu'on
   touche au pouce, sur une page atteinte depuis un téléphone et nulle part ailleurs. Même
   disposition que les boutons du formulaire de voyage. */
.liste-candidates {
  display: flex;
  flex-direction: column;
  gap: var(--espace-petit);
  margin-bottom: var(--espace-grand);
}

.liste-candidates button,
#bouton-confirmer-depot {
  width: 100%;
}

/* L'intitulé du champ de date des pages de dépôt. Discret et en petit : c'est une **porte de
   sortie**, pas l'action principale — on choisit dans la liste neuf fois sur dix, et on cherche
   une date le jour où on alimente un vieux mardi.

   `display: block` pour que le champ passe dessous : un `<label>` est en ligne par défaut, et
   l'intitulé se collerait au champ sur la même ligne, où il déborderait sur 288 px. */
.etiquette-date-depot {
  display: block;
  margin-bottom: var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* Une sortie déjà tracée reste **visible mais inerte** : elle explique pourquoi elle n'est
   pas choisissable, là où l'omettre laisserait chercher une sortie absente de la liste. */
.liste-candidates button:disabled,
#bouton-confirmer-depot:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---------- Réactions ---------- */

/* **Une seule déclaration pour les deux pouces** — celui d'un commentaire et celui de la
   visionneuse. Ils se retrouvent parfois sur le même écran à quelques centimètres l'un de
   l'autre, et deux règles recopiées dériveraient (voir Conventions dans CLAUDE.md, le cas
   fondateur du `<label>` passé à 0,9 rem contre 1 rem).

   `--texte-courant` et non `--texte-etiquette` : c'est un **contrôle**, et les contrôles
   restent au palier courant pour être lisibles au doigt (voir la règle des trois rôles). */
.bouton-reaction {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  padding: var(--espace-minuscule) 0;
  background: none;
  border: none;
  line-height: 1;
  cursor: pointer;
  font-size: var(--texte-courant);
  /* Discret tant qu'on n'a pas aimé : le geste se propose, il ne se réclame pas. */
  color: var(--couleur-texte-discret);
}

/* Aimé : la couleur prend le relais, **pas la taille**. Faire grossir le bouton ferait
   sautiller la ligne à chaque tape. `--couleur-reaction` porte l'accent du thème — un pouce
   « aimé » doit se lire comme un accent de la palette, pas comme une couleur importée. */
.bouton-reaction.reaction-mienne {
  color: var(--couleur-reaction);
}

.bouton-reaction .ti {
  font-size: var(--icone-texte);
}

/* Le compteur garde sa place même vide : sans cette largeur minimale, le pouce se
   décalerait latéralement au premier j'aime. */
.compteur-reaction {
  min-width: 0.6rem;
  text-align: left;
}

/* La pastille d'une vignette. **Ce n'est pas un bouton** : la vignette en est déjà un, et
   imbriquer deux boutons est du HTML invalide — on aime depuis la visionneuse. Elle ne dit
   donc que combien, et n'apparaît qu'à partir d'un pouce.

   Fond sombre fixe et texte blanc, indépendants du thème : elle est posée sur une photo,
   dont on ne sait rien. C'est la même raison qui fait vivre `--couleur-halo-trace` en
   dehors des blocs de thème. */
/* **Les pastilles vivent dans un conteneur positionné, pas positionnées chacune** : depuis
   qu'une vignette peut en porter deux (un pouce et un doigt), deux `position: absolute` au même
   coin se superposaient — constaté à l'écran, la seconde masquait la première. */
.pastilles-reaction {
  position: absolute;
  right: 0.25rem;
  bottom: 0.25rem;
  display: flex;
  gap: var(--espace-minuscule);
}

.pastille-reaction {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
  padding: var(--espace-minime) var(--espace-minuscule);
  border-radius: 10px;
  background-color: rgba(0, 0, 0, 0.55);
  color: #FFFFFF;
  font-size: var(--texte-mention);
  line-height: 1.4;
}

.pastille-reaction .ti {
  font-size: var(--icone-petite);
}

/* ---------- Animations ---------- */

/* Ajoutées le 15/08/2026, sur demande. Toutes suivent trois règles :
   - elles **rendent visible quelque chose qui existe déjà** et qu'on ne voyait pas, plutôt
     que d'habiller ce qui se lit très bien immobile ;
   - elles se déclenchent une fois et s'arrêtent — aucune animation permanente dans le champ
     de vision, ce qui fatigue et finit par se faire ignorer ;
   - elles disparaissent toutes sous `prefers-reduced-motion`, voir le bloc en fin de
     section. */

/* **Le tracé se dessine.** `pathLength="1"` est posé sur le chemin par `trace.js` : il
   normalise la longueur du parcours à 1 quelle que soit la trace, ce qui rend l'animation
   écrivable en CSS pur — sans lui il faudrait mesurer chaque chemin en JS et poser une
   valeur en dur, donc une exception de plus à « le CSS ne vit que dans la feuille ».

   Seul le **halo** se dessine, parce qu'il est d'un seul tenant : les segments colorés sont
   une dizaine de morceaux distincts qui, animés ensemble, donneraient une trace qui
   apparaît en pièces détachées plutôt qu'un trait qui avance. Ils se révèlent après, une
   fois le chemin parcouru. */
@keyframes tracer-chemin {
  from { stroke-dashoffset: 1; }
  to { stroke-dashoffset: 0; }
}

.vignette-carte .trace-halo {
  stroke-dasharray: 1;
  animation: tracer-chemin 1.1s ease-out;
}

@keyframes revenir {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* `backwards` est indispensable : sans lui, les segments sont peints en clair pendant la
   seconde d'attente, et l'effet de dessin ne se voit pas du tout. */
.vignette-carte .trace-chemin,
.vignette-carte .chevron-trace,
.vignette-carte .repere-trace {
  animation: revenir 0.45s ease-out 0.85s backwards;
}

/* **Un commentaire qui arrive pendant qu'on regarde** glisse à sa place. Le temps réel du
   projet existe depuis longtemps mais il est *invisible* : un message apparaissait entre
   deux clignements, sans que rien ne signale qu'il venait de tomber. La classe n'est posée
   que sur les commentaires **jamais vus**, jamais au premier chargement (voir
   `commentaires.js`). */
@keyframes arriver {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to { opacity: 1; transform: none; }
}

.commentaire-nouveau {
  animation: arriver 0.4s ease-out;
}

/* **Le pouce rebondit** au relâchement. C'est le geste le plus léger de l'application, il
   méritait le retour le plus léger — et il n'en avait aucun : le compteur changeait, sans
   qu'on sache si la tape avait porté. */
@keyframes rebondir {
  0% { transform: scale(1); }
  40% { transform: scale(1.25); }
  100% { transform: scale(1); }
}

.reaction-tapee {
  animation: rebondir 0.35s ease-out;
}

/* **Les compteurs comptent.** La valeur monte de l'ancienne à la nouvelle quand une réponse
   arrive, au lieu de sauter. C'est `reponses.js` qui interpole ; le CSS ne fait que teinter
   brièvement le chiffre qui bouge, pour que l'œil sache lequel regarder. */
@keyframes compteur-change {
  from { color: var(--couleur-accent-secondaire-texte); }
  to { color: inherit; }
}

.compteur-anime {
  animation: compteur-change 0.9s ease-out;
}

/* **Le réglage du système passe avant la feuille**, et il passe par les PROPRIÉTÉS plutôt que
   par une liste de sélecteurs. Les deux blocs énumérés qui vivaient ici ont été remplacés le
   22/08/2026, pour deux défauts mesurés qui sont les deux façons de rater une liste (`D4` et
   `D5` de `docs/revue-etats-2026-08-21.md`) :

   - une liste **se contourne par la cascade** — `.bandeau-ephemere` y figurait et animait quand
     même, sa règle vivant 300 lignes plus bas, à spécificité égale ;
   - une liste **s'oublie** — `.carte-flash` n'y a jamais figuré, et les cinq `transition` du
     projet non plus, dont le glissement de 260 à 320 px des deux panneaux latéraux, sur les neuf
     pages qui portent un menu.

   Une liste posée au milieu de 5000 lignes demande donc qu'on se souvienne de deux choses à
   chaque ajout : l'y inscrire, et le faire au-dessus. Une neutralisation par propriété ne
   demande rien.

   **`!important` est ici le seul endroit du projet où ce n'est pas une odeur** — et c'est le
   premier : une préférence système doit gagner sur ce que l'auteur a écrit, y compris sur ce
   qu'il écrira demain en bas de feuille. C'est exactement ce que `D5` a coûté.

   **`0.01ms` et non `none`** : une durée nulle ne déclenche pas `animationend`, et
   `flasherCarte()` (`admin.js`) retire sa classe sur cet événement — avec `none` elle
   resterait posée pour toujours. Même raison côté Leaflet, qui écoute `transitionend` pour
   finir son zoom.

   **`animation-delay` et `transition-delay` ne sont pas décoratifs** : sans eux, `revenir`
   garde son attente de 0,85 s et la trace resterait invisible presque une seconde.

   **Le périmètre est volontairement plus large que le mouvement strict.** Il emporte aussi la
   transition de couleur de la pastille de thème, qui n'est pas du mouvement au sens de WCAG
   2.3.3 : c'est un élargissement assumé et non un oubli, il ne coûte aucun sens, et le refuser
   imposerait de revenir à une liste — précisément ce qui a échoué deux fois. Il emporte de même
   les transitions de `leaflet.css`, **sans toucher au fichier vendorisé**, ce qui est un
   avantage de cette forme : un zoom de carte en plein écran est le haut de la liste de 2.3.3.

   **Ce que le CSS ne peut pas atteindre reste à faire à la main** : `animerNombre()` de
   `reponses.js` fait défiler son compteur en JavaScript, et porte donc son propre garde-fou. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }

  /* **Une exception, et elle est dans le bloc plutôt qu'en dehors** (22/08/2026) : la règle
     universelle ci-dessus emportait `.carte-flash`, et c'était une **régression**. Le
     commentaire qui justifiait le périmètre large disait « il ne coûte aucun sens » : vrai de
     la teinte de la pastille de thème, **faux de celle-ci**. Un flash de fond n'est pas du
     mouvement au sens de WCAG 2.3.3 — c'est un **retour d'action**, et le seul de trois gestes
     de l'administration. « Régénérer » un jeton change le lien au milieu d'une URL de soixante
     caractères : sans le flash, plus rien ne dit que quelque chose s'est passé.

     **Elle vit à l'intérieur du même bloc**, et c'est ce qui compte : une exception posée
     ailleurs dans la feuille rouvrirait exactement la dette d'ordre d'écriture que ce bloc a
     été écrit pour fermer (`D5`). Ici elle ne peut ni s'oublier ni se faire contourner. */
  .carte-flash {
    animation-duration: 1.8s !important;
  }
}

/* **La suppression d'une sortie, en pied de l'onglet « Sortie » de l'administration.** Séparée
   des actions de sondage par un trait : ce n'est pas une action de plus sur la même ligne, c'est
   un geste irréversible sur l'objet entier — le contraste d'espacement et le trait le disent
   avant que le bouton rouge ne le confirme. */
/* **Un bloc détaché de ce qui précède par un filet** — quatre zones, une seule déclaration
   (28/08/2026, `RC-CODE-046`). Posé sur l'élément qui **suit**, comme tous les séparateurs du
   projet (voir `#item-theme` dans `docs/interface.md`).

   **Le regroupement avait déjà été fait pour deux des quatre le 21/08/2026**, avec cet
   argument : « les deux zones font la même chose au même endroit — une action détachée de la
   liste qui la précède, séparée par un filet. La suppression d'une sortie et l'envoi d'un
   souvenir sont dans deux panneaux différents de la même page, donc un écart entre les deux se
   verrait en changeant d'onglet. » Il valait pour les quatre, et les deux autres gardaient
   chacune leur copie du même corps : **la convention était connue, invoquée, et appliquée à
   moitié**. C'est ce qui rend ce groupe-ci le plus instructif des trois.

   Ce que chacune sépare : le bloc du nouveau venu de son formulaire — sans le filet, le lien se
   lit comme un champ de plus ; la note qui présente une liste du formulaire de tête ; et les
   deux actions de fin de panneau. */
#nouveau-membre,
.intro-liste-admin,
.zone-suppression-sortie,
.zone-souvenir-admin {
  margin-top: var(--espace-bloc);
  padding-top: var(--espace-grand);
  border-top: 1px solid var(--couleur-bordure);
}

.zone-suppression-sortie[hidden] {
  display: none;
}

.note-suppression-sortie {
  margin: var(--espace-petit) 0 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* **La rangée de réactions** : un ou deux groupes, chacun étant un contrôle et les pseudos de
   ceux qui l'ont choisi (20/08/2026). `flex-wrap` parce que « Lucas, Benoît, Guillaume » suivi
   d'un second groupe ne tient pas sur 390 px, et que la ligne doit se replier plutôt que
   déborder. L'écart entre deux groupes est plus large que celui d'un groupe à ses pseudos :
   serré à l'intérieur, large entre deux (voir les Conventions de CLAUDE.md). */
.rangee-reactions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--espace-minime) var(--espace-grand);
}

.groupe-reaction {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
}


/* **Le pseudo du membre courant, en tête du menu** (20/08/2026). Séparé des destinations par un
   trait, comme le choix de thème l'est en pied : ce n'est pas un endroit où aller, c'est une
   information sur qui regarde.

   Le pseudo reprend exactement le traitement des noms d'hôte — `--police-affichage` en italique,
   second accent — parce que c'est la même chose : le nom d'un membre du groupe. Il partage donc
   sa déclaration avec `.pseudo-hote` plutôt que d'en recopier une (voir les Conventions de
   CLAUDE.md). */
/* **C'est un lien depuis le 20/08/2026, et il garde son habillage propre** : l'en-tête du
   menu, séparé de la liste par son trait, et non un item de navigation de plus. D'où le
   `text-decoration` annulé — ce qui dit qu'il s'ouvre est le chevron, pas un soulignement qui
   le rangerait avec les autres. */
/* **Il est passé dans le coin haut droit du panneau le 27/08/2026**, sur demande — « pour
   optimiser l'espace ». Il occupait une rangée pleine largeur en tête de liste, sous les trois
   traits ; il occupe désormais la **bande vide** que le `padding-top: 4rem` de la liste réserve
   au bouton de menu, à droite de celui-ci. La place gagnée est celle d'une rangée entière, et
   elle ne coûte rien : cette bande ne portait rien.

   **Positionné en absolu depuis le CSS, et pas déplacé dans le HTML** : le bloc du menu est
   recopié à l'identique sur les six pages qui en portent un, donc le déplacer aurait été six
   occasions d'en oublier une — et l'oubli n'aurait pas fait d'erreur, juste une page au menu
   différent. `.panneau-lateral` est en `position: fixed`, donc il fait déjà contexte.

   **Aligné sur le bouton de menu** (`top: 1rem`, hauteur de cible 40 px) : les deux se lisent
   comme une seule barre en haut du panneau. */
#item-pseudo-courant {
  position: absolute;
  top: 1rem;
  right: 1rem;
  /* Ne jamais passer sous les trois traits, même avec un long pseudo : 40 px de bouton plus
     ses marges. Le pseudo s'y coupe plutôt que de déborder. */
  max-width: calc(100% - 5rem);
}

/* **Une pastille et non du texte posé là** (06/09/2026, à la demande : « comment rendre plus
   identifiable le fait qu'on puisse accéder à son compte »).

   **Ce qui a été mesuré avant d'y toucher.** C'est le **seul** point d'entrée vers `compte.html`
   de toute l'application — un unique `href` dans les neuf menus. Sa cible faisait **57 px de
   large** dans un panneau de 260, là où chaque item de navigation en fait 228 : un quart de la
   rangée. Et le pseudo porte la couleur des **noms** du projet, celle du contenu, donc il se
   lisait comme une étiquette « voilà qui tu es » plutôt que comme une destination.

   **Le coin est conservé, et c'est délibéré** : il y a été mis le 27/08/2026 sur demande, « pour
   optimiser l'espace », et le remettre en rangée pleine largeur défairait ce gain. La lisibilité
   est donc allée chercher **dans le coin** ce qu'une rangée aurait donné gratuitement — un bord,
   une surface au toucher, un chevron.

   **Le bord fait tout le travail** : sur un panneau uni, un rectangle arrondi se lit comme un
   contrôle, là où du texte se lit comme un titre. Il prend `--couleur-bordure`, le jeton que le
   projet emploie déjà pour ça, donc il suit les trois thèmes sans une ligne de plus. */
.pseudo-courant {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  /* La hauteur du bouton de menu : c'est aussi la cible tactile minimale du projet. */
  min-height: 40px;
  padding: 0 var(--espace-petit);
  border: 1px solid var(--couleur-bordure);
  border-radius: 8px;
  color: var(--couleur-texte-discret);
  text-decoration: none;
}

/* Un pseudo long se coupe au lieu de pousser la pastille sous le bouton de menu. */
.pseudo-courant span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* **Le chevron est revenu le 06/09/2026**, et l'argument qui l'avait masqué s'est retourné tout
   seul. Il disait : « il marquait le bord droit d'une ligne pleine largeur ; dans un coin, il n'a
   plus de bord à marquer ». C'était juste tant que l'entrée était du texte nu — **la pastille lui
   rend un bord**, celui de son propre cadre, et il y dit à nouveau « ça s'ouvre ».

   **Il était resté dans le HTML des neuf pages** pendant qu'il était masqué, pour ne pas éditer
   neuf fichiers : c'est ce qui rend ce retour gratuit, une seule ligne de feuille.

   `--icone-petite` et la couleur discrète : il accompagne, il n'annonce pas. */
.chevron-pseudo {
  font-size: var(--icone-petite);
  color: var(--couleur-texte-discret);
}

#item-pseudo-courant[hidden] {
  display: none;
}

/* **La cloche du fil, en miroir du bouton de menu** : même pastille, même taille de cible, même
   `z-index`, mais à droite. Les deux encadrent le haut de l'écran, ce qui rend leur symétrie
   lisible — et depuis le 22/08/2026 ils **partagent réellement leur déclaration**, posée avec
   celle du menu (voir plus haut). Il ne reste ici que ce qui les distingue.

   **Elles ne partagent pourtant PAS leur déclaration**, contrairement à ce que cette phrase a
   affirmé jusqu'au 22/08/2026 : ce sont deux blocs distincts, à plus de quatre mille lignes
   d'écart, qui répètent douze déclarations identiques. Le coût s'est vu le jour où il a fallu
   poser la même couleur de glyphe sur les deux (`D3`) — une seule décision, deux éditions,
   exactement le mode d'échec de `.etiquette-bouton` que les Conventions de `CLAUDE.md`
   décrivent. **La fusion reste à faire** ; ce qui est corrigé ici, c'est le commentaire qui
   affirmait le contraire de ce que fait la feuille. */
#bouton-fil {
  /* Le `position: fixed` de la déclaration partagée fait de lui un contexte de positionnement,
     ce dont la pastille des non-lus a besoin ; un `relative` posé ici serait ignoré. */
  right: 1rem;
}

#bouton-fil .ti {
  font-size: var(--icone-bouton);
}

/* Le panneau vient de la droite. Il **hérite de `.panneau-lateral`** et n'en change que le côté :
   même largeur, même animation, même filet de défilement pour les écrans bas. */
.panneau-droite {
  left: auto;
  right: 0;
  width: min(320px, 88vw);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
  transform: translateX(100%);
  padding: 4.25rem var(--espace-moyen) var(--espace-grand);
  text-align: left;
}

.titre-fil {
  margin: 0 0 var(--espace-petit) var(--espace-petit);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* Une ligne du fil : l'icône dit le genre d'événement, la phrase ce qui s'est passé, la mention
   la cible et le moment. **`<a>` ou `<div>` selon qu'il y a où aller** — d'où la remise à zéro
   des styles de lien, le bloc ne devant pas se distinguer d'un autre par un soulignement. */
.ligne-fil {
  display: flex;
  gap: var(--espace-petit);
  align-items: flex-start;
  padding: var(--espace-moyen) var(--espace-petit);
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
}

a.ligne-fil:hover,
a.ligne-fil:focus-visible {
  background-color: var(--couleur-surface-encastree);
}

.ligne-fil .ti {
  flex: none;
  margin-top: var(--espace-minime);
  font-size: var(--icone-texte);
  color: var(--couleur-texte-discret);
}

.phrase-fil {
  margin: 0;
  font-size: var(--texte-courant);
  line-height: 1.35;
}

.mention-fil {
  margin: var(--espace-minime) 0 0;
  font-size: var(--texte-mention);
  color: var(--couleur-texte-discret);
}

/* **La pastille des non-lus**, posée sur la cloche. Le second accent et non l'accent principal :
   celui-ci habille déjà l'icône de la cloche, et une pastille de la même couleur sur son propre
   fond ne se détacherait pas.

   **Le texte prend le rôle de la palette, pas un blanc fixe** (22/08/2026, `D2` de
   `docs/revue-etats-2026-08-21.md`). Le rapprochement avec la pastille de réaction était faux :
   celle-là est posée sur une **photo**, dont le thème ne sait effectivement rien, alors qu'ici le
   fond est une couleur du thème — donc le thème sait quoi écrire dessus, et
   `--couleur-texte-sur-accent-secondaire` existe exactement pour ça. Un blanc fixe donnait
   **2,68:1** en thème sombre, l'accent secondaire y étant un rose clair ; le rôle donne 6,62:1, et
   ne change rien aux deux thèmes clairs où il vaut `#FFFFFF` (mesuré sur les trois). C'est le
   couple qu'emploie déjà `.bandeau-ephemere`, posé sur ce même fond. */
#pastille-fil {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  border-radius: 9px;
  background-color: var(--couleur-accent-secondaire);
  color: var(--couleur-texte-sur-accent-secondaire);
  font-size: var(--texte-mention);
  font-weight: 600;
  line-height: 1;
}

#pastille-fil[hidden] {
  display: none;
}

/* ------------------------------------------------------------------ La page « compte »

   Chantier `M` de docs/roadmap.md. Les cartes elles-mêmes sont celles des statistiques,
   déclaration partagée plus haut — il ne reste ici que ce qui leur est propre.

   **L'espacement suit la règle du projet : serré dans un groupe, large entre deux groupes.**
   Le titre et sa note disent la même chose et se collent ; l'état et les boutons forment le
   second groupe, et c'est le blanc entre les deux qui donne sa structure à la carte. Un `gap`
   uniforme les aurait tous mis à plat, exactement le défaut payé sur la carte d'une sortie
   libre le 16/08/2026.

   **Un seul alignement, celui de l'application** : `body` centre tout, ces cartes aussi. Le
   premier jet posait un `display: flex` sur le titre pour y loger une icône, ce qui le
   ramenait à gauche — d'où titre à gauche, note centrée, état centré et boutons à gauche
   dans la même carte. Quatre alignements pour trois lignes, exactement ce que la règle
   ci-dessus dit de ne pas faire : c'est le contraste d'espacement qui structure, jamais
   l'alignement. Vu au navigateur, pas à la lecture. */
.titre-carte-compte {
  margin: 0 0 var(--espace-minuscule);
  font-family: var(--police-affichage);
  font-style: italic;
  font-size: var(--titre-section);
}

.note-compte {
  margin: 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.actions-compte {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--espace-petit);
  margin-top: var(--espace-grand);
}

/* **Le `gap` porte l'écart, pas la marge du champ** (04/09/2026, `RC-UX-064`). Un champ hérite de
   `margin: 0.25rem 0 0.75rem` — la paire qui porte les 16 px entre deux champs **empilés**
   (`D14`) —, or ici il est dans une **rangée**, où les marges ne fusionnent pas avec le `gap` :
   elles s'y **ajoutent**. Mesuré, 12 + 8 = **20 px** rendus entre le champ et son bouton, une
   valeur hors échelle.
   **Un joint, une seule règle** — c'est la même correction que `.champ-rappel` le même jour, et
   le même mécanisme que les six cas de « sortir un élément d'un conteneur qui annule les
   marges ». */
.actions-compte input {
  margin: 0;
}

/* **Le lien se coupe n'importe où**, `anywhere` et pas `break-word` : un jeton est une suite
   de 36 caractères sans espace ni tiret exploitable, et sans cette règle il déborde de la
   carte sur un téléphone étroit au lieu de passer à la ligne. */
.lien-personnel-compte {
  margin: var(--espace-moyen) 0 0;
  padding: var(--espace-petit) var(--espace-moyen);
  border-radius: 10px;
  background-color: var(--couleur-surface-encastree);
  border: 1px solid var(--couleur-bordure);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  overflow-wrap: anywhere;
}

/* ---------- L'avatar (27/08/2026, chantier `M`) ----------

   **Une seule déclaration pour la photo et pour l'initiale**, et c'est la règle du projet :
   deux éléments qui doivent se ressembler partagent leur déclaration au lieu de la recopier
   (le cas fondateur est `.etiquette-bouton`, qui avait dérivé à 0,9 rem). Ici l'enjeu est
   concret : dans un fil de commentaires, une photo et une initiale se suivent ligne après
   ligne, et un pixel d'écart ferait bouger les pseudos selon qui a mis une tête.

   **`object-fit: cover` malgré le recadrage carré fait au navigateur** : la ceinture et les
   bretelles ne coûtent rien ici, et les images déjà en base le jour où l'on changerait de
   côté ne se déformeraient pas. */
.avatar {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  background-color: var(--couleur-surface-encastree);
  /* **Sans ça, la déclaration partagée ne partage plus rien** : l'initiale porte une bordure
     d'un pixel, la photo non, donc le rond de celui qui n'a pas de tête faisait 98 px là où
     l'autre en fait 96. Mesuré au navigateur, invisible à la lecture — le projet n'a pas de
     `box-sizing` global. */
  box-sizing: border-box;
}

.avatar-initiale {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--couleur-bordure);
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  /* Une initiale seule dans un rond ne se lit bien qu'un peu appuyée. */
  font-weight: 600;
  /* Une lettre n'est pas une image : sans ça, un pseudo accentué déborde du rond. */
  line-height: 1;
}

/* Toute règle qui pose `display` sur un élément masquable doit poser son `[hidden]`, sinon
   `display: flex` l'emporte sur celui du navigateur à spécificité égale (voir
   docs/revue-espacements-2026-08-21.md). */
.avatar-initiale[hidden] {
  display: none;
}

/* **La taille de la page compte**, où l'on vient justement vérifier ce que les autres verront.
   Modificateur et non seconde classe complète : la forme, le fond et le recadrage restent
   déclarés une seule fois au-dessus. */
.avatar-grand {
  width: 96px;
  height: 96px;
  font-size: var(--titre-section);
}

/* **La taille des deux endroits où une tête remplace un pictogramme** : le menu et le fil. Un
   seul modificateur pour les deux, et c'est la règle du projet plutôt que de la coquetterie —
   ces deux colonnes doivent se ressembler, l'une comme l'autre alignant du texte à sa droite.
   24 px et non 32 : au-delà, l'item de menu dépasse la hauteur des autres lignes de navigation. */
.avatar-petit {
  width: 24px;
  height: 24px;
}

/* **Le pictogramme du fil prend la même boîte que la tête qui le remplace** (27/08/2026). Sans
   ça, une ligne avec avatar poussait sa phrase 8 px plus à droite qu'une ligne sans — mesuré au
   navigateur, 32 px contre 16. C'est la largeur qui aligne, `text-align` qui recentre le glyphe
   dedans, et la même marge haute sur les deux qui les pose à la même hauteur de ligne. */
.ligne-fil .ti {
  width: 24px;
  text-align: center;
}

.ligne-fil .avatar {
  margin-top: var(--espace-minime);
}

/* L'aperçu et ses deux boutons forment un groupe : serré à l'intérieur, l'écart au reste de la
   carte étant porté par `.actions-compte` et par la note. */
.zone-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--espace-grand);
  margin-top: var(--espace-petit);
}

/* Les boutons de cette carte sont déjà dans un groupe horizontal : les empiler évite que
   « Choisir une photo » et « Retirer » ne poussent l'aperçu hors du centre sur un téléphone
   étroit. La marge haute vient de `.actions-compte`, qu'on annule ici — c'est la rangée qui
   porte l'écart, pas le bloc de boutons. */
.zone-avatar .actions-compte {
  flex-direction: column;
  margin-top: 0;
}

/* ---------- Un mot pour le groupe (21/08/2026) ---------- */

/* **Le bandeau vit dans le flux normal, jamais en `sticky`** : celui des erreurs occupe déjà
   `top: 0` avec un `z-index` de 800, et deux éléments collés au même bord finiraient par se
   recouvrir le jour où les deux s'affichent. Celui-ci pousse le contenu vers le bas — on le lit,
   on le ferme, la page reprend sa place.

   **La surface accentuée et non la couleur d'accent pleine** : c'est un mot du groupe, pas une
   alerte. Le rouge est pris par l'échec, l'accent plein par la confirmation ; il fallait un
   troisième registre, et le fond teinté d'une carte le donne sans inventer de couleur. */
/* **Il doit trancher, et c'est demandé** (21/08/2026). Premier jet : fond de carte et liseré
   d'accent — « ça apparaît presque comme un bloc naturel de l'application », et c'était juste.
   Un mot pour le groupe n'est pas un bloc de plus, c'est une interruption.

   **Le fond est l'accent secondaire de la palette**, et c'est le bon registre parce qu'il est
   *libre* : le rouge appartient à l'échec, l'accent plein à la confirmation, et ce sont les deux
   seules surfaces bruyantes que le projet possédait. La famille secondaire existe dans les trois
   thèmes et y est **délibérément opposée à la principale** — rose contre cyan, magenta contre
   bleu, sarcelle contre orange. Elle tranche par construction, sans inventer de couleur.

   **Il lui manquait une couleur de texte**, les `-texte` de la palette servant à écrire *avec*
   l'accent et non *dessus*. D'où `--couleur-texte-sur-accent-secondaire`, mesurée sur les trois
   thèmes : 6,62:1, 5,73:1 et 5,67:1.

   **Posé tout en haut, avant le logo** : c'est ce qu'on lit avant l'application elle-même. En
   flux normal et non `sticky` — celui des erreurs occupe `top: 0`, et un mot qui suivrait le
   défilement serait exactement le meuble permanent que la doctrine des animations écarte. */
.bandeau-ephemere {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-moyen);
  /* **3,5rem en haut, et c'est mesuré, pas choisi à l'œil.** Le bouton de menu et la cloche sont
     `fixed` à 16 px du bord, en 40x40 : ils **encadrent le logo** au lieu de réserver une rangée,
     donc un bloc posé en tête de page passe dessous et se fait recouvrir des deux côtés à la
     fois. Constaté à l'écran — le début du message disparaissait sous le menu, la croix sous la
     cloche. 16 + 40 = 56 px les dégage exactement.

     **Passer dessous plutôt que se rogner**, et c'est là toute la différence avec
     `.bandeau-erreur` : celle-ci **se rogne** des deux côtés depuis le 22/08/2026 (`D11`), au
     prix de 40 px de texte, parce qu'elle doit rester au ras du haut d'écran. Celui-ci passe
     **dessous** par une marge et garde donc toute sa largeur. (La rédaction d'origine disait
     que `.bandeau-erreur` « n'a rien à droite » : c'était vrai jusqu'au 22/08, ça ne l'est
     plus, et l'opposition qu'elle décrivait n'existe donc plus.) */
  margin: var(--espace-degagement) 0 var(--espace-bloc);
  padding: var(--espace-grand) var(--espace-grand);
  border-radius: 14px;
  background-color: var(--couleur-accent-secondaire);
  color: var(--couleur-texte-sur-accent-secondaire);
  text-align: left;
  /* **`arriver`, réemployée telle quelle**, celle des commentaires jamais vus. Elle respecte la
     doctrine écrite en tête de la section Animations : elle rend visible quelque chose qui
     existe, elle se joue **une fois**, et elle s'efface sous `prefers-reduced-motion`. Le bloc
     part de `display: none`, donc elle se déclenche au moment exact où le message apparaît. */
  animation: arriver 0.4s ease-out;
}

.bandeau-ephemere[hidden] {
  display: none;
}

/* **La police d'affichage, celle des titres.** C'est ce que le projet a de plus remarquable sans
   rien ajouter — la même que « Mon lien personnel » ou « La sortie du mardi ». Un mot au groupe
   n'est pas du texte courant : il se lit comme une annonce, pas comme une ligne de plus.

   `min-width: 0` sans quoi un message long pousse la croix hors du cadre au lieu de passer à la
   ligne — le même piège que `.ligne-hote`, et il se règle de la même façon. */
.texte-bandeau {
  min-width: 0;
  margin: 0;
  font-family: var(--police-affichage);
  font-style: italic;
  font-size: var(--texte-fort);
  overflow-wrap: anywhere;
}

/* **La croix hérite de la couleur du bandeau**, sans quoi elle reste au gris discret du
   `.bouton-nu` et disparaît sur un fond plein — c'est exactement le défaut du 20/08 où
   `button:hover` gagnait en spécificité sur les boutons secondaires posés sur une carte. */
.bandeau-ephemere .bouton-nu {
  color: inherit;
  flex-shrink: 0;
}

.liste-bandeaux {
  margin-top: var(--espace-grand);
  text-align: left;
}

.message-ligne-bandeau {
  margin: 0;
  font-size: var(--texte-courant);
  overflow-wrap: anywhere;
}

#formulaire-bandeau {
  display: flex;
  flex-direction: column;
  /* **`gap: 0`, et c'est la marge des champs qui porte le rythme** (22/08/2026). Dans un
     conteneur `flex` les marges ne fusionnent pas : le `gap` de 0,75 rem s'ajoutait à la
     marge basse du champ au lieu de la remplacer, soit **28 px** rendus contre 16 partout
     ailleurs. Même correction que `#formulaire-creation-voyage`, et c'est le troisième et
     dernier formulaire du projet en `flex` — les autres sont des blocs, où la question ne se
     pose pas. Trouvé par la sonde des écarts, formulaires dépliés. */
  gap: 0;
  margin-top: var(--espace-grand);
  text-align: left;
}

#formulaire-bandeau[hidden] {
  display: none;
}

/* ---------- Faire entrer quelqu'un (21/08/2026) ---------- */

/* Le formulaire replié. **Aligné à gauche dans une carte centrée**, comme les formulaires de
   tête de l'administration : une étiquette de case à cocher centrée se lit mal, la ligne
   suivante ne commençant pas sous la précédente. */
#formulaire-ajout-membre-compte {
  display: flex;
  flex-direction: column;
  /* **`gap: 0`, et c'est la marge des champs qui porte le rythme** (22/08/2026). Dans un
     conteneur `flex` les marges ne fusionnent pas : le `gap` de 0,75 rem s'ajoutait à la
     marge basse du champ au lieu de la remplacer, soit **28 px** rendus contre 16 partout
     ailleurs. Même correction que `#formulaire-creation-voyage`, et c'est le troisième et
     dernier formulaire du projet en `flex` — les autres sont des blocs, où la question ne se
     pose pas. Trouvé par la sonde des écarts, formulaires dépliés. */
  gap: 0;
  margin-top: var(--espace-grand);
  text-align: left;
}

#formulaire-ajout-membre-compte[hidden] {
  display: none;
}

#nouveau-membre[hidden] {
  display: none;
}

.titre-nouveau-membre {
  margin: 0;
  font-size: var(--texte-fort);
}

/* **Le retour en arrière est discret et le reste**, à l'opposé des trois boutons d'envoi
   au-dessus : c'est le geste qu'on cherche quand on s'est trompé, pas un choix qu'on propose.
   Le souligné le désigne comme cliquable là où un `.bouton-nu` seul ne montre rien. */
.lien-annuler {
  display: block;
  margin: var(--espace-moyen) auto 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  text-decoration: underline;
}

.liste-appareils {
  list-style: none;
  margin: var(--espace-grand) 0 0;
  padding: 0;
}

/* Le trait sur l'élément qui suit, comme partout dans le projet : la première ligne n'en
   porte donc pas, et la liste ne se termine pas sur un trait orphelin.

   **Calquée sur `.ligne-hote`**, qui résout déjà le même problème : deux blocs en
   `space-between`, un `gap` que rien ne peut manger, et `min-width: 0` sur celui de gauche
   pour qu'il se replie au lieu de pousser le bouton. La description d'un appareil peut être
   longue — « Samsung Internet sur Android · installée à l'écran d'accueil ». */
/* **`.ligne-bandeau` partage ces deux déclarations plutôt que de les recopier** (21/08/2026) :
   les deux listes vivent sur la même page, à deux cartes d'écart — un texte à gauche, un bouton
   à droite, un trait entre les lignes. Un écart de gabarit se verrait en faisant défiler. */
.ligne-appareil,
.ligne-bandeau {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-moyen);
  padding: var(--espace-petit) 0;
}

.ligne-appareil + .ligne-appareil,
.ligne-bandeau + .ligne-bandeau {
  border-top: 1px solid var(--couleur-bordure);
}

/* **Alignée à gauche, seule entorse au centrage de la page, et c'est la rangée qui l'impose** :
   une ligne en `space-between` a déjà deux bords, centrer son bloc de gauche le détacherait de
   son propre bord sans rien gagner. La description et sa mention forment un bloc, d'où le
   groupe — sans lui, `space-between` les enverrait chacune à un bout. */
.textes-appareil {
  display: flex;
  flex-direction: column;
  gap: var(--espace-minime);
  min-width: 0;
  text-align: left;
}

.description-appareil {
  font-size: var(--texte-courant);
}

.mention-appareil {
  font-size: var(--texte-mention);
  color: var(--couleur-texte-discret);
}

/* L'appareil courant est le repère de la liste : c'est le seul qu'on identifie avec
   certitude, il porte donc l'accent — et il est aussi le seul sans bouton « Oublier ». */
.ligne-appareil.actuel .description-appareil {
  font-weight: 600;
  color: var(--couleur-accent-secondaire-texte);
}

/* `.bouton-nu` porte sa marge négative à droite pour que sa surface tactile ne décale rien.
   Ici il est en bout de rangée, donc cette marge le collerait au bord de la carte : on la
   remet à zéro plutôt que d'en refaire un ailleurs. */
.ligne-appareil .bouton-nu {
  margin-right: 0;
  font-size: var(--texte-courant);
  text-decoration: underline;
}

/* ------------------------------------------------------------------ Le calendrier
   **FullCalendar habillé par ses variables, pas par ses classes** (29/08/2026, chantier `AE`).
   Il en expose trente ; on en mappe une quinzaine sur les jetons du projet. C'est tout
   l'habillage — je craignais de devoir recouvrir ses 874 classes `.fc-`, la mesure a dit
   l'inverse.

   **Et comme les jetons changent déjà avec `:root[data-theme]`, le calendrier suit les trois
   thèmes tout seul** : il n'y a pas une déclaration par thème ici, et il ne faut pas en ajouter.
   Poser une couleur en dur rouvrirait exactement la dérive que la palette referme. */
.calendrier {
  --fc-border-color: var(--couleur-bordure);
  --fc-page-bg-color: transparent;
  --fc-neutral-bg-color: var(--couleur-surface-encastree);
  --fc-neutral-text-color: var(--couleur-texte-discret);
  --fc-today-bg-color: var(--couleur-surface-teintee);
  --fc-highlight-color: var(--couleur-surface-teintee);

  --fc-event-bg-color: var(--couleur-accent);
  --fc-event-border-color: var(--couleur-accent);
  --fc-event-text-color: var(--couleur-texte-bouton-principal);

  --fc-button-bg-color: var(--couleur-surface-neutre);
  --fc-button-border-color: var(--couleur-bordure);
  --fc-button-text-color: var(--couleur-texte);
  --fc-button-hover-bg-color: var(--couleur-surface-teintee);
  --fc-button-hover-border-color: var(--couleur-bordure);
  --fc-button-active-bg-color: var(--couleur-accent);
  --fc-button-active-border-color: var(--couleur-accent);

  --fc-small-font-size: var(--texte-etiquette);

  /* La grille prend la largeur du corps, qui est déjà borné à 480 px et centré. */
  font-size: var(--texte-etiquette);
}

/* **Le numéro du jour reste discret**, c'est le contenu qui doit se lire. Même raisonnement que
   les intitulés de zone, qui gardent le gris en montant d'un cran de taille. */
.calendrier .fc-daygrid-day-number {
  color: var(--couleur-texte-discret);
  padding: var(--espace-minuscule);
}

/* **La rangée de la vue semaine a besoin d'un plancher** (29/08/2026, à la demande). Avec
   `height: "auto"`, une vue qui ne compte qu'une seule rangée la dimensionne sur son contenu :
   mesuré, **39 px** — une bande, pas une semaine. Les autres vues n'ont pas ce problème, cinq
   ou six rangées remplissant l'écran d'elles-mêmes.

   **Un plancher et non une hauteur** : la case grandit si le jour porte plusieurs entrées, donc
   rien n'est jamais replié derrière « +N en plus » — ce qui est précisément ce que la vue semaine
   apporte par rapport au mois. 160 px, mesuré à l'écran : la grille tient sous le titre sans
   défilement, et un jour vide ne fait pas un océan. */
.calendrier .fc-dayGridWeek-view .fc-daygrid-day-frame {
  min-height: 160px;
}

.calendrier .fc-col-header-cell-cushion {
  color: var(--couleur-texte-discret);
  font-weight: normal;
  padding: var(--espace-minuscule) 0;
}

/* **Le titre du mois prend la police d'affichage**, comme les intitulés principaux du projet. */
.calendrier .fc-toolbar-title {
  font-family: var(--police-affichage);
  font-size: var(--texte-fort);
  color: var(--couleur-texte);
}

.calendrier .fc-toolbar.fc-header-toolbar {
  margin-bottom: var(--espace-grand);
  gap: var(--espace-petit);
}

/* **Une couleur par genre**, et c'est ce qui rend la grille lisible d'un coup d'œil : on
   reconnaît un mardi d'une sortie libre sans lire. Les quatre viennent de la palette, donc
   elles suivent les thèmes.

   **Le mardi garde l'accent**, parce qu'il est le rendez-vous du groupe et que c'est ce qu'on
   vient chercher ; les trois autres se distinguent sans lui disputer la vedette. */
/* **La couleur du texte se pose par la variable de FullCalendar, jamais par `color`**
   (29/08/2026). Il peint le titre dans un enfant, `.fc-event-main`, dont la règle est
   `color: var(--fc-event-text-color)` — elle **écrase** ce que la pastille hérite. Les quatre
   `color:` du premier jet étaient donc **inertes** : les quatre genres affichaient le même
   `--couleur-texte-bouton-principal`, celui de la table de correspondance.

   **Mesuré avant de le voir** : « Voyage de fin d'année 2026 » sortait à **3,09:1** en clair et
   **3,13:1** en clair-orange, sous le seuil de 4,5:1 — du texte sombre sur le rose du second
   accent, alors que la règle demandait du blanc. L'événement noté était pire en puissance :
   texte `#18181b` sur une case de thème sombre, soit invisible, et rien ne le signalait
   puisqu'aucun n'était affiché au moment du contrôle. */
.calendrier .fc-event.genre-mardi {
  background-color: var(--couleur-accent);
  border-color: var(--couleur-accent);
  --fc-event-text-color: var(--couleur-texte-bouton-principal);
}

.calendrier .fc-event.genre-sortie-libre {
  background-color: var(--couleur-accent-secondaire);
  border-color: var(--couleur-accent-secondaire);
  --fc-event-text-color: var(--couleur-texte-sur-accent-secondaire);
}

/* **Le voyage est un fond et non une pastille** (29/08/2026) — `calendrier.js` détaille les trois
   défauts que ça referme d'un coup.

   **Il prend le second accent, et `--couleur-serie-velo` était une erreur de lecture de la
   palette** : cette variable « reprend l'accent assombri » (voir `docs/themes-palette.md`), elle
   valait donc **exactement** la couleur du mardi en thème sombre, et une nuance du même bleu ou
   du même orange dans les deux clairs. Partager la teinte des sorties libres est au contraire un
   gain de sens : les journées d'un séjour *sont* des sorties libres, et la bande les rassemble.

   **L'opacité de FullCalendar est écartée** : `.fc-bg-event` porte `opacity: 0.3`, qui s'applique
   à tout l'élément — le nom du séjour serait délavé avec le fond. Un `color-mix` teinte le fond
   seul et laisse le titre à pleine encre, sans rien inventer hors palette. */
.calendrier .fc-bg-event.genre-voyage {
  opacity: 1;
  background-color: color-mix(in srgb, var(--couleur-accent-secondaire) 30%, transparent);
}

/* **Un fond doit laisser passer le clic.** Il recouvre la case, et c'est la case qui ouvre le
   détail du jour — seul endroit d'où l'on atteint le voyage, FullCalendar ne déclenchant pas
   `eventClick` sur un fond. Son propre code de sélection écarte d'ailleurs explicitement les
   clics tombés sur `.fc-bg-event` : sans cette ligne, la bande serait une zone morte. */
.calendrier .fc-bg-event {
  pointer-events: none;
  /* **Le nom du séjour se pose en bas de la bande, et c'est une mesure qui l'a décidé.** Au
     premier jet il occupait la ligne du haut — donc celle du numéro du jour, et « Tremp 🇪🇸 »
     effaçait le « 21 ». Une case de mois fait 65 px : 25 pour la ligne des numéros, une
     vingtaine pour la pastille de la journée, et le reste en bas. C'est le seul endroit libre,
     et le seul qui ne dépende pas de ce que la case porte par ailleurs. */
  display: flex;
  align-items: flex-end;
}

/* FullCalendar met le titre d'un fond en italique et en petit, ce qui convient à une mention
   d'arrière-plan mais pas au nom d'un séjour : c'est l'information que porte la bande. */
.calendrier .fc-bg-event .fc-event-title {
  margin: 0;
  padding: 0 var(--espace-minuscule);
  font-size: var(--texte-etiquette);
  font-style: normal;
  font-weight: 600;
  color: var(--couleur-texte);
}

/* **L'événement noté est le seul en creux**, et c'est voulu : il n'engage à rien, il ne doit
   donc pas peser autant qu'un rendez-vous. Le contour plutôt que le plein dit exactement ça. */
.calendrier .fc-event.genre-evenement {
  background-color: transparent;
  border-color: var(--couleur-texte-discret);
  --fc-event-text-color: var(--couleur-texte);
}

.calendrier .fc-event {
  border-radius: var(--espace-minuscule);
  padding: 0 var(--espace-minuscule);
  cursor: pointer;
}

/* La légende, sous la grille : les mêmes couleurs, en petit. */
.legende-calendrier {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--espace-petit) var(--espace-grand);
  margin: var(--espace-grand) 0 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

.legende-calendrier span {
  display: inline-flex;
  align-items: center;
  gap: var(--espace-minuscule);
}

.legende-calendrier i {
  width: 0.8em;
  height: 0.8em;
  border-radius: var(--espace-minuscule);
  border: 1px solid transparent;
}

.legende-calendrier .genre-mardi { background-color: var(--couleur-accent); }
.legende-calendrier .genre-sortie-libre { background-color: var(--couleur-accent-secondaire); }
.legende-calendrier .genre-voyage {
  background-color: color-mix(in srgb, var(--couleur-accent-secondaire) 30%, transparent);
}
.legende-calendrier .genre-evenement { border-color: var(--couleur-texte-discret); }

/* ---- L'atelier (29/08/2026, chantier `AB`) --------------------------------------------- */

/* **Les vélos sont groupés par membre**, et l'écart entre deux groupes doit être plus large que
   celui entre deux vélos d'une même personne — sans quoi la page se lit comme une liste plate de
   vélos et le groupement ne sert à rien (règle « serré à l'intérieur d'un groupe, large entre
   deux groupes »). */
.groupe-velos {
  margin-bottom: var(--espace-bloc);
}

.entete-membre-velos {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-petit);
  font-weight: 600;
}

/* Même habillage que les autres cartes du projet : deux blocs qui doivent se ressembler partagent
   leur déclaration plutôt que d'en recopier une (voir `.carte-stats`). */
/* **`text-align: left` est indispensable et non cosmétique** : le `body` du projet centre, et une
   fiche de quinze lignes étiquette/valeur centrée donnerait une colonne de libellés en dents de
   scie. Posé sur la carte, il couvre le résumé, la fiche et les blocs de réglage d'un coup. */
.carte-velo {
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-radius: 12px;
  margin-bottom: var(--espace-petit);
  overflow: hidden;
  text-align: left;
}

/* La ligne compacte est un `<button>` et non un `<div>` cliquable : elle s'atteint au clavier et
   s'annonce comme dépliable, ce qu'`aria-expanded` complète. */
.resume-velo {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  width: 100%;
  padding: var(--espace-moyen);
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.nom-velo {
  font-weight: 600;
}

/* **Les deux moitiés du sous-titre d'un vélo partagent leur déclaration** (20/09/2026), comme le
   veut la convention : elles se suivent sur la même ligne, et un écart de taille ou de couleur s'y
   verrait aussitôt. Seul leur comportement à l'étroit diffère, et c'est tout le sujet. */
.details-velo,
.programme-velo {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  white-space: nowrap;
}

.details-velo {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* **Ce qui ne se tronque jamais.** La marque et le modèle nomment le vélo, le programme dit ce
   qu'on en fait — c'est ce qu'on lit en parcourant la liste, et c'était pourtant le premier à
   partir tant que les trois tenaient dans une seule chaîne à `text-overflow`. `flex: 0 0 auto`
   le sort de la négociation de place : la marque et le modèle se serrent à sa place. */
.programme-velo {
  flex: 0 0 auto;
}

/* **Il ne lui reste que sa place dans la ligne** (03/09/2026) : la couleur et la rotation
   viennent de `.chevron-deplier`, partagée avec les deux autres chevrons du projet. Il portait
   les siennes, et **divergeait déjà** — pas de couleur atténuée, donc un chevron plus appuyé ici
   qu'ailleurs pour le même rôle. C'est exactement ce que la convention prévient : deux éléments
   qui doivent se ressembler partagent leur déclaration.

   **Sa rotation aussi lui venait d'une classe `depliee`** posée par le script en plus de
   `aria-expanded`. Deux sources pour un même état, ce que le projet s'interdit ailleurs —
   « l'attribut qui annonce l'état au lecteur d'écran est celui qui pilote l'affichage ». La
   classe ne servait qu'à ça, vérifié avant retrait. */
.chevron-velo {
  flex-shrink: 0;
}

.fiche-velo {
  padding: 0 var(--espace-moyen) var(--espace-moyen);
}

/* `[hidden]` posé explicitement : une règle `display` sur un élément masquable l'emporte sinon
   sur le `display: none` du navigateur, à spécificité égale — l'élément garderait sa place et
   son accessibilité au clavier (voir l'invariant, trois cas trouvés le 21/08/2026). */
.fiche-velo[hidden] {
  display: none;
}

/* **Une grille à deux colonnes, et non plus un `space-between`** (31/08/2026, signalé sur capture :
   « ce n'est pas terrible sur la partie suspension »).

   **Ce qui n'allait pas tenait à la longueur des valeurs.** « RockShox Super Deluxe Ultimate » ne
   tient pas sur une ligne : elle passait à deux, et le libellé — seul enfant d'un `flex` étiré —
   se centrait verticalement en face du bloc, si bien qu'« Amortisseur » flottait entre ses deux
   lignes. `space-between` poussait en plus chaque valeur contre le bord droit, donc leur début
   changeait de position à chaque ligne : une colonne en dents de scie qu'on relit mal.

   **La grille corrige les deux d'un coup** : le libellé occupe une colonne fixe, la valeur commence
   toujours au même endroit, et `align-items: baseline` pose le libellé sur la PREMIÈRE ligne de la
   valeur plutôt qu'au milieu du bloc.

   **`minmax(0, 1fr)` et non `1fr`** : sans lui une valeur longue refuse de rétrécir sous sa largeur
   intrinsèque et déborde la carte — le même piège que la grille des réglages. */
.ligne-fiche {
  display: grid;
  grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
  align-items: baseline;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-minime);
}

/* La valeur reste alignée à gauche de sa colonne : c'est ce qui donne une seconde colonne franche,
   là où l'alignement à droite faisait dépendre le début de chaque valeur de sa longueur. */
.ligne-fiche > :last-child {
  overflow-wrap: break-word;
}

/* Encore `justify-content` et non `text-align` : `.label-stats` est un conteneur flex centré, et
   c'est la troisième fois que ce piège se pose sur cette page. */
.ligne-fiche .label-stats {
  justify-content: flex-start;
}

.ligne-fiche[hidden] {
  display: none;
}

.titre-zone-fiche {
  margin: var(--espace-grand) 0 var(--espace-petit);
  font-weight: 600;
}

.bloc-reglage {
  background-color: var(--couleur-surface-encastree);
  border-radius: 8px;
  padding: var(--espace-petit);
  margin-bottom: var(--espace-petit);
}

.entete-bloc-reglage {
  margin: 0 0 var(--espace-petit);
  font-weight: 600;
}

/* **Une marge haute seulement quand ce n'est pas le premier titre du bloc** : « Suspension » ouvre
   la plupart des blocs et n'a rien à décoller, « Pneus » suit des lignes de valeurs et doit s'en
   détacher. Sans le `:first-child`, le bloc s'ouvrirait par un trou. */
/* Ces titres portent aussi `.label-stats`, donc ils sont `flex` et centrés : c'est
   `justify-content` qui les aligne, pas `text-align` — voir la note sur `.grille-reglage`. */
.titre-section-reglage {
  margin: var(--espace-petit) 0 var(--espace-minime);
  justify-content: flex-start;
}

.titre-section-reglage:first-child {
  margin-top: 0;
}

.note-reglage {
  margin: var(--espace-petit) 0 0;
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

.ligne-entretien {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-minime);
}

/* Un bouton de rappel discret, plus léger qu'un bouton secondaire : ces actions vivent DANS une
   fiche déjà dense, et trois boutons pleins par vélo l'écraseraient. */
/* **`flex` et non `inline-flex`, avec `fit-content`** : deux boutons en ligne se suivaient sans le
   moindre écart — « Noter un entretien » et « Corriger la fiche » se lisaient comme un seul
   libellé. Chacun prend donc sa ligne, et `fit-content` l'empêche de s'étendre sur toute la
   largeur, ce qui en ferait une cible de clic bien plus grande que le mot qu'elle porte. */
.bouton-discret {
  display: flex;
  width: fit-content;
  align-items: center;
  gap: var(--espace-minuscule);
  margin-top: var(--espace-petit);
  padding: var(--espace-minime) 0;
  background: none;
  border: none;
  /* **`--couleur-accent-texte` et non `--couleur-accent`** (04/09/2026, `RC-UX-065`) : la
     seconde est une couleur de **surface**, et l'employer comme couleur de texte sur le fond de
     page donnait **3,24:1** en thème clair et **2,14:1** en orange — sous le seuil AA de 4,5:1,
     et sous celui de 3:1 pour le pictogramme qui l'accompagne. La variante dédiée au texte
     existait déjà dans les trois thèmes : mesurée sur les mêmes fonds, elle donne **5,90:1** et
     **5,08:1**.
     **C'est la récidive de `RC-UX-055`** — « le ✓ de validation d'un invité est à 1,90:1 en
     thème orange, remède existant non appliqué ». Même cause, même remède, autre élément. */
  color: var(--couleur-accent-texte);
  font: inherit;
  font-size: var(--texte-etiquette);
  cursor: pointer;
}

.bouton-discret[hidden] {
  display: none;
}

/* La grille du formulaire de réglage : une étiquette, puis les deux côtés. C'est la forme qui rend
   l'écart avant/arrière lisible — vingt champs empilés obligeraient à compter les lignes pour
   savoir de quel côté on est. */
.grille-reglage {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  align-items: center;
  gap: var(--espace-minime);
}

/* **`justify-content` et non `text-align`** : `.label-stats` est un conteneur `flex` centré, sur
   lequel `text-align` n'a aucun effet — la propriété calculée disait pourtant « left », ce qui
   rend le défaut invisible à qui interroge la mauvaise propriété. Mesuré au Range : le texte
   commençait à 64 dans une cellule ouverte à 35. */
.grille-reglage .label-stats {
  justify-content: flex-start;
}

.entete-reglage {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  text-align: center;
}

/* **Jamais sous 1rem** : un champ de saisie plus petit fait zoomer iOS à chaque mise au point
   (voir Conventions). La grille rétrécit les colonnes, pas le texte. */
.grille-reglage input {
  width: 100%;
  min-width: 0;
  font-size: var(--texte-champ);
}

/* **L'écart entre l'intitulé et son champ est porté par le seul `gap`** (12/09/2026) : le champ
   gardait la marge haute de la règle générique, qui s'ajoutait aux 2 px du `gap` pour rendre 6 px —
   hors échelle, relevé par la sonde 2 en posant ce motif sur le formulaire des sorties libres. */
.champ-etiquete {
  display: flex;
  flex-direction: column;
  gap: var(--espace-minuscule);
}

.champ-etiquete > input,
.champ-etiquete > select,
.champ-etiquete > textarea {
  margin-top: 0;
}

.champ-etiquete[hidden] {
  display: none;
}

.note-vide {
  margin: 0 0 var(--espace-petit);
  font-size: var(--texte-etiquette);
  font-style: italic;
  color: var(--couleur-texte-discret);
}

.note-vide[hidden] {
  display: none;
}

/* La photo du velo, en tete de fiche (30/08/2026). Pleine largeur, ratio conserve : le fichier
   sort du navigateur deja reduit a 800 px et non recadre, donc rien a rogner ici. */
.photo-velo {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  margin-bottom: var(--espace-petit);
}

/* La photo et ses boutons forment un groupe : l'ecart le detache du bloc des champs, sans quoi
   « Retirer la photo » colle a « Marque ». */
.zone-photo-velo {
  margin-bottom: var(--espace-grand);
}

/* La vignette de la ligne compacte. **Le meme gabarit pour l'image et pour son repli**, sinon les
   noms se desalignent selon qui a mis une photo — meme regle que l'avatar et son initiale. */
.vignette-velo {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  object-fit: cover;
}

/* `object-fit: cover` recadre a l'affichage seulement : le fichier garde son ratio, c'est la
   vignette qui montre le centre. Recadrer a l'encodage aurait mutile la photo de la fiche. */
.vignette-velo-vide {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--couleur-surface-encastree);
  color: var(--couleur-texte-discret);
}

/* L'invite d'activation des notifications (30/08/2026). Elle ne parait qu'apres une
   reinstallation, quand la permission n'a jamais ete demandee — donc rarement, et jamais deux
   fois de suite. Centree et compacte : elle interrompt la page, elle ne doit pas l'occuper. */
.invite-notifications {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-radius: 12px;
  padding: var(--espace-moyen);
  /* **`--espace-degagement` en haut, et c'est une mesure et non un goût** : l'invite se pose en
     tête de page, sous les deux boutons `fixed` (40 px à 16 px du bord). Sans lui, le menu et la
     cloche recouvrent son texte — mesuré, 19 px de chevauchement. Trois règles du projet s'en
     servent déjà pour la même raison. */
  margin: var(--espace-degagement) auto var(--espace-grand);
  max-width: 34rem;
}

.invite-notifications p {
  margin: 0;
  flex-basis: 100%;
  text-align: center;
}

/* Le formulaire du velo (30/08/2026). Des sections titrees et des paires cote a cote plutot que
   dix-sept champs a la suite — meme defaut que la fiche avant sa version depliable. */
.titre-section-formulaire {
  margin: var(--espace-grand) 0 var(--espace-petit);
  font-size: var(--texte-etiquette);
  font-weight: 600;
  color: var(--couleur-texte-discret);
  border-bottom: 1px solid var(--couleur-bordure);
  padding-bottom: var(--espace-minime);
  text-align: left;
}

/* Deux colonnes : c'est ce qui met le debattement avant en face de l'arriere, et qui raccourcit
   la popup d'un tiers. `minmax(0, 1fr)` et non `1fr` — sinon un champ au contenu long refuse de
   retrecir et deborde la grille. */
.grille-champs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--espace-petit);
}

/* Pour ce qui ne va pas par paire : un nom de modele n'a pas de pendant. */
.grille-champs .pleine-largeur {
  grid-column: 1 / -1;
}

/* `.label-stats` est un conteneur flex centre : c'est `justify-content` qui l'aligne, jamais
   `text-align` — le piege deja paye sur la grille des reglages. */
.champ-etiquete .label-stats {
  justify-content: flex-start;
}

/* Les documents d'un velo (31/08/2026). Le lien prend toute la ligne moins la corbeille, pour
   qu'un titre long ne pousse pas le bouton hors du cadre. */
.ligne-document {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espace-petit);
  margin: 0 0 var(--espace-minime);
}

.lien-document {
  display: flex;
  align-items: center;
  gap: var(--espace-petit);
  min-width: 0;
  flex: 1;
  color: inherit;
}

/* Le titre cede avant le poids : c'est lui qui peut etre long, et le poids fait deux mots. */
.lien-document span:not(.poids-document) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.poids-document {
  flex-shrink: 0;
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
}

/* Le bouton d'ajout de velo (31/08/2026). Il reprend `.bouton-discret` comme toutes les actions
   de cette page ; il ne lui faut qu'un centrage, la classe etant taillee pour vivre alignee a
   gauche dans une fiche. `auto` de chaque cote, puisque `fit-content` lui donne sa largeur. */
.bouton-ajout-velo {
  margin: 0 auto var(--espace-bloc);
  /* **Du rembourrage pour le doigt, pas pour l'oeil** : sans fond ni bordure, il reste aussi
     discret, mais sa cible passe de 21 px a 41 — mesure, contre 44 vises pour une zone tactile.
     Les autres `.bouton-discret` vivent dans un flux de lecture ou l'on vise au calme ; celui-ci
     est la premiere action de la page, et se rater dessus n'a rien d'anodin. */
  padding: var(--espace-moyen);
}

/* La barre d'envoi d'un document (31/08/2026). `<progress>` a un rendu natif different sur chaque
   navigateur : on le remet a plat, puis on peint la piste et la portion remplie separement — c'est
   -webkit-progress-value pour les uns, -moz-progress-bar pour les autres, et il faut les deux. */
.progres-document {
  appearance: none;
  display: block;
  width: 100%;
  height: 6px;
  border: none;
  border-radius: 3px;
  background-color: var(--couleur-surface-encastree);
  overflow: hidden;
}

.progres-document[hidden] {
  display: none;
}

.progres-document::-webkit-progress-bar {
  background-color: var(--couleur-surface-encastree);
}

.progres-document::-webkit-progress-value {
  background-color: var(--couleur-accent);
  transition: width 0.2s;
}

.progres-document::-moz-progress-bar {
  background-color: var(--couleur-accent);
}

/* La comparaison de deux reglages (31/08/2026). Encastree dans le bloc pour qu'on voie qu'elle en
   depend, et non qu'elle vaut par elle-meme. */
.comparaison-reglage {
  background-color: var(--couleur-fond);
  border-radius: 8px;
  padding: var(--espace-petit);
  margin-top: var(--espace-petit);
}

.comparaison-reglage[hidden] {
  display: none;
}

/* L'ecart chiffre : accentue, parce que c'est la seule chose qu'on vient lire. */
.ecart-reglage {
  color: var(--couleur-accent);
  font-weight: 600;
}

/* Les velos sur lesquels une roue sert (31/08/2026). Empiles, chacun sur sa ligne : ce sont des
   choix independants, pas une rangee de boutons. */
.cases-velos {
  display: flex;
  flex-direction: column;
  gap: var(--espace-minime);
}

/* ---- Le jeu (01/09/2026, en rodage)

   **Le canvas porte tout le dessin**, donc il n'y a presque rien ici : un cadre, un voile et
   trois chiffres. Les couleurs du terrain, elles, sont lues dans la palette par `jeu.js` — un
   canvas ne sait pas résoudre une variable CSS, et sans cette lecture le jeu serait le seul
   écran du projet à ignorer les trois thèmes. */
.carte-jeu {
  background-color: var(--couleur-surface-teintee);
  border: 1px solid var(--couleur-bordure);
  border-radius: 12px;
  padding: var(--espace-grand);
  text-align: left;
}

/* `position: relative` porte le voile, et `line-height: 0` retire les quelques pixels que le
   navigateur réserve sous un élément en ligne — sans quoi le cadre est plus haut que son
   canvas et le fond dépasse. */
.cadre-jeu {
  position: relative;
  line-height: 0;
  border-radius: 8px;
  overflow: hidden;
  margin: var(--espace-moyen) 0;
}

/* `touch-action: none` est indispensable et pas décoratif : sans lui, un appui maintenu sur le
   canvas fait défiler la page ou déclenche le zoom du navigateur, donc le seul geste du jeu
   devient inutilisable au doigt. */
.cadre-jeu canvas {
  display: block;
  width: 100%;
  touch-action: none;
  cursor: pointer;
}

.voile-jeu {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--espace-petit);
  line-height: normal;
  text-align: center;
  padding: var(--espace-grand);
  background-color: var(--couleur-surface-encastree);
}

/* **Une règle `display` sur un élément masquable doit poser son `[hidden]`** : `display: flex`
   l'emporte sinon sur le `display: none` du navigateur à spécificité égale, et le voile
   resterait posé sur le jeu pendant toute la partie. */
.voile-jeu[hidden] {
  display: none;
}

.titre-voile-jeu {
  font-family: var(--police-affichage);
  font-size: var(--texte-fort);
  color: var(--couleur-texte);
  margin: 0;
}

.texte-voile-jeu {
  font-size: var(--texte-etiquette);
  color: var(--couleur-texte-discret);
  margin: 0;
}

/* Les trois chiffres de la partie, sur une ligne : ils se lisent d'un coup d'œil sans quitter
   le terrain des yeux, ce qui est tout ce qu'on leur demande. */
.hud-jeu {
  display: flex;
  justify-content: space-between;
  gap: var(--espace-petit);
  margin-bottom: var(--espace-moyen);
}

.hud-jeu p {
  margin: 0;
}

.hud-jeu span {
  color: var(--couleur-texte);
  font-family: var(--police-affichage);
  font-size: var(--texte-fort);
}

/* **Une règle `display` sur un élément masquable doit poser son `[hidden]`** : le HUD est en
   `flex`, donc la ligne de série garderait sa place — et son écart — tant qu'aucune série n'est
   en cours. C'est le piège relevé trois fois par la revue des espacements du 21/08/2026. */
.hud-jeu p[hidden] {
  display: none;
}

/* **L'écran ne doit pas se sélectionner quand on tape dessus** (01/09/2026, signalé à l'usage :
   « l'écran devient bleu à chaque fois que je touche l'écran »). Un jeu se joue en appuyant vite et
   souvent : sans ça le navigateur prend chaque appui répété pour un début de sélection de texte et
   surligne la carte en bleu.

   `-webkit-tap-highlight-color` est le second morceau, et il est distinct : c'est le voile gris
   qu'Android pose sur l'élément touché, indépendant de la sélection. */
.carte-jeu {
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* Le bouton qui déplie les manettes (02/09/2026). Il vit dans une `.carte-empilee`, dont le `gap`
   fait déjà l'écart : la marge basse que `.bouton-deplier` porte pour la page des sorties libres
   s'y **ajouterait** au lieu de la remplacer. C'est le piège relevé six fois par la revue des
   espacements — annuler la marge au site, et laisser le besoin sur la classe.

   Les deux sélecteurs sont nécessaires : l'état déplié a sa propre marge, de même spécificité que
   celui-ci, donc seul l'ordre d'écriture les sépare — et un bloc qui remonterait dans la feuille
   rouvrirait l'écart sans qu'on comprenne pourquoi. */
.carte-jeu .bouton-deplier,
.carte-jeu .bouton-deplier[aria-expanded="true"] {
  margin-bottom: 0;
}

/* Les manettes de mise au point du jeu (01/09/2026). **Une grille à trois colonnes** : le libellé
   dit l'effet, le curseur se manipule au pouce, la valeur se lit sans deviner.

   Le filet, la marge et le retrait du haut sont partis le 02/09/2026 avec le titre : c'est le
   bouton qui fait désormais la séparation, et le `gap` de la carte qui fait l'écart. */
.panneau-reglages {
  text-align: left;
}

.panneau-reglages[hidden] {
  display: none;
}

.ligne-manette {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) 3.5rem;
  align-items: center;
  gap: var(--espace-petit);
  margin-bottom: var(--espace-minuscule);
}

/* Le curseur reste manipulable au doigt : la hauteur de cible compte plus que la finesse. */
.ligne-manette input[type="range"] {
  width: 100%;
  accent-color: var(--couleur-accent-forme);
}

/* De l'air au-dessus du retour aux valeurs du code : les lignes de manettes sont serrées entre
   elles (`--espace-minuscule`), et un bouton collé au dernier curseur s'appuierait par mégarde au
   milieu d'un réglage — c'est le contraste d'espacement, serré dans un groupe, large entre deux. */
.bouton-reglages-origine {
  margin-top: var(--espace-moyen);
}

.valeur-manette {
  font-family: var(--police-affichage);
  font-size: var(--texte-courant);
  color: var(--couleur-texte);
  text-align: right;
}

/* ---------------------------------------------------------------------------------------------
   Le mouvement ajouté le 06/09/2026, à la demande : « du flow et du fun, sans faire application
   où on a mis tous les effets possibles ».

   **La règle qui a servi de filtre** : une animation dit quelque chose qu'aucun texte ne dit —
   d'où l'on vient, que quelque chose a changé, qu'un geste a porté. Si on la retire et que rien
   n'est perdu à la compréhension, c'est du décor. Cinq trous ont été comblés, et l'inventaire
   d'avant en comptait déjà huit : ce n'est pas une application pauvre en mouvement, c'est une
   application dont cinq gestes n'en avaient pas.

   **Aucune de ces règles n'a besoin d'un garde-fou** : le bloc `prefers-reduced-motion` du bas de
   cette feuille les neutralise toutes par les propriétés, sans avoir à les y inscrire — c'est
   exactement ce que sa forme universelle a coûté deux essais à obtenir.
   --------------------------------------------------------------------------------------------- */

/* **1. La vue qui arrive vient du côté où l'on va.** Descendre vers un détail le fait entrer par
   la droite, remonter vers la liste la fait entrer par la gauche : le sens porte la hiérarchie,
   c'est ce qui distingue « j'ai navigué » de « la page s'est rechargée ». Le décalage est court
   — 1,5 rem — parce qu'un glissement de pleine largeur donne le mal de mer sur un téléphone. */
@keyframes entrer-depuis-droite {
  from { opacity: 0; transform: translateX(1.5rem); }
  to { opacity: 1; transform: none; }
}

@keyframes entrer-depuis-gauche {
  from { opacity: 0; transform: translateX(-1.5rem); }
  to { opacity: 1; transform: none; }
}

.vue-entre-avant {
  animation: entrer-depuis-droite 0.22s ease-out;
}

.vue-entre-arriere {
  animation: entrer-depuis-gauche 0.22s ease-out;
}

/* **2. Une carte repliée s'ouvre au lieu d'apparaître.** Le repli des trois outils d'après-sortie
   passe par `display: none`, qui ne s'anime pas — et la hauteur d'une carte n'est pas connue
   d'avance, donc rien à interpoler non plus.

   **Ce qui est animé est donc le contenu révélé, pas la boîte** : chaque enfant qui revient se
   fond en place. C'est le compromis honnête — l'ouverture se sent, la fermeture reste sèche.
   L'alternative (envelopper les enfants pour animer `grid-template-rows`) demanderait de toucher
   un balisage **recopié à l'identique depuis `index.html`** et ciblé par trois modules partagés :
   trop cher pour un fondu.

   **Les trois cartes sont nommées une à une, et il le faut** : un sélecteur sur `.carte-stats`
   aurait attrapé les présences, les invités et le rappel — qui se repeignent à chaque événement
   du flux, donc dont tout le contenu se serait mis à clignoter dès que quelqu'un répond à l'autre
   bout. Ces trois-ci sont les seules dont l'état replié existe. */
#detail-libre-tricount:not(.carte-repliee) > *:not([aria-expanded]),
#photos-sortie:not(.carte-repliee) > *:not([aria-expanded]),
#trace-sortie:not(.carte-repliee) > *:not([aria-expanded]) {
  animation: arriver 0.25s ease-out;
}

/* **3. Ta réponse se voit là où elle compte.** Le sondage est le geste le plus fréquent de
   l'application et il n'avait aucun retour au-delà du bouton qui se remplit : le nom surgissait
   d'un coup dans la liste des présents. Les noms d'un groupe sont un seul nœud texte — on ne peut
   donc pas animer un nom, mais on peut animer la ligne qui vient de changer, ce qui dit la même
   chose. Elle réemploie `arriver`, écrite pour les commentaires : deux choses qui doivent se
   ressembler partagent leur déclaration. */
.presence-changee {
  animation: arriver 0.35s ease-out;
}

/* **4. Les cartes de la liste arrivent en cascade**, et seulement au premier rendu. Le rang est
   posé par le JS en **valeur** et non en règle — même exception que les barres des statistiques,
   les seules du projet avec la couleur du thème (voir Conventions dans CLAUDE.md).

   **Jamais sur un rafraîchissement du flux temps réel** : la liste se repeint entièrement dès que
   quelqu'un répond à l'autre bout, et une cascade à chaque fois ferait frétiller la page sous les
   yeux de qui ne fait rien. C'est `sorties-pas-du-mardi.js` qui tient ce drapeau. */
.carte-en-cascade {
  animation: arriver 0.3s ease-out backwards;
  animation-delay: calc(var(--rang, 0) * 40ms);
}

/* **5. La pastille des non-lus bat quand son chiffre bouge.** Elle passait de 2 à 3 sous les yeux
   sans que rien ne le signale — or c'est précisément l'information qu'elle porte. Un seul
   battement, jamais une boucle : ce qui pulse sans fin cesse d'être une information et devient un
   décor qu'on apprend à ignorer.

   Elle réemploie `rebondir`, écrite pour le pouce, et pour la même raison qu'elle : c'est le
   retour le plus léger possible sur un changement qu'on n'a pas provoqué. */
.pastille-bat {
  animation: rebondir 0.4s ease-out;
}
