/* =============================================================
   MM — la coquille et les composants portés.

   Chargé APRÈS tokens.css, qui est la source de vérité unique des
   couleurs. Les dix règles dures du §0 de DESIGN_SYSTEM.md tiennent
   ici, et trois d'entre elles sont vérifiables à la lecture :

     · AUCUN hex en dur — toute couleur est une variable --c-*.
       Sinon le mode sombre et la palette daltonien se défont
       silencieusement, un composant à la fois.
     · TOUTE taille de police est un CRAN de l'échelle —
       `var(--mil-fs-0…6)`, définis dans `tokens.css`. Le
       multiplicateur du zoom texte y est déjà inclus, donc le cran
       s'écrit seul, sans `calc` autour. Recopier sa valeur à la main
       rend le même pixel et perd la seule chose que l'échelle
       promet : le jour où un cran bouge, le point d'usage écrit à la
       main ne bouge pas. Deux tests le tiennent — la règle 5 pour le
       zoom, et `…lit_un_cran_de_l_echelle` pour la forme.
     · Classes SÉMANTIQUES (.list-row, .card-actions), jamais
       utilitaires (.mt-4, .flex).

   Ce qui n'est PAS ici : les écrans d'avant-connexion, qui n'ont ni
   navigation ni personne à nommer → auth.css.
   ============================================================= */

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* `overflow-wrap` est un DÉFAUT TYPOGRAPHIQUE, au même titre que `line-height`
   juste au-dessus : un mot plus long que son conteneur se coupe au lieu d'en
   sortir. Posé sur `body`, il s'hérite — donc l'en-tête, les avis et tout
   composant à venir l'ont sans y penser, ce qu'une règle par écran ne
   garantit jamais bien longtemps.

   Il le faut ici parce que la règle 5 du §0 (zoom texte) et la règle 9
   (phone-first) se rencontrent : à 375px et 150 %, « renseignements » mesure
   287px à lui seul dans une colonne de lecture de 293. Le mot le plus long du
   français ne rétrécit pas quand l'écran rétrécit.

   `break-word` et pas `anywhere` : le second changerait AUSSI la largeur
   min-content de chaque boîte, donc la taille naturelle des boutons, des
   pastilles et des colonnes de tableau. On veut couper quand il le faut, pas
   déclarer que tout peut se réduire à une lettre.

   ── ET `hyphens` DÉCIDE OÙ ────────────────────────────────────────────────
   `hyphens: auto` coupe aux SYLLABES, avec un vrai trait d'union :
   « rensei-gnements » au lieu de « renseigneme / nts ». Il lui faut deux
   choses, vraies toutes les deux ici — un dictionnaire de la langue dans le
   navigateur, et `lang` sur `<html>`, que les deux gabarits posent depuis la
   locale de l'app.

   LES DEUX RESTENT, ET LA REDONDANCE EST LE BUT. Mesuré : à 320px, chacun tient
   seul les vingt écrans — retirer l'un OU l'autre ne rouvre aucun débordement
   dans un navigateur qui hyphène le français. C'est justement pourquoi les deux
   sont là : `hyphens` est une amélioration CONDITIONNELLE — pas de
   dictionnaire, pas de césure, sans le moindre avertissement — et il n'a rien à
   dire d'une adresse courriel ou d'un identifiant, qui n'ont pas de syllabes.
   `overflow-wrap` est le plancher qui ne dépend de rien. Le premier choisit où
   couper quand il le peut ; le second garantit qu'on coupe.

   `-webkit-hyphens` parce que Safari n'a rendu le nom nu qu'en 17 : sans le
   préfixe, l'iPhone d'avant-hier — le téléphone que la règle 9 vise en premier
   — retombe silencieusement sur la coupure sèche.

   CE QUE ÇA CHANGE AILLEURS. La césure ouvre des points de coupure, donc elle
   rétrécit la largeur min-content — et une colonne de tableau se règle
   là-dessus. Le tableau de /confidentialite réclamait 341px ; il tient
   maintenant dans la colonne de lecture, à 320px comme à 375px, et ne défile
   plus. `.table-scroll` reste pour le roster, qui demande toujours 635px parce
   qu'un `<select>` et deux boutons n'ont pas de syllabes. Le corollaire est que
   ces largeurs dépendent désormais du dictionnaire du navigateur — sans
   conséquence, PARCE QUE la boîte qui défile absorbe n'importe quelle valeur.
   L'ordre compte : la césure est arrivée après elle, pas à sa place. */

/* LA TAILLE DE RÉFÉRENCE PASSE DE 15 À 16 PX, et c'est le seul arbitrage de
   cette tranche qui déplace TOUT — un paragraphe, une cellule de tableau, le
   bloc de tête d'une rangée : tout ce qui n'annonce pas sa propre taille hérite
   d'ici.

   ── POURQUOI 16 ET PAS 14 ───────────────────────────────────────────────────
   Les deux crans voisins sont `--mil-fs-2` (14) et `--mil-fs-3` (16), et
   l'échelle a déjà répondu : le cran 3 s'appelle « texte courant », le cran 2
   « interface : boutons, champs, rangées ». `body` EST le texte courant.
   Descendre à 14 mettrait la prose de l'app à la taille de ses propres boutons,
   et une cellule de tableau passerait sous le libellé du bouton posé dedans.
   16 px est par ailleurs le défaut du navigateur, celui que le
   `-webkit-text-size-adjust: 100%` juste au-dessus prend pour repère.

   ── ET POURQUOI PAS RESTER À 15 ─────────────────────────────────────────────
   Parce que 15 n'est sur aucun cran, et que c'est ici que ça se voit le plus :
   une échelle qui décrit tout l'app sauf sa taille de base ne décrit pas
   grand-chose. Les dix valeurs mesurées en tête de `tokens.css` comptaient 15 ;
   l'échelle l'a délibérément écarté en repliant 12-13-14-15-16 sur quatre crans.
   Il restait à trancher de quel côté `body` tombe, pas s'il tombe.

   ── CE QUE ÇA RÉPARE AU PASSAGE ─────────────────────────────────────────────
   `auth.css` lit déjà 16 px. Les écrans d'avant-connexion et l'app avaient donc
   deux tailles de lecture différentes, sans que personne l'ait décidé : on
   changeait de corps de texte en se connectant. Elles n'en ont plus qu'une.

   MESURÉ avant / après, à 375 px et 150 % de zoom texte, sur les dix-sept
   écrans de la coop : aucun débordement horizontal ne s'ouvre — `scrollWidth`
   vaut `clientWidth` partout, avant comme après. */
body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--mil-fs-3);
  line-height: 1.5;
  color: var(--c-text);
  background: var(--c-bg);
  overflow-wrap: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

/* ---------------------------------------------------------------
   Le pont vers l'ACTIF PARTAGÉ « destinations »

   `mil-destinations.css` est une copie conforme du composant du Hub
   (MIL-DESTINATIONS.md) : il ne se modifie JAMAIS ici, sinon les deux
   apps divergent — c'est précisément ce qu'il existe pour empêcher.
   L'accorder passe donc par ses jetons, surchargés ci-dessous, et par
   rien d'autre.

   CE QUE LE PONT ACHÈTE. Le composant lit `--mil-*` sans rien savoir
   de nous ; nos `--c-*`, eux, sont redéfinis par `[data-theme]` et
   `[data-palette]`. Une propriété personnalisée se résout AU POINT
   D'USAGE : le bouton reçoit donc le mode sombre et la palette
   daltonien sans qu'une ligne du fichier partagé les mentionne.

   Deux jetons n'ont pas d'équivalent direct chez nous et se
   composent :
     · le halo est l'accent rendu translucide — `color-mix` le garde
       accroché à l'accent DE LA COOP, pas à celui de MIL ;
     · l'ombre est une valeur complète, donc écrite ici avec nos deux
       teintes d'ombre plutôt qu'avec un noir en dur.

   LE PONT SE TIENT À JOUR OU IL LIVRE LE BLEU DE MIL. La révision du
   2026-08-10 a ajouté quatre jetons au composant — la pastille
   « bêta » et les boutons d'Office 365 — et chacun porte son REPLI
   écrit en dur dans le fichier partagé : deux teintes du bleu de MIL,
   deux gris. Un jeton oublié ici ne casse donc rien de visible ; il
   expédie l'accent de MIL chez la coop suivante, exactement ce
   qu'AD-4 et `brand.css` interdisent. Le pont n'est pas une
   commodité : c'est ce qui rend le fichier partagé multi-coop.

   Les deux gris n'ont pas de jeton de bouton chez nous — nos boutons
   secondaires sont une surface bordée, pas un aplat. On les prend
   donc dans l'échelle neutre, où ils s'escaladent dans le bon sens
   des DEUX côtés : `--c-hover-bg` puis `--c-border` s'assombrit en
   clair et s'éclaircit en sombre, ce qu'un gris en dur ne fait pas.
   --------------------------------------------------------------- */
:root {
  --mil-accent:         var(--c-accent);
  --mil-accent-dark:    var(--c-accent-dark);
  --mil-accent-soft:    var(--c-accent-soft);
  --mil-glow:           color-mix(in srgb, var(--c-accent) 45%, transparent);
  --mil-surface:        var(--c-surface);
  --mil-text:           var(--c-text);
  --mil-text-2:         var(--c-text-mid);
  --mil-hover:          var(--c-hover-bg);
  --mil-btn-grey:       var(--c-hover-bg);
  --mil-btn-grey-hover: var(--c-border);
  --mil-shadow-pop:     0 12px 28px 0 var(--c-shadow-strong), 0 2px 4px 0 var(--c-shadow);
}

/* ---------------------------------------------------------------
   La coquille : le rail, puis l'écran
   ---------------------------------------------------------------
   L'en-tête `.top` est parti d'ici, et il partait RÉPARÉ : `a843cac`
   venait de lui rendre son `flex-wrap`, sans quoi la page entière
   défilait latéralement à 375 px. Le rail ne corrige donc pas un
   défaut, il range — quatre destinations au plus, le reste dans deux
   menus, au lieu de cinq à sept liens à plat qui ne tenaient sur un
   téléphone que par un repli.
   --------------------------------------------------------------- */

/* LES TROIS MESURES DU RAIL, écrites UNE fois.

   Elles étaient trois nombres en dur dans quatre règles : la largeur de la
   colonne, et les deux hauteurs de barre que `.shell-main` compense au
   téléphone. Le mode de défaillance d'un nombre recopié est silencieux — le
   jour où une barre gagne une rangée, la première ligne de l'écran passe
   dessous et rien ne le signale. Ici, la barre et sa compensation lisent la
   même valeur, donc elles ne peuvent plus se contredire.

   Elles suivent le zoom texte, comme tout le reste : à 150 %, « Registres » ne
   tient pas dans 84 px et une colonne fixe couperait le mot au milieu. */
:root {
  --rail-w: calc(84px * var(--mil-fs, 1));
  --rail-bar-h: calc(64px * var(--mil-fs, 1));
  --rail-tabbar-h: calc(76px * var(--mil-fs, 1));

  /* La colonne des comités, quatrième mesure de la coquille et pour la même
     raison que les trois autres : elle suit le zoom texte, sinon « Maintenance
     bâtiment » se coupe au milieu dès 150 %. */
  --cnav-w: calc(260px * var(--mil-fs, 1));

  /* La colonne des filtres de l'annuaire, cinquième mesure et même règle : à
     150 %, « Profil mis à jour depuis moins d'un an » doit pouvoir se replier
     sans que la case à cocher sorte de la colonne.

     Elle n'est PAS dans la coquille — c'est une colonne de l'écran, pas du
     meuble — mais elle vit ici avec les quatre autres parce que c'est le seul
     endroit où l'on peut lire d'un coup ce que la largeur de la page se
     partage. */
  --annuaire-facets-w: calc(240px * var(--mil-fs, 1));
}

.shell {
  display: flex;
  align-items: stretch;
  min-height: 100vh;
}

/* LE RAIL — les destinations de l'APP, jamais son contenu.
   Celui qu'on a retiré en `5003ed7` listait les vingt-deux comités : il
   dépendait des données, donc il affichait une ligne ou rien pour la majorité
   de la coop, et il repliait chaque page de comité sur la liste des autres.
   Celui-ci ne dépend de rien — quatre entrées, les mêmes pour tout le monde.

   AUCUN `overflow` ICI, ET C'EST UNE CONTRAINTE. Le menu des destinations
   s'ouvre en `position: absolute` sur 280 px de large, donc il traverse le bord
   droit d'une colonne de 84 px. Un `overflow: hidden` posé ici ou sur `.shell`
   le rendrait invisible — il s'ouvrirait quand même, et ça ressemblerait à un
   bogue de JavaScript. Le piège a déjà été payé sur le Hub, où c'était
   `.topbar-brand` qui écrêtait.

   La largeur vit dans `--rail-w`, avec les deux hauteurs de barre. */
/* LE `z-index` DU RAIL EST CE QUI LAISSE SES MENUS S'OUVRIR (2026-08-10).

   ── LE PIÈGE, ET IL NE SE DÉDUIT D'AUCUN `z-index` ─────────────────────
   `position: sticky` crée TOUJOURS un contexte d'empilement — à la différence
   de `relative` et `absolute`, qui n'en créent un qu'avec un `z-index`. Le rail
   et la colonne des comités sont collants tous les deux, donc chacun est un
   contexte, tous deux en `z-index: auto`, et c'est alors l'ORDRE DU DOCUMENT
   qui tranche : la colonne vient après, donc elle passe par-dessus.

   Conséquence, mesurée à l'écran : `.rail-menu-panel` a beau porter
   `z-index: 40`, ce 40 ne vaut qu'À L'INTÉRIEUR du rail. Les menus Réglages et
   Compte s'ouvraient SOUS la colonne — un panneau de 260 px pour un rail de
   84, donc les trois quarts du menu disparaissaient derrière la liste des
   comités. Le symptôme est trompeur : on lit `z-index: 40` sur le panneau,
   `auto` sur la colonne, et on conclut que ça devrait marcher.

   ── POURQUOI LES DEUX SONT ÉCRITS ──────────────────────────────────────
   Le rail seul suffirait (20 bat `auto`). La colonne porte quand même le sien,
   parce qu'un ordre qui repose sur l'ordre du document se casse à la première
   personne qui déplace un `@include` dans `layouts/app`, sans rien qui
   l'avertisse. Deux nombres écrits disent l'intention : le rail au-dessus de
   la colonne, toujours.

   Le menu des destinations, lui, n'a jamais eu ce défaut : le composant le
   sort dans `<body>`, donc il échappe au contexte du rail. C'est pourquoi il
   passait déjà par-dessus la colonne (`5310551`) alors que ces deux-ci non. */
.rail {
  flex: 0 0 auto;
  width: var(--rail-w);
  position: sticky;
  top: 0;
  z-index: 20;
  align-self: flex-start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--space-3) var(--space-2);
  background: var(--c-surface);
  border-right: 1px solid var(--c-border);
}

.shell-main {
  flex: 1;
  min-width: 0;
}

/* ---------------------------------------------------------------
   LA COLONNE DES COMITÉS — le second niveau de navigation
   ---------------------------------------------------------------
   Elle n'existe que dans la section Comités, et `layouts/app` porte la
   condition. Le rail dit dans quelle section on est ; celle-ci dit où on
   est DANS la section. Deux niveaux, deux colonnes, et la seconde
   disparaît là où elle n'a rien à dire.

   ── ELLE, ELLE A LE DROIT DE DÉFILER ────────────────────────────────
   `.rail` et `.shell` n'ont AUCUN `overflow`, et c'est une contrainte
   tenue par un test : le menu des destinations traverse leur bord. Ici
   c'est différent et ce n'est pas une exception au hasard — le menu est
   `position: fixed` avec un `z-index` de 40, donc il ne peut pas être
   écrêté par un frère, et il passe par-dessus cette colonne comme
   par-dessus le reste. Vingt-deux comités font ~1 000 px : sans
   défilement propre, la colonne étirerait la page ou perdrait ses
   dernières lignes.

   ── LE `100vh` EST UNE HAUTEUR DE MEUBLE, PAS DE CONTENU ────────────
   Collante en haut, haute d'un écran : la liste reste là quand la page
   défile, comme le rail. `dvh` en second : sur iOS la barre d'adresse
   mange `vh`, et la dernière ligne se retrouvait sous elle. */
.cnav {
  flex: 0 0 auto;
  /* `min()` ET la requête média plus bas, parce qu'elles répondent à deux
     questions différentes. La requête dit À PARTIR DE QUAND la colonne a le
     droit d'exister ; `min()` dit ce qu'elle prend quand elle existe.

     Sans le `min()`, `--cnav-w` suit le zoom texte (c'est voulu, sinon
     « Maintenance bâtiment » se coupe) et atteint 390 px à 150 % : sur un
     écran de 1 024 la colonne mangerait alors 390 px des 940 disponibles. Le
     plafond en `vw` la fait céder AVANT le contenu — un nom coupé à l'ellipse
     est un moindre mal qu'une carte de comité à 500 px, et le `title` du
     gabarit rend la fin du nom au survol. */
  width: min(var(--cnav-w), 25vw);
  position: sticky;
  top: 0;
  /* SOUS LE RAIL, ET C'EST ÉCRIT PLUTÔT QUE SUBI — voir le pavé sur `.rail`,
     qui porte le pourquoi : deux collants sont deux contextes d'empilement, et
     sans ces deux nombres c'est l'ordre du document qui décide lequel avale les
     menus de l'autre. */
  z-index: 10;
  align-self: flex-start;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  padding: var(--space-3);
  background: var(--c-surface);
  border-right: 1px solid var(--c-border);
}

/* L'en-tête reste en vue pendant que la liste défile — le filtre ne sert à
   rien s'il faut remonter pour l'atteindre. `--c-surface` explicite : sans
   fond, les lignes défileraient VISIBLEMENT derrière lui. */
.cnav-head {
  position: sticky;
  top: calc(var(--space-3) * -1);
  z-index: 1;
  margin: calc(var(--space-3) * -1) calc(var(--space-3) * -1) 0;
  background: var(--c-surface);
}

/* `.cnav-title` est parti le 2026-08-13 (voir le gabarit) : un titre
   « Comités » au-dessus de la liste des comités, dans la section dont le rail
   dit déjà « Comités ». L'en-tête ne porte donc plus que le filtre.

   Découvert par `comites-nav.js` — et c'est pour ça que l'en-tête n'a plus de
   `padding` à lui : sans JavaScript le champ n'existe pas, et une boîte
   collante vide laisserait un trait de fond au-dessus du premier groupe. Le
   `padding` vit maintenant SUR le champ découvert, donc il n'y a rien à
   rattraper quand il n'y a rien à montrer. */
.cnav-filter {
  width: calc(100% - var(--space-3) * 2);
  margin: var(--space-3);
}

/* Le titre de groupe, avec les mots de la page. Il n'est pas dans la liste
   (voir le gabarit) : un `<li>` de titre fausserait le compte annoncé. */
.cnav-group {
  margin: var(--space-4) 0 var(--space-1);
  font-size: var(--mil-fs-0);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-muted);
}

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

/* L'air entre deux CARTES, et il vaut maintenant 4 px au lieu d'un pixel. Une
   bordure a besoin d'un écart pour se lire comme un bord : à 1 px, vingt et un
   rectangles bordés se recollent en tableau — exactement ce que l'ancienne
   pastille de survol évitait en se détachant. Pas de trait de séparation,
   toujours : le trait est pour une liste qu'on COMPARE (la rangée), pas pour
   une qu'on parcourt.

   4 ET NON 8, et c'est mesuré : à 8 px la colonne d'Eric fait 1 625 px, à 4
   elle fait 1 553. C'est la bordure qui sépare ici, pas le vide — donc le vide
   n'a que le travail de ne pas les coller. */
.cnav-item + .cnav-item { margin-top: var(--space-1); }

/* LES SOUS-COMITÉS, EN RETRAIT SOUS LEUR PARENT (tranche 3). La liste
   imbriquée vit DANS le <li> du parent — un lecteur d'écran l'annonce comme
   une liste dans la liste, ce qui est la structure — et le retrait est ce que
   l'œil en reçoit. Un seul niveau, jamais deux (AD-6), donc un seul retrait à
   dessiner. Le nom du parent, lui, est dit sur la carte de l'enfant en
   `visually-hidden` : le retrait ne se lit pas à voix haute. */
.cnav-sub {
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0 0 0 var(--space-4);
}

/* Le MÊME retrait dans le sélecteur de `/comites`, où il n'y a pas de liste
   imbriquée à indenter : la grille de la forme téléphone garde l'enfant collé
   sous son parent (l'ordre du gabarit), et la marge fait le reste. */
.picker .comite-card.is-sous { margin-left: var(--space-4); }

/* LA CARTE D'UN COMITÉ — un seul dessin, une seule ligne (2026-08-17).

   ── ELLE SERT À DEUX ENDROITS, ET C'EST VOULU ───────────────────────────
   La colonne permanente (juste au-dessus) et le sélecteur de `/comites`
   (`.picker`, plus bas) affichent le MÊME objet : un comité, mon rang dedans,
   ce qu'il cherche. D'où `comite-` et non `cnav-` — un préfixe de lieu aurait
   forcé une copie pour le second lieu, et deux copies d'un même dessin
   divergent au premier changement d'avis. La panne aurait été visible et
   laide : la carte d'un comité ne se ressemblant pas selon qu'on la regarde à
   gauche ou au milieu.

   Depuis le 2026-08-14 les deux ne sont plus jamais à l'écran EN MÊME TEMPS —
   le sélecteur ne se montre que là où la colonne est cachée — et le partage
   compte encore plus : la carte doit être la même de part et d'autre du seuil
   de 1 000 px, où personne ne compare jamais deux captures.

   Ce qui reste en `cnav-` est ce qui appartient vraiment à la colonne — sa
   largeur, son en-tête collant, l'espace entre deux cartes.

   ── LA GRILLE À DEUX DESSINS EST PARTIE (maquette « en icônes ») ────────
   La carte a eu deux formes — une ligne sans rang, deux avec, la chip du rang
   faisant la seconde. Les indicateurs sont devenus des dessins (l'étoile du
   coordo, le porte-voix de l'annonce), qui tiennent À CÔTÉ du nom : plus rien
   ne justifie une seconde ligne, donc plus de grille à placement automatique —
   une rangée flex, le nom qui cède (ellipse), les indicateurs qui ne cèdent
   jamais. Le gabarit porte les mots que l'œil ne voit plus (règle 6).

   ── CE QUE ÇA REND, MESURÉ SUR LES DONNÉES DE MIL ───────────────────────
   La carte à deux lignes coûtait 64 px sur quinze des vingt et une cartes
   d'Eric — 1 469 px de colonne. À une ligne partout, la colonne revient à
   ~1 050 : le prix de la seconde ligne est rendu, et le rang se lit encore —
   d'un dessin au lieu d'un mot. */
.comite-card {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface-2);
  /* Un lien de navigation, pas un lien DANS le contenu : `.panel a` le
     mettrait en accent, et vingt-deux cartes bleues à la file ne se lisent
     plus comme une liste. La couleur dit le texte, l'état dit le fond. */
  color: var(--c-text);
  text-decoration: none;
}

/* La bordure bouge AUSSI au survol. Sur une carte, un simple changement de
   fond se voit mal — le bord est ce qui la dessine, donc c'est lui qui doit
   répondre. */
.comite-card:hover {
  background: var(--c-hover-bg);
  border-color: var(--c-border-strong);
}

/* L'état courant : le même dessin que l'entrée allumée du rail, parce que
   c'est la même idée à un niveau de profondeur. `aria-current="page"` vit
   dans le gabarit — le fond ne porte jamais l'état tout seul (règle 6). */
.comite-card.is-current {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
  font-weight: 600;
}

/* LE NOM, COUPÉ À L'ELLIPSE. `min-width: 0` avec `flex: 1`, pour la raison
   habituelle : « Représentation » sans point de coupure élargirait la colonne
   au lieu de se couper, et c'est le DOCUMENT qui se mettrait à défiler de
   côté. Le nom entier reste dans le HTML, avec un `title` — voir le gabarit.
   C'est la seule pièce de la carte qui cède : les dessins et la pastille
   gardent leur taille quoi qu'il arrive. */
.comite-name {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--mil-fs-2);
}

/* UN INDICATEUR DE CARTE — l'étoile du coordo, le porte-voix de l'annonce.
   Le dessin suit le zoom texte comme la vignette de l'annuaire (`calc` sur
   `--mil-fs`) : une icône qui ne grossit pas avec les mots redevient un détail
   pour exactement la personne qui a monté le zoom.

   Le porte-voix prend le gris des métadonnées ; l'étoile prend l'accent —
   c'est le seul dessin accentué de la colonne, et c'est ce qui la fait
   trouver d'un balayage parmi vingt-deux cartes. L'état, lui, n'est pas dans
   la couleur : le mot du rang est dans la carte, `visually-hidden` (règle 6). */
.comite-flag {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  color: var(--c-text-mid);
}

.comite-flag svg {
  width: calc(15px * var(--mil-fs, 1));
  height: calc(15px * var(--mil-fs, 1));
}

.comite-flag-role { color: var(--c-accent-dark); }

/* LA PASTILLE DU COMPTE — un rond, et un rond qui reste rond à un chiffre
   comme il s'allonge à deux. `min-width` égale à la hauteur donne le cercle ;
   le `padding` horizontal fait la pilule au-delà, plutôt qu'un « 12 » écrasé.

   `tabular-nums` : les vingt et un comptes sont lus en colonne, et des
   chiffres de largeurs différentes les font onduler.

   Elle ne se comprime jamais (`flex-shrink: 0`) : c'est le nom qui cède, et
   il sait le faire.

   ── `.is-none` EST PARTIE AVEC LA MAQUETTE « EN ICÔNES » (2026-08-17) ────
   Zéro perdait déjà son bord ; la maquette tranche — à zéro la pastille ne se
   REND plus du tout, une absence ne se compte pas. La phrase « rien à
   donner » reste dite dans le gabarit, `visually-hidden`, et
   `CommitteeNavTest` la tient. */
.comite-count {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 var(--space-1);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--mil-fs-1);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  color: var(--c-text-mid);
}

/* LA MÊME COULEUR DANS LE PANNEAU QUE DANS LA COLONNE, et il faut l'écrire.

   `.comite-card` pose déjà `color: var(--c-text)` avec sa raison — vingt et une
   cartes bleues à la file ne se lisent plus comme une liste. Ça suffisait tant
   que la carte ne vivait que dans la colonne, qui est HORS `.panel`. Dans le
   sélecteur elle y entre, et `.panel a` (0,1,1) l'emporte sur `.comite-card`
   (0,1,0) : les vingt et un titres viraient à l'accent, ce que le commentaire
   d'à côté annonçait sans pouvoir l'empêcher.

   Vu à l'écran le 2026-08-10, pas déduit. `.is-current` n'a pas le problème —
   deux classes battent une classe et un type — et garde donc son accent.

   `.ecran` A REJOINT LA LISTE LE 2026-08-17, vu à l'écran encore : quand
   `.ecran a` est né (2026-08-13, les liens de contenu du cadre), la colonne
   vivait déjà DANS le cadre — et les vingt-deux noms viraient à l'accent
   depuis, dans la seule surface qu'on regarde sept écrans sur sept. La sonde
   des couleurs calculées l'a nommé en une seconde ; personne ne relit une
   colonne qu'on croit terminée. */
.panel .comite-card,
.ecran .comite-card { color: var(--c-text); }

/* `.comite-note` — la troisième ligne de carte qui disait « n mains en
   attente » au coordo — est partie le 2026-08-14 avec le bloc « Ce qui attend
   une réponse » : la page d'accueil des comités ne montre plus que les besoins
   annoncés, et ce compte se lit sur l'écran de chaque comité. */

/* LE SÉLECTEUR DE `/comites` — les mêmes cartes, en grille.

   ── IL N'EXISTE QUE LÀ OÙ LA COLONNE N'EST PAS (2026-08-14) ─────────────
   Eric : « sur la page d'accueil des comités, enlève tout sauf les À faire
   annoncés. » Sur son écran, le sélecteur rendait les vingt-deux comités au
   milieu de la page pendant que la colonne permanente les rendait à trente
   centimètres de là — deux fois le même objet, et le second sur le seul écran
   où la colonne est certaine d'être lue.

   Le retirer PARTOUT aurait cassé les écrans étroits sans que rien le dise :
   sous 1 000 px la colonne est à `display: none` (la règle est vingt lignes
   plus bas, avec sa mesure), et l'app n'a aucune autre liste des comités — le
   rail mène ici, et ici ne mènerait plus nulle part. Les deux règles sont donc
   écrites l'une contre l'autre, et elles sont exactement complémentaires : au
   plus une des deux surfaces est à l'écran, jamais zéro.

   Le seuil ne se dédouble pas : si celui de la colonne bouge, celui-ci bouge
   avec, et une valeur différente ouvrirait une bande de largeurs sans aucune
   navigation de second niveau.

   `.picker-tete` couvre le titre de section et les titres de groupe : ils ne
   coiffent que lui, donc ils partent et reviennent avec lui.

   ── POURQUOI UNE GRILLE ET PAS UNE COLONNE ──────────────────────────────
   Vingt et une cartes empilées font ~1 300 px, donc la première annonce
   arriverait sous la ligne de flottaison — le défaut que la fusion de « À
   faire » et « Comités » avait corrigé. En trois colonnes, le même sélecteur
   en fait ~430. Ça vaut encore sous 1 000 px, où la grille tombe à deux
   colonnes puis à une.

   `auto-fill` avec un plancher de 220 px, et pas un nombre de colonnes fixe :
   la largeur disponible dépend du repli du rail et du zoom texte, deux choses
   que la feuille ne connaît pas. La grille compte elle-même, et tombe à une
   colonne sur un téléphone sans qu'une requête média ait à le dire.

   `align-items: start` : sans lui les cartes d'une même rangée s'étirent à la
   hauteur de la plus haute, donc une carte à une ligne voisine d'une carte à
   deux deviendrait un rectangle vide à moitié. */
.picker,
.picker-tete { display: none; }

.cnav-empty {
  margin: var(--space-4) 0 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* SOUS 1 000 PX, LA COLONNE N'EXISTE PAS. Elle n'est pas repliée, pas mise dans
   un tiroir, pas transformée en menu : `/comites` EST déjà cette liste, en plus
   complet, et le rail y mène en une cible. Un second niveau de navigation
   permanent prendrait la moitié d'un écran étroit pour répéter une page qui est
   à un doigt.

   ── 1 000 ET NON 700, ET LE NOMBRE VIENT D'UNE MESURE ───────────────────────
   700 px est le seuil du RAIL — là où il devient une barre du bas — et le
   reprendre ici paraissait cohérent. Mesuré à 701 px, ça donnait : rail 84 +
   colonne 260 + gouttières, donc **309 px pour le contenu**, soit MOINS que
   sur un téléphone à 375 px. Une carte de comité y était illisible, sur la
   seule largeur d'écran où le défaut ne se voit sur aucune capture parce que
   personne n'ouvre une fenêtre de 701 px exprès.

   La colonne demande donc la place de deux colonnes, pas d'une : 84 (rail) +
   250 (elle, plafonnée à 25vw) + ~620 pour le contenu + les gouttières. En
   dessous, le contenu passe avant — c'est lui qu'on est venu lire.

   ── ET C'EST ICI QUE LE SÉLECTEUR REPREND LE RELAIS (2026-08-14) ────────────
   La phrase ci-dessus disait « `/comites` EST déjà cette liste » : c'était vrai
   tant que la page d'accueil portait le sélecteur à toutes les largeurs. Elle
   ne le porte plus qu'ICI, sous ce seuil — au-dessus, elle ne montre que les
   besoins annoncés. Les deux règles vont donc ensemble : la colonne s'éteint,
   la grille s'allume, et un écran étroit garde sa liste des comités. Toucher
   l'une sans l'autre laisse soit deux listes, soit aucune. */
@media (max-width: 999px) {
  .cnav { display: none; }

  /* `block` et non `revert` : les trois éléments visés sont un `<h2>`, un
     `<h3>` et un `<p>`, donc c'est leur valeur d'origine — et `revert`
     renverrait à la feuille de l'agent utilisateur, ce qui est vrai
     aujourd'hui et cesserait de l'être le jour où le titre devient autre
     chose. */
  .picker-tete { display: block; }

  .picker {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    align-items: start;
    gap: var(--space-2);
    margin: var(--space-2) 0 var(--space-5);
  }
}

/* ---------------------------------------------------------------
   L'ÉCRAN — le cadre à hauteur figée d'une section à deux niveaux
   ---------------------------------------------------------------
   Diapo d'Eric du 2026-08-13. Elle ne dessine que des zones : une barre
   en travers du haut, une colonne à gauche (son filtre, puis les
   comités), et à droite l'en-tête du comité, ses onglets, son contenu.
   Le mot **Scrollable** y est écrit deux fois — sur la colonne et sur le
   contenu — et jamais sur la fenêtre.

   ── CE QUE ÇA CHANGE, ET CE N'EST PAS UNE QUESTION DE GOÛT ──────────
   Sur une page qui défile d'un bloc, descendre dans les besoins d'un
   comité emporte hors de l'écran le nom du comité, ses onglets et la
   colonne : on ne sait plus où l'on est ni comment aller ailleurs, et il
   faut remonter pour corriger ce qu'on vient de lire. C'est la
   directive du README, section « C'est une app qu'on manipule au doigt,
   pas un site » — la fenêtre est la fenêtre, les zones défilent.

   ── LA CHAÎNE FLEXIBLE, ET SON PIÈGE ────────────────────────────────
   Chaque maillon porte `min-height: 0`, et c'est la pièce qui ne se
   devine pas : sans lui, un élément flexible ne peut jamais être plus
   PETIT que son contenu. La colonne garderait la hauteur de ses
   vingt-deux cartes, le corps la suivrait, le cadre aussi — et aucun
   `overflow-y` ne défilerait, puisqu'il n'y aurait rien qui déborde. La
   panne ne lève aucune erreur et ne se voit qu'à la molette. Elle a
   déjà été payée sur `/membres` (voir `.annuaire-plein`).

   ── ET L'INTERDIT D'`overflow` TIENT TOUJOURS ───────────────────────
   `RailTest::test_le_rail_n_ecrete_rien` interdit tout `overflow` sur
   `.rail` et `.shell`, parce que le menu des destinations s'ouvre sur
   280 px depuis un rail de 84 : il traverse leur bord. Rien ici n'y
   touche — la hauteur se borne sur `.shell-main`, qui n'est ni l'un ni
   l'autre, et le menu passe par-dessus le reste (il est `fixed`, en
   `z-index` 40). */
/* LA HAUTEUR EST ICI, L'ÉCRÊTAGE EST UN CRAN PLUS BAS, et la séparation
   n'est pas une élégance : `RailTest::test_le_rail_n_ecrete_rien` refuse
   tout `overflow` sur une règle qui nomme `.shell` — et il a raison de le
   faire large. Un `overflow: hidden` posé ici serait sans danger
   aujourd'hui (le menu des destinations est rendu dans `<body>`, le
   panneau du rail est `fixed` et vit dans `.rail` : ni l'un ni l'autre
   n'est un descendant de cette boîte), mais il apprendrait au dépôt qu'on
   peut discuter la règle au cas par cas. C'est `.ecran` qui écrête, comme
   c'est `.annuaire-plein` qui écrête sur `/membres`. */
.shell-main-ecran {
  display: flex;
  flex-direction: column;
  /* `dvh` en second : sur iOS la barre d'adresse mange `vh`, et la
     dernière ligne de l'écran se retrouve dessous. */
  height: 100vh;
  height: 100dvh;
}

/* UN ÉCRAN QUI PREND LA FENÊTRE SANS PRENDRE LE CADRE (2026-08-16).

   `/profil` est le premier : une seule zone qui défile, sous un tableau de bord
   collant, sans barre de section ni colonne de navigation. Il garde donc `.wrap`
   — la gouttière ordinaire des pages — et c'est `.wrap` qui devient la zone à
   remplir.

   ── LES DEUX LIGNES SONT DEUX PIÈGES DIFFÉRENTS ────────────────────────────
   `min-height: 0` autorise la gouttière à être PLUS PETITE que son contenu :
   sans lui, un enfant flexible garde la hauteur de ce qu'il contient, la fiche
   fait cinq écrans, et l'`overflow` du panneau n'a jamais rien à écrêter — la
   fenêtre redéfile en silence (même piège que `.annuaire-plein`, écrit là-bas).

   `align-items: stretch` défait le `align-items: start` de `.wrap`, qui est
   juste pour une PAGE — une carte y prend la hauteur de son contenu et le reste
   est du vide. Ici c'est l'inverse qu'on veut : le panneau prend ce qu'on lui
   donne, et c'est lui qui répartit.

   ── ET LE `100dvh` EST BIEN UN CRAN PLUS HAUT ──────────────────────────────
   `.shell-main` porte au téléphone la compensation des deux barres FIXES du
   rail (`padding-top`/`padding-bottom`). La hauteur posée là-haut est donc
   déjà la fenêtre MOINS les barres — un `100dvh` recalculé ici s'ajouterait à
   ces rembourrages, et l'écran dépasserait par le bas d'exactement la hauteur
   des deux barres. C'est le défaut qu'on vient de corriger, pas un risque
   théorique. */
.shell-main-ecran > .wrap {
  flex: 1 1 auto;
  min-height: 0;
  align-items: stretch;
}

/* ⚠️ UNE ZONE QUI ÉCRÊTE DOIT AUSSI CONTENIR — l'invariant de la fenêtre figée,
   et c'est ici qu'il est écrit. Les autres zones y renvoient d'une ligne.

   ── LE DÉFAUT, ET IL NE RESSEMBLAIT À RIEN ──────────────────────────────────
   Signalé par Eric le 2026-08-14 : sur `/membres` avec une fiche ouverte, la
   page défilait de quelques dizaines de pixels et l'app finissait sur une bande
   blanche vide. Rien n'y était dessiné — le document était simplement plus haut
   que la fenêtre. Une fenêtre figée qui redéfile, ce que la directive du
   2026-08-13 interdit précisément.

   ── CE QUI DÉPASSAIT ÉTAIT INVISIBLE, ET DE UN PIXEL SUR UN ─────────────────
   `.visually-hidden` est en `position: absolute` : c'est ce qui la sort du flux
   sans la retirer au lecteur d'écran. Or un élément absolu se place dans le
   premier ancêtre POSITIONNÉ, et nos zones ne l'étaient pas — son bloc conteneur
   était donc le document entier. Elle échappait à l'`overflow` de la zone (un
   `overflow` n'écrête que ce qu'il CONTIENT), et sa position statique, elle,
   tombait sous la ligne de flottaison dès que la zone était plus longue que
   l'écran. Le document s'allongeait jusqu'à elle.

   Chaque pastille de filtre en porte une, chaque case de la grille des
   disponibilités deux. Le défaut grandit donc avec la coop : invisible sur le
   locataire de démonstration, franc chez MIL et ses vingt-deux comités.

   ── POURQUOI SUR LA ZONE, ET SUR TOUTES ─────────────────────────────────────
   `position: relative` sans décalage ne déplace rien et ne crée pas de contexte
   d'empilement : il rend seulement la zone conteneuse de ce qu'elle écrête déjà.
   Écrit sur la ZONE et pas sur la pastille exprès — la prochaine
   `visually-hidden` posée dans une zone qui défile tomberait dans le même trou,
   et elle n'a aucune raison de savoir qu'il existe.

   `DesignSystemRulesTest` tient l'invariant sur toute la feuille. La seule
   dispense est `.modale-ouverte`, qui vise la RACINE du document : son bloc
   conteneur est déjà le bloc initial, il n'y a rien à contenir de plus. */
.ecran {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* LA BARRE D'ÉCRAN SORT DE SON PANNEAU. Elle est dessinée pour vivre DANS
   un `.panel`, dont elle annule le rembourrage par des marges négatives
   et dont elle reprend le rayon en haut. Ici il n'y a pas de panneau :
   elle coiffe la section entière, d'un bord à l'autre. Deux
   déclarations, et le composant reste le même des deux côtés — une
   seconde version de la barre serait la première divergence. */
.ecran > .apphead {
  margin: 0;
  border-radius: 0;
}

.ecran-corps {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: stretch;
}

/* LA COLONNE CESSE D'ÊTRE COLLANTE. `position: sticky` et `height: 100vh`
   étaient sa façon de tenir en place sur une page qui défile ; dans un
   cadre à hauteur figée elle EST une zone qui défile, et sa hauteur lui
   vient du corps. Son `overflow-y: auto` d'origine fait alors le travail
   qu'il faisait déjà.

   Le `z-index` part avec le collant, et c'est un gain : deux éléments
   collants sont deux contextes d'empilement, et c'est cette colonne-là
   qui avalait les menus du rail avant qu'on les numérote (voir le pavé
   de `.rail`). Un contexte de moins est une règle de moins à tenir. */
/* `relative` ET NON `static`, ET LE PIÈGE A ÉTÉ PAYÉ LE 2026-08-13.

   Le collant partait — dans un cadre à hauteur figée, la colonne EST une zone
   qui défile — et `static` semblait la valeur honnête. Chaque carte porte
   pourtant sa phrase de compte en `.visually-hidden`, c'est-à-dire en
   `position: absolute` : vingt et une ici. Un absolu se cale sur son plus
   proche ancêtre POSITIONNÉ, et le collant en était un sans qu'on l'ait
   demandé. Rendu `static`, la colonne cesse de l'être, les vingt et un
   s'ancrent sur le `<body>` — donc ils SORTENT du conteneur de défilement,
   restent à leur position naturelle jusqu'à 1 554 px, et étirent le document.

   Ce qu'on voit alors : un écran parfaitement juste, chaque zone à sa hauteur,
   et la FENÊTRE qui défile quand même sur 679 px de vide. Aucune mesure de
   boîte ne le montre — `.shell`, `.shell-main`, `.ecran` et la colonne
   rendaient tous exactement 900 et 844. Seul `documentElement.scrollHeight` le
   disait, et seul un `position: relative` posé à la main l'a prouvé.

   Un `overflow: hidden` sur la colonne ne le règle PAS : un absolu dont le bloc
   conteneur est dehors n'est pas écrêté par le scroller qu'il traverse. */
.ecran-corps > .cnav {
  position: relative;
  z-index: auto;
  height: auto;
  min-height: 0;
  /* `align-self` EST LA LIGNE QUI COMPTE, et son oubli ne casse rien de
     visible : la colonne portait `flex-start` pour se coller en haut d'une
     coquille aussi haute que la PAGE. Gardé ici, il la laisse prendre la
     hauteur de ses vingt-deux cartes — 1 554 px mesurés — donc elle déborde le
     cadre par le bas au lieu de défiler dedans, et c'est la FENÊTRE qui se
     remet à défiler. Tout le reste de l'écran a l'air juste pendant ce
     temps-là ; seule la molette le dit. */
  align-self: stretch;
}

.ecran-contenu {
  flex: 1 1 auto;
  /* `min-width: 0` : sans lui, la largeur minimale d'un élément flexible
     vaut son plus long mot, donc un nom de besoin non coupé pousserait la
     colonne des comités hors de l'écran. Même cause que `.wrap > *`. */
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--c-bg);
}

/* L'EN-TÊTE DU COMITÉ NE DÉFILE PAS — c'est le second niveau de la diapo,
   et le seul intérêt de le séparer du premier est qu'il reste en vue
   pendant qu'on lit ce qu'il coiffe. Le nom du comité, son résumé, ses
   liens pratiques et ses onglets sont UN bloc (`partials/header` les
   emballe), donc un seul maillon de la chaîne flexible.

   Il prend ici le rembourrage horizontal que le panneau lui donnait,
   parce que la zone qui défile doit aller d'un bord à l'autre : un
   contenu qui glisse sous une marge se lit comme un contenu coupé. */
.ecran-contenu > .comite-tete {
  flex: 0 0 auto;
  padding: var(--space-4) var(--space-5) 0;
  background: var(--c-surface);
}

/* Le trait du bas appartient à la rangée des onglets, et il devient la
   frontière avec la zone qui défile. L'emballage n'en ajoute pas un
   second — deux hairlines à un pixel d'écart se lisent comme un défaut
   de rendu, pas comme une séparation. */
.ecran-contenu > .comite-tete > .tabs-row {
  margin: var(--space-3) 0 0;
}

/* Sans barre d'onglets, la rangée n'a plus de trait (`.tabs-row-seule`) :
   c'est l'emballage qui porte alors la frontière, sinon l'en-tête et le
   contenu se touchent sans rien entre eux. */
.ecran-contenu > .comite-tete:not(:has(.tabs-row)),
.ecran-contenu > .comite-tete:has(> .tabs-row-seule) {
  border-bottom: 1px solid var(--c-border);
  padding-bottom: var(--space-3);
}

/* LA SEULE ZONE QUI DÉFILE À DROITE. Son rembourrage est le sien et pas
   celui d'un parent : une zone de défilement dont la marge appartient au
   conteneur colle son dernier élément au bord pendant qu'on descend. */
.ecran-defile {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* POSITIONNÉE, pour la même raison que la colonne (voir le pavé de
     `.ecran-corps > .cnav`) : une zone de défilement doit être le bloc
     conteneur de ce qu'elle contient, sinon les `.visually-hidden` — absolus —
     s'ancrent sur le `<body>`, sortent d'elle et étirent le document. Attrapé
     sur `/comites`, dont le sélecteur porte les mêmes cartes que la colonne :
     la fenêtre y défilait encore alors que les six autres écrans étaient déjà
     figés, et rien à l'écran ne distinguait les deux cas.

     La règle, pour la suite : dans ce cadre, toute zone qui reçoit un
     `overflow` reçoit aussi un `position`. */
  position: relative;
  padding: var(--space-5);
}

/* Sous 700 px la gouttière de l'app rétrécit ; celle-ci suit, sinon un
   écran de téléphone paie 16 px de chaque côté pour rien. */
@media (max-width: 700px) {
  .ecran-defile { padding: var(--space-4); }

  .ecran-contenu > .committee-head,
  .ecran-contenu > .tabs-row {
    padding-left: var(--space-4);
    padding-right: var(--space-4);
  }
}

.rail-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

/* La marque de la coop, AVANT que le composant partagé ne l'emballe. Sans ces
   dimensions, le monogramme s'afficherait à sa taille naturelle le temps d'un
   rendu, et le rail sauterait. Après le montage, `.mil-dest-btn img` et
   `.mil-dest-btn > :not(img)` reprennent la main — d'où le rayon identique des
   deux côtés.

   LA MARQUE EST UNE IMAGE OU UNE LETTRE : le logo de la coop quand elle en a
   téléversé un, son monogramme sinon. Les deux portent la même classe, parce
   que le montage cherche `.coop-mark` et se moque de la balise. */
.coop-mark {
  display: block;
  width: 44px;
  height: 44px;
  border-radius: 8px;
}

/* Le MONOGRAMME. Il a été un SVG en `data:` jusqu'au 2026-08-10 — une tuile
   blanche opaque, avec ses couleurs en dur, faute de pouvoir lire un jeton
   depuis une image. C'est du texte depuis que le composant partagé sait
   emballer un nœud sans `<img>` : il hérite donc du thème, de la palette, et
   de l'état du bouton. Voir `App\Support\CoopMark`. */
span.coop-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--mil-fs-5);
  font-weight: 700;
  line-height: 1;
  color: var(--c-text);
}

/* L'ÉTAT OUVERT, ET C'EST TOUT CE QU'IL DEMANDE. La révision du 2026-08-10
   fait passer le bouton en accent plein QUAND LE MENU EST OUVERT, et seulement
   là : l'app hôte doit alors servir une marque qui lit sur l'accent
   (MIL-DESTINATIONS.md — le Hub bascule sur `logo-white.png`, nous changeons
   une couleur). Au repos, rien ne bouge : le bouton est transparent sur le
   rail, et l'ardoise de `--c-text` y est déjà le bon ton.

   `--c-on-accent` et pas un blanc en dur : c'est le jeton qui SUIT l'accent —
   il fonce en mode sombre, où l'accent s'éclaircit, et c'est ce qui a rendu le
   bouton primaire lisible (voir `DIVERGENCES` dans `DesignSystemRulesTest`).

   Le logo téléversé, lui, n'est pas de notre ressort : on ne recolore pas les
   pixels de quelqu'un d'autre. Une image opaque et sombre y garde sa tuile au
   milieu du bleu — pas cassé, dit le contrat, mais pas le rendu de référence. */
.mil-dest-root.is-open span.coop-mark {
  color: var(--c-on-accent);
}

.rail-nav {
  width: 100%;
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Chaque entrée porte SON MOT. Une barre d'icônes nues demande d'apprendre un
   pictogramme par destination ; un rail a la place d'écrire, donc il écrit. */
.rail-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  padding: var(--space-2) var(--space-1);
  border-radius: var(--radius);
  color: var(--c-text-mid);
  text-decoration: none;
}

.rail-item:hover { background: var(--c-hover-bg); color: var(--c-text); }

/* L'entrée courante se distingue par le FOND et par le poids, pas seulement
   par la couleur (règle 6) — et `aria-current` la porte pour la voix. */
.rail-item.is-current {
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 600;
}

/* 11 → 12 px, comme le mot sous la marque, et pour la même raison : le cran 0
   EST le libellé de rail. C'est le seul endroit où le pas d'un pixel se paye en
   mise en page, puisque la colonne est fixe — d'où la mesure avant de trancher.
   Le plus long des quatre mots, « Membres », occupe 75,6 px à 12 px contre 69,9
   à 11, dans les 101 px que laisse l'entrée : il tient toujours sur une ligne,
   avec 25 px de marge. Et le rapport ne bouge pas avec le zoom — `--rail-w`
   porte le même multiplicateur que le cran, donc ce qui tient à 100 % tient
   à 160 %.

   LA COLONNE N'EST PAS LA CONTRAINTE ; LA BARRE DU BAS L'EST. La remarque
   ci-dessus vaut toujours et c'est ce qui la rend trompeuse : le rail grandit
   avec le cran, donc à gauche rien ne serre jamais — « Cahier », entrée du
   2026-08-10 et plus long libellé du lot, tient sur une ligne à 100 % comme à
   160 % (80,4 px dans 117,4). Sous 700 px, la même liste passe dans une largeur
   d'ÉCRAN, qui elle ne grandit pas : cinq colonnes de 63 px à 375 × 160 %, et
   les libellés se coupent aux syllabes sur deux lignes — « Ac-cueil »,
   « Comi-tés », « Re-gistres », « Ca-hier ». La barre en tient trois à 0,6 px
   près, donc deux mots dans un libellé coûteraient la ligne de trop (mesuré :
   « Le Cahier » y prenait ses trois lignes, d'où le nom court).

   Ce qui se décide ici se vérifie donc EN BAS, pas à gauche. */
/* `letter-spacing` NÉGATIF, ET LA RAISON TIENT EN UN CHIFFRE (2026-08-23).

   « Messages » est arrivé dans le rail avec la messagerie et s'est coupé en
   deux — « Mes-sages » — alors que ses six voisins tenaient sur une ligne. Ce
   n'était pas le mot qui était trop long, c'était UN PIXEL ET DEMI : la boîte
   du libellé fait 59 px, le mot en fait 56,9 au poids normal et **60,4 au poids
   600**, celui de la destination COURANTE. Il ne cassait donc que sur l'écran
   qu'il désigne — le seul endroit où on le regarde.

   Deux corrections ont été écartées : raccourcir le mot (le rail porte des noms
   de FONCTION, et « Messages » est celui que les gens emploient), et retirer le
   gras de l'actif (c'est l'affordance qui dit où l'on est, sur tout le rail).
   Reste le crénage : −0,02 em rend 3 px sur huit caractères, ce qui ramène le
   plus long des libellés sous la boîte, sans que rien ne se voie à l'œil.

   Mesuré à l'écran, pas déduit : les sept libellés font 13,8 px de haut, donc
   une seule ligne chacun. Un HUITIÈME libellé, ou un mot plus long que
   « Messages », repasse par cette mesure — pas par le ressenti. */
.rail-label {
  font-size: var(--mil-fs-0);
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-align: center;
}

/* LE BADGE DES NOUVELLES (N1) — un compte sur l'entrée Accueil, posé en coin
   de la tuile. `position: absolute` sur l'item (déjà un bloc positionnable via
   son parent flex) plutôt qu'en ligne : le libellé ne bouge pas d'un pixel
   quand le compte apparaît — une étiquette qui saute au rythme du sondage
   apprendrait l'inverse de ce qu'elle annonce. L'accent vient du jeton, comme
   tout ce qui signale (AD-4 : jamais une couleur en dur). */
.rail-item { position: relative; }

.rail-badge {
  position: absolute;
  top: 4px;
  right: 8px;
  min-width: 18px;
  padding: 1px 5px;
  border-radius: 9px;
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-0);
  font-weight: 700;
  line-height: 1.3;
  text-align: center;
}

/* « DU NOUVEAU » — la liste de la carte : la phrase, la date en dessous, un
   filet entre les lignes. `is-lu` est posé par `nouvelles.js` au moment du
   geste : la ligne reste lisible en retrait jusqu'au rechargement — une ligne
   qui s'efface sous le curseur est la meilleure façon de ne pas finir de la
   lire. */
.nouvelles-liste {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
}

.nouvelles-liste li {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--c-border);
}

.nouvelles-liste li:last-child { border-bottom: 0; }

.nouvelles-liste li.is-lu { opacity: 0.55; }

.rail-foot {
  width: 100%;
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}

/* L'ADMINISTRATION — la place réservée, au-dessus des réglages.

   ELLE A LA BOÎTE D'UNE ENTRÉE DE RAIL, et plus celle du bouton du compte : le
   dessin par-dessus le mot, `min-height: 44px` pour la cible tactile, la
   largeur de la colonne. C'est `.rail-item` sans son `:hover` — depuis que le
   pied porte ses libellés, c'est aux DESTINATIONS qu'il ressemble, pas à la
   pastille ronde d'à côté.

   `min-height` ET NON `height` : le mot passe à deux lignes au cran
   d'accessibilité maximal, et une hauteur fixe le couperait au lieu de laisser
   la colonne grandir — elle en a la place, c'est la barre du bas qui n'en a
   pas (voir le pavé du `@media`).

   ET ELLE SE VOIT ÉTEINTE : `--c-text-muted` là où les autres prennent
   `--c-text-mid`, aucun fond au survol. Ça la range d'un cran en arrière sans
   rien écrire de plus, et le `disabled` du gabarit dit le reste. Un placeholder
   qui a l'air actif est pire que pas de placeholder du tout : on clique. */
.rail-admin {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-1);
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--c-text-mid);
  text-decoration: none;
}

/* Depuis le 2026-08-14 c'est un lien, et il se comporte comme les entrées du
   haut : la place réservée a son écran. Le bloc gardait `cursor: not-allowed`
   et une encre d'un cran plus pâle — les deux marques du chantier, retirées
   avec lui. */
.rail-admin:hover { background: var(--c-hover-bg); color: var(--c-text); }

.rail-admin.is-current,
.rail-admin[aria-current="page"] {
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 600;
}

/* Les deux menus du pied — la coop d'un côté, moi de l'autre. En `<details>`,
   comme les réglages d'affichage l'étaient déjà : ça s'ouvre sans JavaScript.

   PAS de `position: relative` ici : le panneau ne s'ancre plus sur sa pastille,
   il s'ancre sur l'ÉCRAN (voir plus bas). Le laisser laisserait croire à la
   prochaine lecture que ce bloc décide de quelque chose. */
.rail-menu { position: static; }

/* « ··· » N'EXISTE QUE DANS LA BARRE DU TÉLÉPHONE.

   Le rail du bureau est VERTICAL : il a toute la hauteur de l'écran, donc
   aucune destination n'a besoin de se ranger derrière un menu. La contrainte
   est celle de la barre du bas, et elle arrive avec le point de rupture — d'où
   le repliement ici plutôt qu'une condition dans le gabarit.

   Les deux listes sont rendues ensemble, et c'est ce qui rend la bascule
   gratuite : sur large on cache le « ··· », sur étroit on cache les entrées
   qu'il contient. Un `display: none` sort de l'arbre d'accessibilité, donc les
   destinations débordantes ne sont JAMAIS exposées deux fois — pas de double
   tabulation, pas de libellé annoncé en écho. */
.rail-nav-plus { display: none; }

/* Le `<summary>` prend lui aussi la boîte d'une entrée de rail, pour la raison
   écrite sur `.rail-admin` : ils sont l'un sous l'autre, et deux cibles de
   gabarits différents au même endroit se lisent comme un défaut d'alignement.
   `list-style: none` reste indispensable — sans lui, le triangle de `<details>`
   revient s'ajouter au dessin. */
.rail-menu > summary {
  list-style: none;
  cursor: pointer;
  width: 100%;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-1);
  border-radius: var(--radius);
  color: var(--c-text-mid);
}

.rail-menu > summary::-webkit-details-marker { display: none; }
.rail-menu > summary:hover { background: var(--c-hover-bg); color: var(--c-text); }
.rail-menu[open] > summary { background: var(--c-hover-bg); color: var(--c-text); }

/* Le panneau s'ouvre À CÔTÉ du rail — et il est borné par l'ÉCRAN, pas par sa
   pastille.

   ── CE QUE `position: absolute` COÛTAIT, ET C'ÉTAIT MESURÉ ──────────────────
   Ancré `bottom: 0` sur son `<summary>`, le panneau grandissait vers le HAUT
   depuis le pied d'un rail de `100vh`. Sur un viewport court, il sortait donc
   par le haut — et comme le rail est `sticky`, le défilement ne le rattrapait
   pas : ce qui dépassait était INATTEIGNABLE, pas seulement laid.

     812 × 375 (téléphone en paysage), zoom 100 % : panneau 387 px, 23 px hors
     écran ; à 150 %, 448 px et 53 px dehors — « Inviter » réduit à 4 px de
     haut. Un coordo super-admin porte trois rangées de plus, et le même défaut
     l'atteint sur un portable à 150 %.

   `fixed` change l'ancrage de repère : le panneau se pose sur le bas de
   l'ÉCRAN, à droite du rail, et `max-height` l'empêche par construction de
   dépasser en haut. Ce qui ne tient pas défile DANS le panneau.

   ── L'`overflow` EST SUR LE PANNEAU, JAMAIS SUR LE RAIL ─────────────────────
   C'est la même contrainte qu'au-dessus, vue de l'autre bout : un `overflow`
   sur `.rail` ou `.shell` écrêterait le menu des destinations, qui traverse le
   bord droit de la colonne. Ici il est posé sur le panneau lui-même, qui n'a
   rien à laisser dépasser.

   `100dvh` après `100vh` : sur un téléphone, la barre d'adresse rétracte le
   viewport, et `vh` garde la mesure de départ. Le repli sert les navigateurs
   qui ne connaissent pas l'unité.

   `z-index` reste SOUS le menu des destinations (1050), qui doit passer par
   dessus tout le reste. */
.rail-menu-panel {
  position: fixed;
  bottom: var(--space-3);
  left: calc(var(--rail-w) + var(--space-2));
  z-index: 40;
  width: 260px;
  max-height: calc(100vh - var(--space-3) * 2);
  max-height: calc(100dvh - var(--space-3) * 2);
  overflow-y: auto;
  padding: var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  /* Ombre réservée aux menus et aux modales (§0, règle 2). */
  box-shadow: 0 4px 16px var(--c-shadow);
}

/* 44 px de haut, comme partout ailleurs : un menu qui ne s'ouvre qu'au clic
   n'excuse pas des cibles qu'on rate au pouce. */
.rail-menu-item {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--c-accent);
}

.rail-menu-item + .rail-menu-item { border-top: 1px solid var(--c-border); }

/* LA SORTIE SANS SCRIPT — un bouton dessiné comme les liens du panneau
   (2026-08-16). Elle est un `<button>` et pas un `<a>` parce qu'une
   déconnexion est un POST ; il ne lui manque donc que le déshabillage du
   dessin natif.

   PAS `.btn-link`, ET C'EST MESURÉ : il pose `min-height: 0`, il est écrit
   plus bas dans cette feuille, et il a le même poids que `.rail-menu-item` —
   donc il gagne, et la cible retombe à la hauteur du texte. Une commande de
   moins de 44 px dans un menu qu'on ouvre au pouce est exactement ce que la
   règle 9 interdit, et rien à l'écran ne l'aurait signalé.

   `font: inherit` : un bouton natif ne suit ni la police ni la taille de son
   entourage, donc sans ça la seule ligne du panneau qui n'est pas un lien est
   aussi la seule qui ne ressemble pas aux autres. */
.rail-menu-sortie {
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-accent);
  font: inherit;
  /* SOULIGNÉ, PARCE QUE SES VOISINES LE SONT. Mesuré dans le panneau : les
     autres rangées sont des `<a>` et reçoivent leur trait de la feuille ; un
     bouton, lui, n'hérite pas de `text-decoration`. Sans cette ligne, la seule
     commande du menu qui a un effet de bord est aussi la seule à ne pas
     ressembler à quelque chose qu'on peut toucher — et elle ne s'en distingue
     que par sa couleur, qui est la même que les autres (règle 6). */
  text-decoration: underline;
  text-align: left;
  cursor: pointer;
}

/* Le titre qui coupe le panneau en deux — les portes de gestion au-dessus, les
   réglages d'affichage en dessous.

   `--c-text-mid` ET NON `--c-text-muted` (2026-08-14). Mesuré à 2,64:1 sur
   `--c-surface` en clair et 4,23 en sombre : des deux côtés, sous le plancher
   AA de 4,5 que le §5 du système de design pose comme non négociable. Le gris
   moyen rend 6,05 et 7,38 pour exactement le même dessin — c'est la valeur qui
   change, pas le rôle.

   C'est l'arbitrage de `.cnav-group`, rendu ici : le gris muet n'est pas une
   encre de TEXTE. Il sert un chiffre éteint, un séparateur, un état inactif —
   des marques dont l'effacement EST le sens. Le seul mot qui dit où finit une
   moitié du panneau et où commence l'autre n'en est pas une : c'est ce mot qui
   permet de ne pas lire les six rangées du dessus quand on vient régler le
   zoom. La palette daltonien ne redéclare aucun jeton de texte — les deux
   valeurs valent donc aussi en `cb`, ce que la mesure confirme. */
.rail-menu-head {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--mil-fs-0);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
}

/* LE PIED DU PANNEAU — la version qui tourne, et le crédit.

   Un TRAIT au-dessus, comme le bas de la fiche (`.account`) et pour la même
   raison : ce bloc ne parle plus de la coop mais du LOGICIEL, donc il ne doit
   pas se lire comme une dernière rangée de réglages. Au plus petit cran de
   l'échelle : ça se lit une fois par mois, et ça ne doit pas disputer
   l'attention aux portes de gestion juste au-dessus.

   ── « EN GRIS SOURD » EST PARTI, LE RESTE TIENT (2026-08-14) ──────────────
   C'était `--c-text-muted`, mesuré à 2,64:1 en clair et 4,23 en sombre, donc
   sous le plancher AA (§5) ; c'est `--c-text-mid`, qui rend 6,05 et 7,38.

   Le RANG de ce bloc ne tenait pas à sa pâleur et n'a donc rien perdu : le
   trait le sépare, le cran 0 le rapetisse, et sa place tout en bas dit le
   reste. Trois façons de le ranger en arrière, dont aucune ne coûte de
   lisibilité — c'est la quatrième, la seule qui en coûtait, qui part.

   Et c'est le pire endroit où l'économiser : la seule raison d'ouvrir ce
   panneau jusqu'en bas est d'aller LIRE le numéro de version, pour le recopier
   dans un signalement. `.rail-menu-version` n'a pas de couleur à elle et hérite
   d'ici — cette ligne corrige donc les deux. */
.rail-menu-about {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--c-border);
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
}

/* La version sur sa propre ligne : collée au crédit, elle formerait une phrase
   qui n'en est pas — et c'est la moitié qu'on vient chercher. */
.rail-menu-version { display: block; }

/* LE BOUTON DU COMPTE — un lien vers sa fiche, et plus un `<summary>`.

   Il garde EXACTEMENT la boîte de `.rail-menu > summary` : 44 px de côté, même
   rayon, même fond au survol. Ce n'est pas de la ressemblance pour la
   ressemblance — les deux sont côte à côte dans le pied, et deux cibles de
   tailles différentes au même endroit se lisent comme un défaut d'alignement.
   Les 44 px sont par ailleurs le minimum tactile, et la pastille n'en fait que
   32 : sans cette boîte, la zone cliquable serait la pastille.

   L'ÉTAT COURANT est celui des entrées du rail (`.rail-item.is-current`), fond
   compris : sa fiche est une destination comme une autre une fois qu'on y est,
   et deux façons de dire « tu es ici » dans la même colonne en font une de
   trop. */
.rail-account {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius);
  text-decoration: none;
}

.rail-account:hover { background: var(--c-hover-bg); }
.rail-account.is-current { background: var(--c-accent-soft); }

/* Les initiales plutôt qu'une photo : MM n'en a aucune, et une silhouette
   générique répétée sur chaque page n'apprend rien. */
.rail-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-size: var(--mil-fs-0);
  font-weight: 700;
}

/* La photo REMPLIT le rond, quitte à recadrer : une image de profil est carrée
   une fois sur deux et portrait l'autre fois, et `contain` laisserait des
   bandes de fond accent autour d'un visage.

   ── ET C'EST L'IMAGE QUI EST RONDE, PAS LE CONTENANT ───────────────────────
   Le réflexe est `overflow: hidden` sur `.rail-avatar`. `RailTest::
   test_le_rail_n_ecrete_rien` le REFUSE, et il a raison : le menu des
   destinations s'ouvre en `position: absolute` sur 280 px depuis une colonne
   de 84, donc il traverse le bord du rail. Un `overflow` posé n'importe où
   dans le rail le rendrait invisible — il s'ouvrirait quand même, et ça
   ressemblerait à un bogue de JavaScript.

   Le rayon sur l'image fait exactement le même dessin, sans rien écrêter. */
.rail-avatar-img {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-pill);
  object-fit: cover;
  display: block;
}

/* LE TÉLÉPHONE — le rail se coupe en deux.

   Les destinations descendent en BARRE DU BAS, au pouce ; la marque et les deux
   menus restent en haut. C'est la seule disposition qui garde le menu des
   destinations à l'écran : il s'ouvre vers le BAS (`top: 100%`, dans l'actif
   partagé, qu'on ne modifie pas), donc une marque posée en bas l'ouvrirait hors
   de l'écran.

   ELLES SONT CINQ DANS LA BARRE, ET CE N'EST PLUS LE NOMBRE QU'IL Y EN A.
   Cette note disait « une SIXIÈME entrée ne se décide plus au ressenti, elle se
   mesure ici d'abord ». La mesure a fini par être faite — le 2026-08-23, quand
   la messagerie a posé la SEPTIÈME — et elle a donné trois libellés qui
   débordent leur colonne.

   La barre ne grandit donc plus avec la liste : elle garde cinq destinations et
   un « ··· » qui porte le reste (décision d'Eric, « as more rail items might
   come in the future »). Le raisonnement et les chiffres vivent sur
   `.rail-nav-deborde`, plus bas dans ce même bloc ; le rang qui décide vit dans
   `partials/rail`. Ajouter une destination ne demande donc plus rien ici —
   c'est le chiffre CINQ qui redeviendrait une mesure, pas la liste. */
@media (max-width: 700px) {
  .shell { display: block; }

  /* LA PILE D'AVIS MONTE AU-DESSUS DE LA BARRE DU BAS, et c'est ici que ça se
     décide. `toasts.css` est chargé par les trois coquilles, dont deux n'ont
     pas de rail du tout : il ne peut donc pas connaître cette hauteur. Il lit
     `--toast-bottom` avec le cas ordinaire en repli, et cette seule ligne est
     ce que l'app lui apprend en plus.

     Sans elle, un avis d'ERREUR — qui reste tant qu'on ne l'a pas fermé —
     couvrirait les destinations, c'est-à-dire toute la navigation du
     téléphone. La croix suffirait à s'en sortir ; encore faut-il la trouver
     avant de chercher pourquoi le rail ne répond plus.

     ── C'ÉTAIT `--toast-top`, ET LA BARRE À ÉVITER A CHANGÉ AVEC LA PILE ────
     Les avis vivaient en haut à droite, donc c'est la barre du HAUT qu'ils
     recouvraient (les deux menus du pied et le bouton du compte). Ils sont
     descendus en bas au centre le 2026-08-12 ; le voisin encombrant est
     maintenant la barre des destinations, et `env()` s'ajoute parce qu'elle
     le porte aussi — la poignée d'accueil d'un iPhone est sous les deux.

     ── LE `:has()` N'EST PAS UNE COQUETTERIE ───────────────────────────────
     Cette coquille sert aussi des pages PUBLIQUES (`/confidentialite`), et un
     visiteur n'a pas de destinations : `partials/rail` ne rend alors aucun
     `.rail-nav`, donc aucune barre en bas. Sans la condition, l'avis
     flotterait à 64 px du bord au-dessus de rien, et ça se voit. */
  :root { --toast-bottom: var(--space-4); }

  :root:has(.rail-nav) {
    --toast-bottom: calc(var(--rail-tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--space-2));
  }

  /* `height` et non `min-height` : c'est la valeur que `.shell-main` compense
     plus bas, et les deux lisent le même jeton. Si le contenu venait à ne plus
     tenir, il déborderait VISIBLEMENT de la barre — au lieu de la faire
     grandir en silence par-dessus la première ligne de l'écran. */
  .rail {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 30;
    width: auto;
    height: var(--rail-bar-h);
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-2) var(--space-3);
    border-right: 0;
    border-bottom: 1px solid var(--c-border);
  }

  .rail-brand { flex-direction: row; gap: var(--space-2); }

  .rail-foot { width: auto; margin-top: 0; flex-direction: row; }

  /* LE PIED REPERD SES MOTS ICI, et c'est la seule place qui décide.
     En haut de l'écran, `.rail-foot` est une rangée dans une barre de 64 px
     (`--rail-bar-h`) : le dessin par-dessus le mot y demande ~58 px de contenu
     dans les 48 px que laisse le remplissage de la barre, donc la barre
     grandirait — par-dessus la première ligne de chaque écran, puisque
     `.shell-main` compense la HAUTEUR DU JETON et pas celle du contenu.

     Les cibles retrouvent donc leur carré de 44 px, et leur nom passe à
     l'`aria-label` que le gabarit garde exprès pour ce cas. C'est l'inverse du
     compromis d'en bas : la barre des destinations, elle, a la largeur d'un
     écran et garde ses libellés. */
  .rail-foot .rail-label { display: none; }

  .rail-admin,
  .rail-menu > summary {
    width: 44px;
    min-height: 0;
    height: 44px;
    padding: 0;
  }

  /* Aligné sur le bord droit et ouvert vers le BAS : à côté du rail, il n'y a
     plus de côté. Il reste `fixed` — donc borné par l'écran comme sur bureau —
     et sa hauteur maximale part du bas de la barre. */
  .rail-menu-panel {
    bottom: auto;
    left: auto;
    right: var(--space-3);
    top: calc(var(--rail-bar-h) + var(--space-2));
    max-height: calc(100vh - var(--rail-bar-h) - var(--space-5));
    max-height: calc(100dvh - var(--rail-bar-h) - var(--space-5));
  }

  .rail-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 30;
    margin: 0;
    height: var(--rail-tabbar-h);
    flex-direction: row;
    justify-content: space-around;
    background: var(--c-surface);
    border-top: 1px solid var(--c-border);
    /* `env()` : la barre passerait sous la poignée d'accueil d'un iPhone. Elle
       s'AJOUTE à la hauteur du jeton, et `.shell-main` en dessous ajoute la
       même chose — les deux lisent la même expression. */
    padding: var(--space-1) var(--space-2) env(safe-area-inset-bottom, 0px);
  }

  .rail-nav li { flex: 1; }

  /* LA BASCULE DE LA BARRE DU BAS — cinq destinations et « ··· ».

     ── LE CHIFFRE, ET IL EST MESURÉ DANS LE NAVIGATEUR ─────────────────────
     C'est ce que le pavé d'en tête réclamait, payé le 2026-08-23 quand
     Messages est devenu la septième destination. Mesuré à 375 px, sur la
     barre elle-même — et le calcul de coin de table s'était trompé de trois
     pixels et demi, ce qui suffisait à changer la réponse :

         6 colonnes → 56,5 px        7 colonnes → 48,4 px

     Les libellés, au crénage du rail : Bris 21, Plus 24, Cahier 35,
     Accueil 39, Comités 45, Registres 51, Membres 53, Messages 57.

     À sept colonnes, quatre libellés débordent. À six — cinq destinations
     plus « ··· » — les six tiennent sur une ligne, **à condition** de rendre
     les 8 px de remplissage latéral (voir juste en dessous) : sans ça
     « Registres » repasse à deux lignes tout seul.

     ── ET ÇA NE SE REPOSERA PLUS ───────────────────────────────────────────
     C'est l'intérêt de la décision d'Eric : une huitième, une neuvième
     destination tombent derrière « ··· » sans toucher à cette feuille. Le
     rang est posé par le gabarit (`$railVisibles`), donc le seul geste qui
     redevient une mesure est de CHANGER ce chiffre.

     Le libellé du « ··· » reste visible, contrairement à ceux du pied : il
     est dans la barre du BAS, qui a la largeur d'un écran, et une colonne
     muette au milieu de cinq colonnes nommées se lit comme un bouton cassé. */
  .rail-nav-deborde { display: none; }

  .rail-nav-plus { display: block; }

  /* LE REMPLISSAGE LATÉRAL TOMBE À 2 px DANS LA BARRE, et c'est la mesure qui
     l'a exigé — pas le goût.

     `--space-1` de chaque côté mange 8 px des 56,5 que vaut une colonne à
     375 px, et « Registres » (51 px au crénage du rail) passait alors à DEUX
     lignes pendant que ses cinq voisins en gardaient une. Un seul libellé plus
     haut que les autres décale sa rangée : ça ne casse rien, ça se lit comme un
     défaut d'alignement.

     Ça ne rétrécit AUCUNE cible : le `<li>` est en `flex: 1` et c'est lui qui
     porte les 56,5 px de large et les 44 px de haut de la règle 9. Le
     remplissage ne dessinait qu'une marge intérieure autour du couple
     dessin + mot. */
  .rail-nav .rail-item {
    padding-left: 2px;
    padding-right: 2px;
  }

  /* Le `<summary>` reprend la boîte d'une entrée de rail — il est SUR la même
     rangée que cinq destinations, et deux gabarits côte à côte se lisent comme
     un défaut d'alignement (la raison écrite sur `.rail-admin`). Il ne reprend
     PAS le carré de 44 px du pied : celui-là existe parce que la barre du HAUT
     n'a pas la place des mots, et ici on les garde. */
  .rail-menu-plus > summary {
    width: auto;
    height: auto;
    min-height: 0;
    padding: var(--space-2) var(--space-1);
  }

  /* Le panneau s'ouvre VERS LE HAUT depuis la barre du bas, et à droite : sous
     la barre, il n'y a plus d'écran. Il reste `fixed`, donc borné, et ce qui ne
     tient pas défile dedans — jamais hors de la vue, qui est le défaut que
     `.rail-menu-panel` a déjà payé une fois en haut. */
  .rail-menu-plus > .rail-menu-panel {
    top: auto;
    right: var(--space-3);
    bottom: calc(var(--rail-tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--space-2));
    max-height: calc(100dvh - var(--rail-tabbar-h) - var(--rail-bar-h) - var(--space-5));
  }

  /* L'ENCRE DE LA BARRE DU BAS EST CELLE D'UNE SURFACE, PAS CELLE DE LA DALLE.

     ── LE DÉFAUT, MESURÉ LE 2026-08-14 ────────────────────────────────────
     Le rail sur fond accent (2026-08-12) peint son encre en blanc atténué —
     `.rail .rail-item { color: color-mix(--c-on-rail 78%) }`, tout en bas de
     ce fichier — et cette règle-là ne connaît aucune largeur. Or `.rail-nav`
     cesse d'être posée sur la dalle ICI : elle se détache en barre du bas et
     reprend `--c-surface` (juste au-dessus). En thème clair, les cinq
     destinations étaient donc écrites en blanc sur blanc — 1:1, la barre de la
     capture est vide — c'est-à-dire TOUTE la navigation du téléphone
     invisible, sur des pages que le §9 veut phone-first.

     ── POURQUOI ÇA A TENU DEUX JOURS SANS SE VOIR ─────────────────────────
     C'est une régression du fond accent, pas un défaut d'origine : `.rail-item`
     portait `--c-text-mid` avant lui, ce qui allait très bien sur une barre
     blanche. Et le mode sombre n'a JAMAIS montré le défaut — l'encre claire y
     tombe sur une barre foncée (8,36:1) — donc la seule façon de le voir était
     d'ouvrir le téléphone en clair. `BrandTokensTest` ne pouvait pas le dire
     non plus : il mesure la paire `--c-on-rail`/`--c-rail`, c'est-à-dire
     l'encre sur LA DALLE, et le fond de cette barre-ci n'est pas la dalle.

     ── CE QUI EST RENDU ───────────────────────────────────────────────────
     Les trois encres de `.rail-item`, celles d'avant le fond accent : 6,05:1
     au repos, 10,41 au survol, 4,54 pour l'entrée courante en clair ; 7,38 /
     12,91 / 5,96 en sombre. Les quatre contextes passent AA, palette daltonien
     comprise.

     Le sélecteur passe par `.rail-nav` pour ne reprendre QUE la barre : en
     haut de l'écran, `.rail` est toujours la dalle accent, et son pied
     (`.rail-admin`, les réglages, le compte) garde l'encre blanche qui va
     dessus. Deux fonds dans un même rail, donc deux encres. */
  .rail .rail-nav .rail-item { color: var(--c-text-mid); }

  .rail .rail-nav .rail-item:hover {
    background: var(--c-hover-bg);
    color: var(--c-text);
  }

  /* Le fond ET le poids, comme la règle de base : `aria-current` porte l'état
     pour la voix, et la couleur n'est jamais seule (règle 6). */
  .rail .rail-nav .rail-item.is-current {
    background: var(--c-accent-soft);
    color: var(--c-accent-dark);
  }

  /* La place des deux barres fixes, LUE DANS LES MÊMES JETONS qu'elles. Sans
     cette compensation, la première ligne de chaque écran passe sous le haut du
     rail et le dernier bouton sous la barre du bas ; avec deux nombres
     recopiés, elle se met à mentir dès que l'une des barres change de taille,
     et rien ne le dit. */
  .shell-main {
    padding-top: var(--rail-bar-h);
    padding-bottom: calc(var(--rail-tabbar-h) + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------------------------------------------------------------
   Mise en page
   --------------------------------------------------------------- */

/* UNE COLONNE, partout — et ça reste vrai avec un rail.
   Il y a eu ici une seconde colonne de 240px : la liste des vingt-deux comités,
   recopiée par chaque vue, qui sous 700px repliait chaque comité sur la liste
   des autres. Ce n'est pas ce que le rail a ramené. Le rail vit dans la
   COQUILLE et pas dans la vue — une vue neuve n'a rien à en copier, ce qui
   était la vraie raison de retirer l'autre. Choisir son comité vit toujours
   sur `/comites`. */
/* ── PAS DE REMBOURRAGE ICI : IL ÉTAIT LA SECONDE GOUTTIÈRE (Eric, 2026-08-16)
   `.wrap` posait `--space-5` tout autour, et son unique enfant est un
   `<main class="panel">` qui pose les mêmes 24 px à l'intérieur. Mesuré sur
   l'accueil : le rail s'arrête à 84 px, le panneau commençait à 108, le texte à
   132 — quarante-huit pixels de marge pour une seule bordure.

   Le cadre plus récent ne fait pas ça. Sur `<x-ecran>`, `.ecran`,
   `.ecran-corps` et `.ecran-contenu` sont tous à zéro et la gouttière vit UNE
   FOIS, dans la zone qui défile (`.ecran-defile`). Les deux familles d'écrans
   se ressemblaient donc de moins en moins à mesure que la seconde grandissait.

   Les dix-neuf `.wrap` de l'app tiennent tous exactement un `<main
   class="panel">` — vérifié gabarit par gabarit — donc `gap` n'a jamais eu deux
   éléments à séparer. Il reste pour la grille qu'il déclare, pas pour ce qu'il
   fait aujourd'hui. */
.wrap {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}

@media (max-width: 700px) {
  .wrap { gap: var(--space-4); }
}

/* CE QUI EMPÊCHE UN SEUL MOT DE DÉCIDER DE LA LARGEUR DE LA PAGE.

   La taille minimale automatique d'un élément de grille vaut sa largeur
   min-content : `min-width: auto` — le défaut — signifie « jamais plus étroit
   que ton plus long mot ». Un `1fr` ne borne donc rien du tout, parce que la
   piste s'élargit pour contenir l'élément, et le document défile
   latéralement.

   Mesuré sur /confidentialite à 375px et 150 % de zoom texte : le panneau
   faisait 391px dans une colonne qui en offrait 343, et la PAGE défilait de
   32px. Le symptôme trompe : tous les enfants du panneau étaient à 341px, donc
   rien à l'écran n'avait l'air trop large — ils étaient ÉTIRÉS par le panneau,
   pas en train de le pousser. Le coupable était le mot le plus long du
   tableau, invisible dans la mesure des rectangles.

   `min-width: 0` rend la décision à la colonne. Couper le mot, c'est le
   `overflow-wrap` hérité de `body` ; pour les cellules de tableau, dont la
   largeur se calcule autrement, voir la section « Tableaux ».

   Sur `.wrap > *` et pas sur `.panel` : la cause est d'être un élément de
   grille, pas d'être un panneau. Tout ce que la phase 7 posera dans cette
   colonne a exactement le même défaut. */
.wrap > * { min-width: 0; }

/* L'ÉCRAN FUSIONNÉ. Il a remplacé `.pick-list` — la liste où l'on choisissait
   son comité — parce que la question « lequel ? » a disparu avec le sélecteur :
   les vingt-deux sont là, triés, et le travail se lit sans rien ouvrir.

   Le TITRE de groupe est tout ce qui reste ici : les trois groupes du tri
   (« mes comités », « ils cherchent du monde », « les autres ») séparent une
   page de vingt-deux sections. Ce que l'ancien commentaire annonçait à cet
   endroit — la carte, la rangée, la chip — a été écrit et vit plus bas, dans
   « Les composants ». */
.group-head {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--c-border);
}

.panel {
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  padding: var(--space-5);
}

h1 {
  font-size: var(--mil-fs-6);
  font-weight: 700;
  letter-spacing: -.4px;
  margin: 0 0 var(--space-1);
}

/* En-tête de section : caps, discret, gris (§3 du système de design). */
h2 {
  font-size: var(--mil-fs-1);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
  margin: var(--space-6) 0 var(--space-3);
}

.muted {
  color: var(--c-text-mid);
  font-size: var(--mil-fs-1);
}

/* Les liens DANS le contenu. Sans cette règle ils sortent en bleu par défaut
   du navigateur — une couleur qui n'est pas un jeton, qui ne bascule pas en
   mode sombre, et qui ne ressemble à rien d'autre dans l'app.

   `.ecran` A REJOINT `.panel` LE 2026-08-13, et c'est une réparation. Cette
   règle nommait le PANNEAU, que chaque vue rendait elle-même ; le cadre plein
   écran l'a retiré des six écrans de la section Comités le même jour. Leurs
   liens de contenu sont donc repassés au bleu du navigateur — visible sur
   « Apprécier la participation » et sur les titres de besoins.

   Les deux redéclarations juste sous `.btn-primary` la suivent, et il FAUT
   qu'elles la suivent : sans elles, un `<a class="btn btn-primary">` du cadre
   sortirait en accent SUR l'accent, c'est-à-dire un rectangle bleu sans
   libellé visible. C'est la panne que leur commentaire raconte, et l'ajouter
   ici sans elles la rejouerait à l'identique sur « Nouveau à faire ». */
.panel a,
.ecran a { color: var(--c-accent); }

/* ---------------------------------------------------------------
   Les composants — la carte, la rangée, la chip, le tiroir, l'onglet
   ---------------------------------------------------------------
   CINQ NOMS POUR CE QUE LA PHASE 6 POSAIT ÉCRAN PAR ÉCRAN. Elle devait
   la matière et s'interdisait la forme, ce qui était la bonne décision
   et laisse deux dettes exactes, mesurées avant d'être payées :

     · `.hand-row` servait de rangée générique à DIX points d'usage dans
       neuf vues, sous un nom qui ne parlait que des mains levées ;
     · `.committee`, `.task-row` et `.group-head` étaient des
       placeholders assumés — leur propre commentaire annonçait « le
       travail de la couche de composants (carte, rangée, chip) ».

   Nommer coûtait un renommage à dix endroits. À la dixième vue de plus,
   il en coûte vingt : c'est la seule raison pour laquelle ça se fait
   AVANT d'habiller un écran de plus.

   CE QUE CHAQUE COMPOSANT DOIT À TOUS LES ÉCRANS, écrit ici une fois :

     · 44 px de cible tactile (§0, règle 9) ;
     · l'état en TOUTES LETTRES, jamais porté par la seule couleur
       (règle 6) — une chip contient donc un mot, jamais un point ;
     · `min-width: 0` dès qu'un composant est un élément flex ou de
       grille. Sans lui sa largeur plancher vaut son plus long mot, et
       c'est le DOCUMENT qui se met à défiler de côté ;
     · aucune ombre. Le §0 (règle 2) la réserve aux menus et aux
       modales, et une page de vingt-deux cartes ombrées vibre.
   --------------------------------------------------------------- */

/* LA CARTE — une boîte de contenu DANS le panneau.

   Elle remplace `.committee` et `.bv-thing`, qui étaient la même
   déclaration écrite deux fois : même bordure, même rayon, même
   `--space-4`, à deux endroits qui ne se relisaient jamais ensemble.
   C'est la forme que prend une dette de nommage — pas un bug, deux
   vérités parallèles qui divergeront le jour où l'une bouge.

   La gouttière est SUR LES DEUX BORDS, et les marges adjacentes se
   replient l'une dans l'autre : deux cartes à la suite sont séparées de
   24 px, pas de 48. Le bord du bas n'est pas décoratif — sans lui la
   carte colle au paragraphe qui la suit, vu sur /bienvenue où une phrase
   de bas de page touchait le cadre. */
.card {
  margin: var(--space-5) 0;
  padding: var(--space-4);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}

/* Le nom, et son rang à côté. `flex-wrap` parce que le rang est une chip
   et qu'un nom long plus une chip poussent la carte à 375 px.

   ── ELLE ANNULE CE QUE `h2` POSE, ET C'EST OBLIGATOIRE ──────────────────
   Le titre de carte est un DESSIN, pas un rang : sur /comites il tombe sur
   un `h3` (sous le `h2` du groupe), sur les mains levées sur un `h2` (sous
   le `h1` du comité). Le rang suit le plan du document, que lit un lecteur
   d'écran ; la classe suit l'œil. Mais `h2` porte les caps grises des
   en-têtes de section, et une classe ne gagne que sur ce qu'elle
   REDÉCLARE — sans ces trois lignes, le nom d'une personne se serait
   affiché en petites capitales grises le jour où la carte a changé de
   balise. */
.card-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font-size: var(--mil-fs-4);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text);
}

.card-title > * { min-width: 0; }

/* L'EN-TÊTE DE CARTE — le titre, et ce qui se pousse à sa droite.

   Une rangée de plus que `.card-title` parce que le titre est un `h3` et que
   ce qui l'accompagne n'en fait pas partie : sur /comites, « Ouvrir le comité »
   est un lien vers le comité, pas un mot de son nom. Le mettre DANS le titre
   l'aurait fait annoncer avec lui par un lecteur d'écran.

   `align-items: baseline` et non `center` : le titre est à 18 px, le lien à 14,
   et deux boîtes centrées l'une sur l'autre donnent deux textes qui ne
   s'alignent sur rien. Ce sont leurs lignes de base qui doivent se rejoindre.

   Le repli est la sortie à 375 px, où le nom d'un comité prend déjà la ligne :
   le lien passe dessous. Rien à borner, rien à cacher — mais il ne reste PAS
   collé à droite (voir le repli plus bas), un lien seul aligné sur le bord
   opposé au texte qu'il concerne se lisant comme la commande d'autre chose. */
.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-3);
}

.card-head > * { min-width: 0; }

/* La gouttière du titre passe sur la rangée, sinon elle s'ajoute à celle-ci et
   la carte a deux gouttières là où elle en veut une. */
.card-head > .card-title { margin-bottom: 0; }

.card-head-end {
  margin-left: auto;
  font-size: var(--mil-fs-2);
  /* La cible reste au plancher des contrôles (36 px, §0 règle 9 — les 44 px
     sont pour le geste membre au pouce, pas pour un lien de navigation), et
     elle est obtenue par la HAUTEUR de ligne et non par du remplissage : un
     lien souligné dont on gonfle la boîte donne une zone cliquable qui ne
     ressemble pas à ce qu'on voit. */
  display: inline-flex;
  align-items: center;
  min-height: 36px;
}

/* Le même repli que `.list-row-actions` juste au-dessus, et pour la même raison
   inversée : là-bas la rangée rend sa largeur aux boutons, ici l'en-tête rend
   son alignement au lien. Replié à droite, « Ouvrir le comité » se retrouvait
   seul contre le bord opposé au nom qu'il ouvre — mesuré à 375 px, où le nom
   d'un comité prend la ligne entière à lui seul. */
@media (max-width: 700px) {
  .card-head-end { margin-left: 0; }
}

/* ---------------------------------------------------------------
   L'EN-TÊTE D'UN COMITÉ — le nom, et le geste d'adhésion à droite
   ---------------------------------------------------------------
   Le §6.5 bis avait posé « Je démissionne » sur la carte du catalogue,
   sous un menu « ··· », pour qu'il ne se clique pas par erreur. La
   maquette d'Eric le remonte ici (2026-08-10) ; la raison du repli n'a
   pas disparu avec la décision, elle a changé de porteur — c'est le
   FORMULAIRE qui l'assure maintenant, et le bouton rouge n'existe
   qu'une fois le panneau ouvert.

   Le repli à 375 px met le geste sous le nom, à gauche : un bouton seul
   contre le bord opposé au titre qu'il concerne se lit comme la
   commande d'autre chose — la même raison que sur `.card-head-end`. */
.committee-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
}

.committee-head > * { min-width: 0; }
.committee-head > h1 { margin-bottom: 0; }
.committee-head-end { margin-left: auto; }

/* LE GROUPE DE DROITE (2026-08-13) — les portes du comité et le geste
   d'adhésion sur la MÊME ligne que son nom, demande d'Eric : « move Écrire au
   comité beside Je démissionne ».

   C'est LE GROUPE qui porte le `margin-left: auto`, jamais ses enfants : deux
   frères qui le portent chacun se partagent l'espace libre à parts égales —
   « Écrire au comité » atterrirait au milieu de la ligne, à cinquante
   centimètres du bouton auquel Eric vient de le coller. D'où l'annulation
   explicite sur le `.committee-head-end` imbriqué, que `resign` et `join`
   posent eux-mêmes et qu'on ne veut pas leur retirer : ils servent aussi hors
   de ce groupe. */
.committee-head-actions {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.committee-head-actions > .committee-head-end { margin-left: 0; }

@media (max-width: 700px) {
  .committee-head-end,
  .committee-head-actions { margin-left: 0; }
}

/* Une demande déjà posée : la date, et de quoi la reprendre. Pas un menu —
   il n'y a rien à replier, et les deux se lisent d'un coup. */
div.committee-head-end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* LE PANNEAU DU GESTE — le même `<details>` que le tiroir du catalogue et que
   les menus du pied du rail, avec un `<summary>` qui a l'air d'un bouton.

   ── POURQUOI IL FLOTTE, ALORS QUE `.card-menu` NE FLOTTE PAS ─────────────────
   Le menu d'une carte refuse de se positionner parce que sa pastille atterrit
   où le repli la met, donc un panneau ancré dessus sortirait de l'écran d'un
   côté ou de l'autre. Ici le repère est FIXE : le geste est en haut à droite du
   panneau, toujours. Le panneau s'ancre donc à droite et pousse rien.

   `position: absolute` et non `fixed` : il doit défiler AVEC l'en-tête. Rien
   ne l'écrête — `.panel` n'a pas d'`overflow`, et c'est vérifié par le même
   test qui garde le rail. */
.head-menu { position: relative; }

.head-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  font-size: var(--mil-fs-2);
  color: var(--c-text);
  white-space: nowrap;
}

.head-menu > summary::-webkit-details-marker { display: none; }
.head-menu > summary:hover { background: var(--c-hover-bg); }

/* Le geste qui vise les vingt-deux personnes sans comité est le bouton
   PRIMAIRE de l'écran ; celui qui en sort ne l'est pas. Les deux règles
   redéclarent ce que `.btn-primary` pose, parce qu'un `<summary>` n'est pas un
   bouton et n'hérite de rien. */
.head-menu > summary.btn-primary {
  border-color: var(--c-accent);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-weight: 600;
}

.head-menu > summary.btn-primary:hover { background: var(--c-accent-dark); }

.head-menu-panel {
  position: absolute;
  right: 0;
  z-index: 20;
  width: min(34rem, calc(100vw - var(--space-5) * 2));
  margin-top: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  /* Ombre réservée aux menus et aux modales (§0, règle 2) : c'en est un. */
  box-shadow: 0 4px 16px var(--c-shadow);
  text-align: left;
}

.head-menu-panel > :first-child { margin-top: 0; }
.head-menu-panel > :last-child { margin-bottom: 0; }

/* Le titre du panneau est un `h2` dans le plan du document, mais il n'est pas
   un en-tête de section : ses caps grises en feraient une étiquette au-dessus
   d'un geste sans retour. Même partage que `.card-title` et `.detail-title`. */
.head-menu-panel > h2 {
  margin: 0 0 var(--space-3);
  font-size: var(--mil-fs-3);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text);
}
/* ---------------------------------------------------------------
   LES DEUX VOLETS DE « À FAIRE » — la liste, et le besoin ouvert
   ---------------------------------------------------------------
   La maquette d'Eric dessine un maître-détail : les besoins à gauche,
   celui qu'on a choisi à droite. Sans étape de construction (règle 10),
   ouvrir un besoin est une NAVIGATION — l'URL du besoin rend le même
   écran avec le volet rempli — donc le CSS n'a qu'une chose à faire :
   décider s'il y a la place pour deux colonnes.

   ── POURQUOI CE N'EST PAS LA DISPOSITION RETIRÉE DE LA CARTE ────────
   Une version à deux volets de la CARTE du catalogue a été écrite puis
   retirée le même jour : elle laissait un trou de 294 px en médiane,
   parce qu'un comité a rarement autant de texte que de besoins. Ici les
   deux volets n'ont aucune raison d'avoir la même hauteur et ça ne se
   voit pas : ce sont deux surfaces distinctes, séparées par un trait,
   pas deux moitiés d'une même boîte. Le vide sous une liste courte se
   lit comme de la marge, pas comme un trou.

   ── LE SEUIL EST CELUI DE LA COLONNE, ET IL S'AJOUTE À ELLE ─────────
   1 100 px de FENÊTRE, mais la colonne des comités en prend déjà 250 :
   le volet ne s'ouvre donc en pratique qu'à partir de ~1 350 px. C'est
   voulu et c'est mesuré — à 1 100 avec la colonne, chaque volet
   tomberait à 300 px, la largeur qui avait déjà disqualifié le seuil de
   700 px pour la colonne elle-même. */
.work-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}

.work-split > * { min-width: 0; }

/* ---------------------------------------------------------------
   DEUX VOLETS QUI DÉFILENT CHACUN POUR SOI (2026-08-13)
   ---------------------------------------------------------------
   Demande d'Eric : « content scroll under the tabs — left panel
   with the à faire and right panel with the details,
   independently ».

   ── CE QUI EST DÉJÀ FAIT AILLEURS, ET QU'ON NE REFAIT PAS ──────
   La fenêtre figée, la barre d'écran, l'en-tête du comité qui reste
   en vue : tout ça vient de `<x-ecran>` et de `.ecran-defile`, qui
   couvrent la section entière et toutes les largeurs. Ce bloc-ci ne
   traite que ce qu'il reste — la zone qui défile en porte DEUX sur
   cet écran-là, et une seule barre de défilement pour les deux
   oblige à remonter deux écrans pour changer de besoin.

   ── LA ZONE CÈDE SON DÉFILEMENT À SES DEUX VOLETS ──────────────
   `.ecran-defile` défile ; `.ecran-defile-volets` ne défile plus et
   devient une colonne flex. `.work-split` y prend le reste
   (`flex: 1`), et `min-height: 0` est ce qui l'autorise à RÉTRÉCIR
   sous sa taille de contenu — sans lui, un élément flex refuse de
   passer sous son `min-content`, les volets poussent le cadre et
   rien ne défile. C'est le mode de panne classique de ce montage.

   `position` sur chaque volet, et c'est la règle que `.ecran-defile`
   énonce plus haut : dans ce cadre, toute zone qui reçoit un
   `overflow` reçoit aussi un `position`, sinon les
   `.visually-hidden` — absolus — s'ancrent sur le `<body>`, sortent
   d'elle et étirent le document.

   ── ET `align-items: stretch`, SINON RIEN DE TOUT ÇA NE SERT ────
   `.work-split` pose `align-items: start`, ce qui est juste quand
   la zone défile d'un bloc : deux volets de hauteurs différentes se
   calent en haut. Ici ça les laisse à leur hauteur de contenu, donc
   plus hauts que la piste, donc débordants — et c'est la ZONE qui
   reprendrait le défilement. Ils doivent occuper la piste entière
   pour avoir quelque chose à faire défiler dedans.

   ── AU-DESSUS DE 1 100 px SEULEMENT ────────────────────────────
   C'est le seuil où les deux volets sont côte à côte. En dessous il
   n'y en a qu'un à l'écran (`has-detail` retire l'autre), donc
   « indépendamment » n'a plus de second terme, et `.ecran-defile`
   fait déjà le travail — y compris sur un téléphone.
   --------------------------------------------------------------- */
@media (min-width: 1100px) {
  .ecran-defile-volets {
    position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* La gouttière du bas passe DANS les volets : gardée ici, elle
       poserait une bande morte sous deux zones qui, elles, doivent
       courir jusqu'au bord du cadre. */
    padding-bottom: 0;
  }

  .ecran-defile-volets > .work-split {
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;
  }

  /* `overscroll-behavior` empêche le geste de continuer sur la zone
     quand on arrive au bout d'un volet — c'est ce qui fait la
     différence entre deux zones et une page à trous. */
  .ecran-defile-volets .work-list,
  .ecran-defile-volets .work-detail {
    position: relative;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: var(--space-5);
  }
}

@media (min-width: 1100px) {
  .work-split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-6);
  }

  /* Le trait qui sépare deux surfaces, et pas une bordure qui encadre une
     boîte : le volet n'est pas une carte (§4 — la carte encadre, le trait
     sépare). Sur le VOLET et non sur la liste, pour qu'il disparaisse avec
     lui quand on repasse à une colonne. */
  .work-detail {
    border-left: 1px solid var(--c-border);
    padding-left: var(--space-6);
  }
}

/* SOUS LE SEUIL, LE VOLET REMPLACE LA LISTE — il ne se serre pas sous elle.
   On vient de cliquer sur un besoin : le faire apparaître après vingt lignes
   de liste demanderait de défiler pour voir ce qu'on a demandé. L'onglet
   « Le besoin » dit où l'on est, et le lien du haut ramène.

   `has-detail` est posé par le serveur : une requête média ne peut pas savoir
   qu'un besoin est ouvert. */
@media (max-width: 1099px) {
  .work-split.has-detail .work-list { display: none; }
}

/* Le volet vide n'existe que là où il est côte à côte avec la liste. Empilé
   sous elle, il n'apporterait qu'une phrase qui invite à cliquer sur ce qui
   est juste au-dessus. */
@media (max-width: 1099px) {
  .work-split:not(.has-detail) .work-detail { display: none; }
}

/* LE TITRE D UNE RANGÉE, qui ouvre le volet. Il reste un lien de contenu — la
   liste est DANS le panneau, pas dans une navigation — mais le besoin ouvert
   se distingue du reste : c est le repère qui dit à quelle rangée répond ce
   qu on lit à droite. `aria-current` porte le même état dans le gabarit, pour
   qui ne voit pas la graisse (règle 6). */
.row-card-title.is-open { font-weight: 600; }

/* Le nom du besoin en tête de son volet. `h2` dans le plan du document (sous
   le `h1` du comité) et dessiné comme un titre, pas comme un en-tête de
   section : les caps grises de `h2` en feraient une étiquette là où c'est le
   sujet de tout ce qui suit. Même partage que `.card-title`. */
.detail-title {
  margin: 0 0 var(--space-3);
  font-size: var(--mil-fs-4);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text);
}

/* ==================================================================
   LE VOCABULAIRE D'ICÔNES DES COMITÉS (maquette approuvée, 2026-08-17)
   ==================================================================

   Eric : « much lighter on text, using icons as much as possible ». Quatre
   pièces, partagées entre les rangées « À faire », le volet du besoin et la
   carte de comité (`.comite-flag`, écrit chez elle) :

     · `.faits` / `.fait` — la bande de faits d'une rangée ou d'un volet : un
       dessin, parfois un chiffre ou un mot. La PHRASE de chaque fait reste
       dans le gabarit, `visually-hidden` et en `title` (règle 6) ;
     · `.fait-badge`   — le sans-réponse du coordo : le seul plein de la
       bande, parce que c'est le seul appel à l'action ;
     · `.main-btn`     — la main « ça m'intéresse » d'une rangée, qui porte
       son état (`aria-pressed`, enfoncée = levée) ;
     · `.outil`        — un geste de barre d'outils : l'annonce, la veille,
       le crayon, la croix du volet, la coche qui assigne.

   Tous les dessins suivent le zoom texte (`calc` sur `--mil-fs`) : une icône
   qui ne grossit pas avec les mots redevient un détail pour exactement la
   personne qui a monté le zoom. Et les deux boutons font leurs 44 px
   (règle 9) — c'est le dessin qui est petit, pas la cible. */
.faits {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  color: var(--c-text-mid);
  font-size: var(--mil-fs-1);
}

.fait {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.fait svg {
  width: calc(15px * var(--mil-fs, 1));
  height: calc(15px * var(--mil-fs, 1));
  flex-shrink: 0;
}

/* L'annonce en accent : c'est la seule chose de la bande qui distingue un
   besoin que la coop entière regarde. Le MOT est dans le gabarit. */
.fait-annonce { color: var(--c-accent-dark); }

/* « C'est à toi » : le seul fait qui parle de moi, dans le vert des états —
   et avec son mot écrit à côté, la couleur ne porte jamais le sens seule. */
.fait-atoi { color: var(--c-success-text); }

/* Le compte SANS RÉPONSE — plein là où tout le reste est au trait, parce que
   c'est l'appel à l'action du coordo et rien d'autre ne doit le concurrencer.
   Rond aux mêmes lois que `.comite-count` : `min-width` égale à la hauteur. */
.fait-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-0);
  font-weight: 600;
}

/* LA MAIN D'UNE RANGÉE. Un carré de 44 (règle 9) qui dit son état par la
   forme ET par les mots : enfoncée (`.is-on`) = ma main est levée, et le
   gabarit écrit `aria-pressed` avec la phrase entière dans le bouton. Le
   dessin est le même dans les deux états — c'est le fond qui répond, comme
   l'entrée allumée du rail. */
.main-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  color: var(--c-text-mid);
  cursor: pointer;
}

.main-btn svg {
  width: calc(18px * var(--mil-fs, 1));
  height: calc(18px * var(--mil-fs, 1));
}

.main-btn:hover {
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
}

.main-btn.is-on {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
}

/* UN OUTIL DE BARRE — sans bord au repos, parce qu'une rangée de quatre
   ronds bordés à côté d'un titre se lit comme un damier : le fond au survol
   et l'état enfoncé suffisent à dire « bouton ». 40 px de dessin dans 44 de
   cible : le padding fait la différence.

   ROND depuis le 2026-08-19 : les gestes à icône de l'app parlent en ronds
   (`.outil-rond` plus bas, `.outil-chip` déjà à 50 %) — le crayon de la barre
   est le même geste que le crayon d'une rangée de tuiles, il prend la même
   forme. Au repos rien ne change ; c'est le survol et l'état enfoncé qui
   deviennent des cercles. */
.outil {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--c-text-mid);
  cursor: pointer;
  text-decoration: none;
}

.outil svg {
  width: calc(18px * var(--mil-fs, 1));
  height: calc(18px * var(--mil-fs, 1));
}

/* Deux des outils sont des LIENS (le crayon vers sa modale, la croix qui
   ferme le volet), et `.ecran a` (0,1,1) bat `.outil` (0,1,0) : sans ces
   redéclarations ils sortent en accent au milieu de boutons gris — la panne
   exacte de `.ecran a.btn`, quarante lignes sous la règle des liens. */
.panel a.outil,
.ecran a.outil { color: var(--c-text-mid); }

.outil:hover,
.panel a.outil:hover,
.ecran a.outil:hover {
  background: var(--c-hover-bg);
  color: var(--c-text);
}

.outil.is-on {
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
}

/* La barre elle-même : collée à droite du titre par le `space-between` de
   `.liste-head`, et les outils serrés — quatre gestes d'une même famille se
   lisent groupés, pas espacés comme des sections. */
.volet-outils {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* UN TITRE DE SECTION DU VOLET — petites capitales muettes, le dessin devant.
   Le volet en porte quatre à la file (les étapes, l'équipe, les mains, le
   reste du comité) : au cran d'un `h2` de page ils pesaient plus lourd que le
   titre du besoin. En capitales de 12 px ils redeviennent ce qu'ils sont — des
   étiquettes de groupes, pas des sujets. Le `h2` reste un `h2` : la hiérarchie
   du document ne change pas de forme avec l'encre (règle 6). */
.volet-sec-titre {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--mil-fs-0);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--c-text-muted);
}

.volet-sec-titre svg {
  width: calc(14px * var(--mil-fs, 1));
  height: calc(14px * var(--mil-fs, 1));
  flex-shrink: 0;
}

/* UNE PERSONNE DANS LE VOLET D'UN BESOIN — une chip : la vignette, le nom, le
   geste. La vignette est `.annuaire-avatar` — le même dessin pour la même
   personne partout — réduite au cran de la chip. Les précisions (provenance,
   date de la main, état) sont dans le `title` et à voix haute : la chip
   montre QUI, le survol dit LE RESTE. */
.gens {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.chip-personne {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px;
  padding-right: var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface-2);
  font-size: var(--mil-fs-1);
}

.chip-personne .annuaire-avatar {
  width: calc(26px * var(--mil-fs, 1));
  height: calc(26px * var(--mil-fs, 1));
  border-radius: 50%;
  font-size: var(--mil-fs-0);
}

.chip-nom { white-space: nowrap; }

/* Le geste d'une chip : la coche assigne, la croix désassigne. Plus petit que
   l'`.outil` de barre — un rond du gabarit de la vignette d'à côté — et c'est
   l'entorse assumée à la cible de 44 px (règle 9) : la chip entière fait la
   rangée, et un bouton de 44 dans une chip de 32 ferait la chip pour lui. */
.outil-chip {
  width: calc(30px * var(--mil-fs, 1));
  height: calc(30px * var(--mil-fs, 1));
  border-radius: 50%;
  margin: -2px -6px -2px calc(-1 * var(--space-1));
}

.outil-chip svg {
  width: calc(14px * var(--mil-fs, 1));
  height: calc(14px * var(--mil-fs, 1));
}

/* La croix qui désassigne rougit au survol : le geste se voit venir avant de
   partir. Elle a été un bouton rouge en toutes lettres tant que la personne
   était une rangée ; le mot entier vit dans le `title` et à voix haute. */
.outil-retrait:hover {
  background: var(--c-danger-soft);
  color: var(--c-danger-text);
}

/* LE BOUTON ROND D'UN OBJET EN CONTEXTE (Eric, 2026-08-19) — ajouter,
   monter, descendre, modifier, retirer : les gestes d'une tuile, d'une
   section, d'une rubrique, posés À CÔTÉ de l'objet qu'ils touchent.

   BORDÉ là où l'`.outil` de barre est nu, et la différence n'est pas un
   goût : un `.outil` vit dans une barre qui le groupe et le fait lire comme
   un bouton ; ces gestes-ci vivent seuls au bout d'une rangée de liste, et
   sans bord quatre dessins y flottent comme des puces. ROND là où `.btn`
   est rectangulaire : le rectangle reste aux surfaces qui portent des mots
   (modales, formulaires), le rond dit « un geste, pas une phrase ». Le mot
   entier vit dans le `title` et à voix haute, comme sur les chips du volet.

   44 px pleins (règle 9) : contrairement à la chip, rien ici ne force plus
   petit. */
.outil-rond {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text-mid);
  cursor: pointer;
  text-decoration: none;
}

.outil-rond svg {
  width: calc(18px * var(--mil-fs, 1));
  height: calc(18px * var(--mil-fs, 1));
}

/* Le + et le crayon sont des LIENS (ils sautent à leur modale sans script) :
   même redéclaration que `.panel a.outil`, même panne évitée — `.panel a`
   (0,1,1) les sortirait en accent au milieu de gestes gris. */
.panel a.outil-rond,
.ecran a.outil-rond { color: var(--c-text-mid); }

.outil-rond:hover,
.panel a.outil-rond:hover,
.ecran a.outil-rond:hover {
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
}

/* L'AJOUT GARDE SON RANG : il était le `.btn-primary` de son écran, il reste
   plein là où les gestes de rangée sont au trait. Un seul par écran — les
   ajouts répétés par rangée (« + rubrique ») restent au trait, sinon quatre
   pleins se disputent l'œil sur une liste. Les paires `a.…` couvrent le lien
   qu'il est toujours. */
.outil-rond.outil-principal,
.panel a.outil-rond.outil-principal,
.ecran a.outil-rond.outil-principal {
  border-color: var(--c-accent);
  background: var(--c-accent);
  color: var(--c-on-accent);
}

.outil-rond.outil-principal:hover,
.panel a.outil-rond.outil-principal:hover,
.ecran a.outil-rond.outil-principal:hover {
  border-color: var(--c-accent-dark);
  background: var(--c-accent-dark);
  color: var(--c-on-accent);
}

/* Le retrait d'un objet rougit comme celui d'une chip — même règle, au
   gabarit de rangée ; le bord suit, puisque ce bouton-ci en a un. Écrite
   APRÈS le survol générique d'au-dessus pour le battre à spécificité
   supérieure (0,3,0). */
.outil-rond.outil-retrait:hover {
  border-color: var(--c-danger);
  background: var(--c-danger-soft);
  color: var(--c-danger-text);
}

/* LES ÉTAPES, EN PUCES — informatives seulement (Eric, 2026-08-18). Elles ont
   été des cases à cocher pendant quelques heures ; l'exécution a été défaite
   le jour même, et la liste redevient ce qu'elle dit : ce que la tâche
   demande, à lire avant de lever la main. Les puces du navigateur, le retrait
   qui les aligne sous le titre de section, et rien qui ressemble à un geste. */
.etapes {
  margin: 0;
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--mil-fs-2);
}

/* L'ÉTOILE DE L'EN-TÊTE — le rang en dessin sur la ligne du nom, au gabarit
   du titre qu'elle qualifie : les 15 px de la colonne feraient une poussière
   à côté d'un h1. Même dessin, même accent — c'est le vocabulaire. */
.committee-head .comite-flag svg {
  width: calc(20px * var(--mil-fs, 1));
  height: calc(20px * var(--mil-fs, 1));
}

/* L'ONGLET QUI N'EXISTE QUE SUR UN ÉCRAN ÉTROIT. Un seul s'en sert —
   « Le besoin », qui EST le volet de droite : là où les deux volets tiennent
   côte à côte, un onglet vers celui de droite mènerait à ce qu'on regarde
   déjà. Il est rendu côté serveur et retiré ici, parce que la largeur de
   l'écran n'est pas une donnée que le serveur connaît. */
@media (min-width: 1100px) {
  /* `.tabs .tab.is-narrow` et non `.tab.is-narrow` : l'onglet est dessiné par
     `.tabs .tab` (0,2,0), plus bas dans le fichier. À spécificité égale c'est
     le dernier qui gagne, donc la règle d'ici ne faisait rien — l'onglet
     restait visible à 1 600 px, où il ne sert à rien. Une règle CSS sans effet
     ne casse aucun test : seul le DOM rendu le dit, et c'est le même piège que
     le `:not()` du §5 quater. */
  .tabs .tab.is-narrow { display: none; }
}

/* LE RETOUR À LA LISTE, MÊME MÉCANIQUE QUE L'ONGLET ÉTROIT ci-dessus, pour les
   volets qui n'ont pas de barre d'onglets au-dessus d'eux — `/reparations` est
   le premier. Sous le seuil, `.work-split.has-detail .work-list` retire la
   liste : sans ce lien, la seule façon d'y revenir serait le bouton Précédent
   du navigateur, c'est-à-dire un geste hors de l'écran.

   Retiré au-dessus du seuil parce que la liste est alors juste à gauche, et un
   lien vers ce qu'on regarde déjà ne fait qu'ajouter une tabulation. */
@media (min-width: 1100px) {
  .detail-retour { display: none; }
}

/* LA PHOTO D'UN BRIS, dans son volet. Bornée en hauteur : une photo de
   téléphone en portrait fait 4 000 px de haut, et rendue à sa taille elle
   pousserait sous la ligne de flottaison tout ce qui vient après — l'état, la
   réponse du comité, et les boutons. `contain` plutôt que `cover` : sur un
   bris, ce qu'on rogne est peut-être exactement la fissure.

   `cursor: zoom-in` annonce que l'image mène à sa version pleine taille — le
   lien est déjà là, mais rien ne le disait sur une image. */
.bris-photo {
  display: block;
  max-width: 100%;
  max-height: 320px;
  object-fit: contain;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  cursor: zoom-in;
}

/* Dans la liste de qui a signalé, la même photo est bornée plus court : trois
   photos de téléphone en pleine hauteur feraient de la page un défilement, et
   ce qu'on vient lire ici est l'état, pas l'image. Le lien vers la version
   pleine taille est le même. */
.bris-photo-vignette { max-height: 150px; }

/* ==================================================================
   LES BRIS — DEUX VUES, DEUX FORMES
   ==================================================================

   Demandé par Eric le 2026-08-14 : « the requester view » et « the Entretien
   comité view ». Elles partagent une adresse et un bouton, pas une mise en
   page — le raisonnement est en tête de `reparations/index.blade.php`.

   La vue du comité réutilise `work-split` et n'ajoute que ses piles et son
   horloge. Tout ce qui suit sert donc surtout la vue de qui signale, qui est
   la seule des deux à introduire une forme neuve. */

/* `.bris-tete` est parti avec la tête de liste (2026-08-17) : la barre
   d'écran du cadre porte maintenant le titre, les états et les actions. */

/* ------------------------------------------------------------------
   La vue de qui signale — des cartes, pas une liste
   ------------------------------------------------------------------ */

.bris-cartes {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* La carte EST le dossier : elle est encadrée (« la carte encadre, le trait
   sépare », §4), à la différence du volet du comité qui n'est qu'un trait. */
.bris-carte {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: var(--space-4);
}

/* Le bris désigné par l'adresse. Un liseré et non un fond : la carte porte
   déjà du texte sur toute sa surface, et une teinte de fond y ferait baisser
   le contraste de tout ce qu'on vient lire. */
.bris-carte.is-open {
  border-color: var(--c-accent);
  box-shadow: inset 3px 0 0 var(--c-accent);
}

/* Un bris réparé s'efface sans disparaître : c'est ce qui fait que la liste
   atteint le calme (règle 8) tout en gardant le dossier lisible. */
.bris-carte.est-close { background: var(--c-surface-2); }

.bris-carte-titre {
  margin: 0;
  font-size: var(--mil-fs-4);
  font-weight: 600;
}

.bris-carte-meta { margin: var(--space-1) 0 0; font-size: var(--mil-fs-1); }

/* L'ÉTAT, la ligne la plus forte de la carte. La couleur ne fait que teinter
   une phrase qui se suffit à elle-même (règle 6) : coupe la feuille de style,
   « Pas à la charge de la coop » se lit encore. */
.bris-carte-etat {
  margin: var(--space-3) 0 0;
  font-size: var(--mil-fs-3);
  font-weight: 600;
}
.bris-carte-etat.etat-fait { color: var(--c-success-text); }
.bris-carte-etat.etat-refuse { color: var(--c-danger-text); }
.bris-carte-etat.etat-pris { color: var(--c-accent-dark); }

.bris-carte-detail { margin: var(--space-3) 0 0; }

/* LA RÉPONSE DU COMITÉ. Sur un refus, c'est le bloc le plus lourd de la carte
   — le seul moment où la coop dit non à quelqu'un, et ce que l'écran promet
   plus haut en toutes lettres. */
.bris-reponse {
  margin: var(--space-3) 0 0;
  padding: var(--space-3);
  border-left: 2px solid var(--c-border-strong);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--c-surface-2);
}
.bris-reponse.est-refus {
  background: var(--c-danger-soft);
  border-left-color: var(--c-danger);
}
.bris-reponse > h4 {
  margin: 0 0 var(--space-1);
  font-size: var(--mil-fs-0);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--c-text-mid);
}
.bris-reponse.est-refus > h4 { color: var(--c-danger-text); }
.bris-reponse > p { margin: 0; }
.bris-reponse > p + p { margin-top: var(--space-2); }

.bris-rdv {
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  background: var(--c-accent-soft);
  font-weight: 500;
}

/* Zéro signalement est l'écran par défaut, pas un cas limite : il porte le
   geste plutôt que d'annoncer une absence. */
.bris-vide {
  text-align: center;
  padding: var(--space-6) var(--space-4);
  border: 1px dashed var(--c-border-strong);
  border-radius: var(--radius);
  color: var(--c-text-mid);
}
.bris-vide > p { margin: 0 0 var(--space-4); }

/* ------------------------------------------------------------------
   Le fil des cinq étapes
   ------------------------------------------------------------------

   VERTICAL À TOUTES LES LARGEURS. La charte : « le téléphone est le design,
   pas l'adaptation — et le grand écran montre la même chose, plus à la fois.
   Jamais l'inverse. » Un fil horizontal qui bascule sous 700 px serait deux
   formes à apprendre pour une seule information, et l'horizontale n'a pas la
   place d'écrire « Pris par Camille Roy » dans l'étape.

   Le trait est un `::after` sur chaque `li`, pas une bordure du `ol` : il doit
   pouvoir changer de couleur d'une étape à l'autre pour montrer où le fil
   s'arrête. */
.bris-fil {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
}
.bris-fil > li {
  position: relative;
  padding: 0 0 var(--space-3) var(--space-5);
  min-height: var(--space-5);
  font-size: var(--mil-fs-2);
  line-height: 1.4;
}
.bris-fil > li:last-child { padding-bottom: 0; }

.bris-fil-point {
  position: absolute;
  top: 4px;
  left: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--c-surface);
  border: 1.5px solid var(--c-border-strong);
  z-index: 1;
}
.bris-fil > li::after {
  content: "";
  position: absolute;
  top: 14px;
  left: 4.25px;
  width: 1.5px;
  height: 100%;
  background: var(--c-border);
}
.bris-fil > li:last-child::after { display: none; }

.bris-fil-mot { font-weight: 600; }
.bris-fil-date { color: var(--c-text-muted); margin-left: var(--space-2); }

/* Franchi. */
.bris-fil > li.est-done > .bris-fil-point {
  background: var(--c-success);
  border-color: var(--c-success);
}
.bris-fil > li.est-done::after { background: var(--c-success); }

/* Ce qu'on attend maintenant. L'anneau est ce qui le distingue d'un point
   franchi sans recourir à la seule teinte — le mot et la date le disent
   déjà, la forme le répète. */
.bris-fil > li.est-now > .bris-fil-point {
  background: var(--c-accent);
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-soft);
}
.bris-fil > li.est-now .bris-fil-mot { color: var(--c-accent-dark); }

/* Pas encore. Le mot pâlit, la date devient « — ». */
.bris-fil > li.est-todo .bris-fil-mot { color: var(--c-text-muted); font-weight: 500; }

/* ÇA N'IRA PAS PLUS LOIN. La seule marque qui change le sens du fil au lieu de
   le faire avancer : le trait qui suit s'éteint, pour que trois étapes grises
   ne promettent pas silencieusement que quelqu'un viendra. */
.bris-fil > li.est-stop > .bris-fil-point {
  background: var(--c-danger);
  border-color: var(--c-danger);
}
.bris-fil > li.est-stop .bris-fil-mot { color: var(--c-danger-text); }
.bris-fil > li.est-stop::after { background: var(--c-border); }

/* ------------------------------------------------------------------
   La vue du comité — les piles et l'horloge
   ------------------------------------------------------------------ */

.bris-pile + .bris-pile { margin-top: var(--space-5); }

.bris-pile > .liste-head { margin-bottom: var(--space-1); }

/* Le compte vit à côté du titre et jamais dedans : un titre qui change de
   longueur avec le nombre fait sauter la mise en page à chaque geste. */
.bris-pile-compte {
  font-size: var(--mil-fs-1);
  font-weight: 600;
  color: var(--c-warning-text);
  background: var(--c-warning-soft);
  border-radius: var(--radius-pill);
  padding: 1px var(--space-2);
}
/* Zéro n'est pas une alerte, c'est une victoire (règle 8). */
.bris-pile-compte.est-zero {
  color: var(--c-success-text);
  background: var(--c-success-soft);
}

.bris-pile-verbe {
  margin: 0 0 var(--space-2);
  font-size: var(--mil-fs-1);
  color: var(--c-text-muted);
}

.bris-pile-vide {
  margin: 0;
  font-size: var(--mil-fs-1);
  color: var(--c-success-text);
  background: var(--c-success-soft);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
}

/* L'ÂGE. Trois crans, et le mot dit toujours ce qu'on attend — la teinte ne
   fait que hiérarchiser (règle 6). Les seuils sont larges à dessein : une coop
   d'habitation n'est pas un centre d'appels. */
.bris-age { font-size: var(--mil-fs-1); color: var(--c-text-mid); }
.bris-age.est-mur { color: var(--c-warning-text); font-weight: 500; }
.bris-age.est-vieux { color: var(--c-danger-text); font-weight: 600; }

/* La pastille d'état DANS une bande de faits (2026-08-18, la rangée en bande) :
   le fait espace déjà ses morceaux (`gap`), et la marge que la pastille porte
   pour ses emplois en pleine phrase s'y additionnerait — douze pixels entre le
   point et son mot, quatre entre les mots. */
.fait > .bris-point { margin-right: 0; }

/* ------------------------------------------------------------------
   LA PASTILLE D'ÉTAT — demandée par Eric le 2026-08-16 pour la carte
   « Réparations » du tableau de bord, puis pour les rangées de `/bris`
   ------------------------------------------------------------------

   Le `dot` du design system (9 px, rond, plein), posé devant l'état écrit en
   toutes lettres. Elle vit ici et non dans la section du tableau de bord parce
   que la couleur qu'elle porte est celle des BRIS : qui retouche `.etat-pris`
   ou `.est-stop` doit voir du même coup les trois endroits qui teintent le même
   état, sans quoi un bris pris sera bleu sur un écran et vert sur l'autre.

   Une pile de `/bris` EST un état — six rangées, six fois le même point. Ce
   n'est pas une redondance à corriger : c'est le prix d'un gabarit de rangée
   unique pour les six listes de l'écran, et ce que ça achète est les trois
   tiroirs MÊLÉS du bas, où un refus et un bris fait se suivent.

   ── ELLE COLORE QUATRE ÉTATS LÀ OÙ LA CARTE DE `/bris` N'EN TEINTE QUE TROIS ─
   `.bris-carte-etat` laisse « signalé » et « approuvé » sans teinte, à raison :
   là-bas la couleur n'est qu'un appui sur une phrase déjà lisible, et l'attente
   normale n'a rien à signaler. Ici, l'absence de teinte serait l'absence de
   pastille — une rangée sans marque dans une colonne qui en a trois se lit
   comme un défaut d'affichage, pas comme du calme. Les deux attentes partagent
   donc l'ambre : ce que la pastille répond en un balayage, c'est « est-ce que
   ça bouge ? », et à cette question-là les deux disent la même chose. Lequel des
   deux, c'est le MOT juste à côté qui le dit.

   Règle 6 tenue de la même façon que dans le fil : coupe la feuille de style,
   « Refusé » est encore écrit. La pastille n'ajoute qu'un repère de balayage. */
.bris-point {
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: var(--space-2);
  border-radius: 50%;
  background: var(--c-border-strong);

  /* `middle` aligne le milieu de la pastille sur la moitié de la hauteur d'x —
     donc sur les minuscules du mot d'à côté. `baseline` la ferait reposer sur
     la ligne d'écriture, c'est-à-dire trois pixels trop bas. */
  vertical-align: middle;
}

/* Ça attend un geste — le triage, ou quelqu'un qui prend. Le même ambre que le
   compte des piles de `/bris`, qui compte exactement ces deux-là. */
.bris-point.est-attente { background: var(--c-warning); }

/* Quelqu'un s'en occupe : l'accent, comme l'étape en cours du fil. */
.bris-point.est-pris { background: var(--c-accent); }

.bris-point.est-fait { background: var(--c-success); }
.bris-point.est-refuse { background: var(--c-danger); }

/* ------------------------------------------------------------------
   « TOUS LES BRIS » — les chips d'état (Eric, 2026-08-17)
   ------------------------------------------------------------------

   Des LIENS en forme de pilules, l'idiome des tris de l'annuaire : l'état se
   dit par `aria-pressed`, jamais par la couleur seule — la chip pressée gagne
   aussi la graisse et la bordure forte, ce qui se distingue en niveaux de
   gris (règle 6). */

.bris-filtres {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
}

.bris-filtre {
  display: inline-block;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  padding: 2px var(--space-3);
  font-size: var(--mil-fs-1);
  text-decoration: none;
}

/* `.panel a` (0,1,1) peint les liens en accent avec un cran de plus que
   `.bris-filtre` (0,1,0) : la couleur se redéclare donc à trois crans, comme
   `.panel a.btn` — sans ça la règle du dessus est juste à la lecture et
   fausse à l'écran. */
.panel a.bris-filtre { color: var(--c-text-mid); }

.bris-filtre:hover { background: var(--c-hover-bg); }

.panel a.bris-filtre[aria-pressed='true'] { color: var(--c-text); }

.bris-filtre[aria-pressed='true'] {
  border-color: var(--c-border-strong);
  background: var(--c-surface-2);
  font-weight: 600;
}

/* Le compte, dans un gris qui recule : c'est le mot qui filtre, le chiffre
   ne fait qu'informer. */
.bris-filtre-compte { color: var(--c-text-muted); font-weight: 400; }

/* ------------------------------------------------------------------
   L'ANNUAIRE À TROIS VOLETS — chercher, parcourir, lire
   ------------------------------------------------------------------

   Demandé par Eric le 2026-08-12 : « recherche à facettes à trois volets ».
   C'est `.work-split` avec une colonne de plus, et les mêmes principes — le
   trait sépare, il n'encadre pas ; le serveur pose ce que la requête média ne
   peut pas deviner ; sous le seuil, le volet PREND la place de la liste au lieu
   de se serrer sous elle.

   ── TROIS SEUILS, PARCE QU'IL Y A TROIS COLONNES ────────────────────────────
   ·   0 →  899 px : une colonne. On empile, et la fiche remplace tout.
   · 900 → 1279 px : deux colonnes. Les filtres tiennent à côté de la liste ;
                     dès qu'une fiche s'ouvre, ce sont les FILTRES qui cèdent —
                     pas la liste. C'est le choix qui distingue cet écran de
                     celui des comités, et il vient de l'usage : on règle les
                     filtres une fois, puis on parcourt. Garder la liste, c'est
                     garder les flèches — une carte cachée n'est pas une carte
                     pour `cartes.js`, donc masquer la liste éteindrait le
                     raccourci au moment précis où il sert.
   · 1280 px et + : les trois, et plus rien ne disparaît.

   ── POURQUOI 1 280 ET PAS 1 100 ─────────────────────────────────────────────
   Mesuré : le rail prend 84 px, `.wrap` et `.panel` 24 de chaque côté chacun.
   À 1 100 px il reste ~1 004 px pour trois colonnes et deux gouttières de
   32 — la fiche tomberait à ~330 px, où sept jours de disponibilités et une
   liste de compétences se replient mot par mot. À 1 280, elle a ~420 px, ce
   qui est la largeur d'un volet qui se lit. L'écran n'a PAS la colonne des
   comités, contrairement à `/comites` : c'est ce qui permet à ce seuil-ci
   d'être plus proche de la vraie largeur de fenêtre. */
.annuaire-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}

/* Même raison que `.wrap > *` : la taille minimale automatique d'un élément de
   grille vaut sa largeur min-content, donc un `1fr` ne borne rien tant qu'on ne
   la rend pas à la colonne. Sans ça, un nom de compétence non coupable élargit
   la colonne des filtres et pousse la liste hors de l'écran. */
.annuaire-split > * { min-width: 0; }

/* Le titre du volet des filtres est le PREMIER élément d'une carte : la marge
   haute de `h2` (var(--space-6), calibrée pour séparer deux sections d'une même
   page) y ouvrirait un vide de 32 px au-dessus du mot. */
.annuaire-facets-title { margin-top: 0; }

/* ── UNE COLONNE : ON EMPILE, ET LA FICHE REMPLACE TOUT ────────────────────
   `has-detail` est posé par le serveur — une requête média ne peut pas savoir
   qu'une fiche est ouverte. On vient de cliquer sur quelqu'un : la faire
   apparaître après les filtres et cent six cartes demanderait de défiler pour
   voir ce qu'on a demandé. Le lien « ← Les membres » en tête du volet est le
   retour, et il n'existe qu'ici. */
@media (max-width: 899px) {
  .annuaire-split.has-detail .annuaire-facets,
  .annuaire-split.has-detail .annuaire-list { display: none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   À L'ÉTROIT, LES FILTRES SE REPLIENT EN VOLET (Eric, 2026-08-13)

   « Narrow fallback should be a collapse side panel for filters. As little
   scrolling as possible. »

   Empilés, les trois cartes de filtres poussaient les cent six personnes à
   deux écrans de défilement : on arrivait sur un écran de recherche dont le
   résultat était hors champ. En volet, la liste commence tout de suite, et les
   filtres reviennent par un bouton.

   ── OÙ ÇA S'APPLIQUE, ET C'EST PLUS LARGE QU'« UN TÉLÉPHONE » ──────────────
   Partout où la colonne des filtres n'a pas de place :
     · sous 900 px, toujours ;
     · entre 900 et 1 280 px QUAND une fiche est ouverte — là, les filtres
       disparaissaient purement et simplement, et le seul moyen de les
       retrouver était de refermer la fiche.
   Le second cas n'était pas dans la demande ; c'est le même défaut, et il se
   corrige avec la même pièce.

   ── LA RÈGLE 4 DE LA CHARTE EST TENUE, ET ELLE EST LE VRAI RISQUE ──────────
   « Rien d'important ne vit uniquement dans un geste. » Un volet qu'on ouvre
   d'un glissement serait exactement ça. Trois choses l'évitent :
     · on l'ouvre par un BOUTON étiqueté « Filtres », toujours visible ;
     · ce bouton porte une PASTILLE avec le nombre de filtres actifs — donc
       « ta liste est réduite » se lit sans ouvrir quoi que ce soit, ce qui est
       la seule information qui compte quand le volet est fermé ;
     · il se ferme au bouton, au voile, et à Échap.

   ── ET IL N'EXISTE QU'AVEC LE SCRIPT ───────────────────────────────────────
   `est-repliable` est posée par `annuaire.js`, comme le bouton est démasqué
   par lui. Sans script, la colonne reste empilée : plus de défilement, mais
   tout est atteignable — un volet fermé qu'aucun bouton n'ouvre serait, lui,
   une page dont les filtres n'existent plus.
   ════════════════════════════════════════════════════════════════════════════ */
/* Sous 900 px : toujours replié. Entre 900 et 1 280 px : seulement quand une
   fiche est ouverte — sans fiche la colonne a sa place, et la replier cacherait
   des filtres qui tenaient à l'écran.

   Les sélecteurs passent par `.annuaire-plein`, et ce n'est pas décoratif : la
   règle qui rend les trois colonnes défilantes (`.annuaire-plein
   .annuaire-facets`) pèse le même poids et s'écrit plus bas. Sans ce nœud de
   plus, elle gagnait, le volet redevenait une colonne statique, et il
   s'affichait par-dessus la liste en permanence. */
@media (max-width: 899px) {
  .annuaire-plein .annuaire-split.est-repliable .annuaire-facets { 
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1200;
    width: min(calc(340px * var(--mil-fs, 1)), 92vw);
    max-height: none;
    display: flex;
    flex-direction: column;
    padding: var(--space-4);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--c-bg);
    border-right: 1px solid var(--c-border);
    box-shadow: 4px 0 24px var(--c-shadow-strong);
    transform: translateX(-102%);
    /* `visibility` en plus du déplacement : un volet seulement déplacé reste
       dans l'ordre de tabulation, donc la tabulation part dans un panneau
       invisible et le focus disparaît de l'écran. Retardée le temps de
       l'animation, sinon la fermeture serait sèche. */
    visibility: hidden;
    transition: transform var(--t-slow) ease, visibility 0s linear var(--t-slow);
   }
  .annuaire-plein .annuaire-split.est-repliable.volet-ouvert .annuaire-facets { 
    transform: none;
    visibility: visible;
    transition: transform var(--t-slow) ease, visibility 0s;
   }
  .annuaire-plein .annuaire-split.est-repliable .annuaire-volet-tete { display: flex; }
  .est-repliable .annuaire-commande.annuaire-filtres-bouton { display: inline-flex; }
  .est-repliable .annuaire-voile { display: block; }
}

@media (min-width: 900px) and (max-width: 1279px) {
  .annuaire-plein .annuaire-split.est-repliable.has-detail .annuaire-facets { 
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1200;
    width: min(calc(340px * var(--mil-fs, 1)), 92vw);
    max-height: none;
    display: flex;
    flex-direction: column;
    padding: var(--space-4);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--c-bg);
    border-right: 1px solid var(--c-border);
    box-shadow: 4px 0 24px var(--c-shadow-strong);
    transform: translateX(-102%);
    /* `visibility` en plus du déplacement : un volet seulement déplacé reste
       dans l'ordre de tabulation, donc la tabulation part dans un panneau
       invisible et le focus disparaît de l'écran. Retardée le temps de
       l'animation, sinon la fermeture serait sèche. */
    visibility: hidden;
    transition: transform var(--t-slow) ease, visibility 0s linear var(--t-slow);
   display: flex; }
  .annuaire-plein .annuaire-split.est-repliable.has-detail.volet-ouvert .annuaire-facets { 
    transform: none;
    visibility: visible;
    transition: transform var(--t-slow) ease, visibility 0s;
   }
  .annuaire-plein .annuaire-split.est-repliable.has-detail .annuaire-volet-tete { display: flex; }
  .est-repliable.has-detail .annuaire-commande.annuaire-filtres-bouton { display: inline-flex; }
  .est-repliable.has-detail .annuaire-voile { display: block; }
}

/* LE BOUTON DES FILTRES N'EXISTE QUE LÀ OÙ IL OUVRE QUELQUE CHOSE.

   Il porte le dessin de `.annuaire-commande` — même ligne, même poids que
   « Prénom » et « Nom » —, et rien d'autre en propre : sa seule règle est
   celle-ci, qui le CACHE. Les deux régimes où la colonne des filtres devient un
   volet flottant le rendent visible (plus haut, dans leurs requêtes média) ;
   partout ailleurs la colonne est sous les yeux, et un bouton qui l'ouvrirait
   n'ouvrirait rien.

   Il a passé une heure visible à toutes les largeurs, à replier la colonne pour
   rendre ses 240 px à la liste. C'était une commande inventée à partir d'un
   repère de maquette — Eric : « Filtres n'était pas un toggle, juste un
   placeholder. »

   ⚠️ `.annuaire-commande.annuaire-filtres-bouton` et non la seconde classe
   seule : `.annuaire-commande` pose `display: inline-flex` et vit PLUS BAS dans
   le fichier. À spécificité égale c'est le dernier qui gagne, donc la règle
   d'ici n'aurait rien caché du tout — le bouton restait visible à 1 512 px, où
   il n'ouvre rien. Même piège que `.tabs .tab.is-narrow` et que
   `.row-card .annuaire-stamp` avant lui : dans cette feuille, une règle qui
   DÉFAIT doit peser plus lourd que celle qu'elle défait, jamais compter sur sa
   place. Les deux requêtes média qui le remontrent pèsent un cran de plus
   encore. */
.annuaire-commande.annuaire-filtres-bouton { display: none; }

/* LE NOMBRE DE FILTRES ACTIFS, en clair dans la pastille — pas une couleur qui
   voudrait dire « il y en a » (règle 8 du système de design). C'est lui qui
   tient la règle 4 quand le volet est fermé : « ta liste est réduite » doit se
   lire sans rien ouvrir. */
.annuaire-filtres-compte {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(20px * var(--mil-fs, 1));
  padding: 0 var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-0);
  font-weight: 600;
}

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

/* La tête du volet — son nom et sa fermeture. Elle ne sert qu'au volet replié :
   en colonne, la carte des filtres porte déjà son titre. */
.annuaire-volet-tete {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}


.annuaire-volet-tete h2 { margin: 0; }

.annuaire-volet-fermer {
  min-height: 44px;
  min-width: 44px;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  color: var(--c-text-mid);
  cursor: pointer;
}

.annuaire-volet-fermer:hover { background: var(--c-hover-bg); color: var(--c-text); }

/* LE VOILE — il retire la liste du champ le temps qu'on règle, et c'est la
   moitié de son travail. L'autre est de dire, sans un mot, que ce qui est
   dessous attend : un volet qui glisse par-dessus une page restée nette se lit
   comme une superposition accidentelle.

   `display: none` par défaut, rendu visible par les deux requêtes média
   ci-dessus. `opacity` et `pointer-events` séparés du `display` : on ne peut
   pas animer une apparition depuis `display: none`, et un voile qui apparaît
   d'un coup fait sursauter là où le volet, lui, glisse. */
.annuaire-voile {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1100;
  border: 0;
  padding: 0;
  background: var(--c-overlay);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-slow) ease;
}

.volet-ouvert .annuaire-voile {
  opacity: 1;
  pointer-events: auto;
}

/* Le volet vide n'existe que là où il est côte à côte avec la liste. Empilé
   sous elle, il n'apporterait qu'une phrase qui invite à cliquer sur ce qui est
   juste au-dessus. */
@media (max-width: 1279px) {
  .annuaire-split:not(.has-detail) .annuaire-detail { display: none; }
}

/* ── DEUX COLONNES ────────────────────────────────────────────────────────── */
@media (min-width: 900px) {
  .annuaire-split {
    grid-template-columns: minmax(0, var(--annuaire-facets-w)) minmax(0, 1fr);
  }

  /* La colonne des filtres reste à l'écran quand la liste défile : on coche,
     on regarde, on recoche. `top` la décolle du haut de la zone qui défile
     d'un cran d'espacement — c'était « la même gouttière que `.wrap` pose
     autour du panneau » jusqu'au 2026-08-16, où cette gouttière a disparu.
     La valeur ne bouge pas pour autant : elle mesure un décollement DANS le
     panneau, pas une compensation de ce qu'il y a autour, et le `max-height`
     en dessous reste exact (le décalage collant en haut, autant d'air en bas).

     ── ET ELLE DÉFILE TOUTE SEULE, SINON LE BOUTON SORT DE L'ÉCRAN ──────────
     Un élément collant plus haut que la fenêtre ne se lit pas jusqu'au bout :
     il se fige, et sa fin reste dessous sans qu'aucun défilement puisse l'y
     ramener. Chez un coordo la colonne porte trois catégories de compétences,
     les champs d'intérêt et deux cases — largement de quoi dépasser un portable
     à 150 % de zoom, et ce qui se perd alors est « Chercher », c'est-à-dire le
     bouton qui valide tout le reste.

     Même remède que `.cnav`, et `dvh` en second pour la même raison : sur iOS
     la barre d'adresse mange `vh`, et la dernière ligne passe dessous. */
  .annuaire-facets {
    position: sticky;
    top: var(--space-5);
    max-height: calc(100vh - 2 * var(--space-5));
    max-height: calc(100dvh - 2 * var(--space-5));
    overflow-y: auto;
  }

  /* Le trait qui sépare deux surfaces, et pas une bordure qui encadre une
     boîte : le volet n'est pas une carte (§4 — la carte encadre, le trait
     sépare). Sur le VOLET et non sur la liste, pour qu'il disparaisse avec lui
     quand on repasse à une colonne.

     Il vit à CE seuil-ci et pas à celui des trois colonnes : ce qui le justifie
     est d'avoir quelque chose à sa gauche, et dès 900 px c'est le cas — la
     liste, quand les filtres se retirent.

     Les filtres, eux, sont bel et bien une carte : c'est une commande, et son
     bord dit où finit ce qu'on règle et où commence ce qu'on lit.

     LE RETRAIT EST PARTI AVEC LA GOUTTIÈRE (Eric, 2026-08-16, « gapless ») :
     il décollait la fiche d'un trait qui était à 32 px d'elle, et les volets se
     touchent maintenant. Ce qui rembourre la fiche est la fiche
     (`.annuaire-detail-body`, `.annuaire-fiche-tete`) ; 32 px de plus du seul
     côté gauche décentraient en prime le volet vide, dont toute la mise en page
     est un texte centré. */
  .annuaire-detail { border-left: 1px solid var(--c-border); }
}

/* CACHER UNE COLONNE NE REDISTRIBUE PAS SA PISTE — et c'est le piège de cette
   largeur-là, trouvé à l'écran à 1 150 px.

   `display: none` retire l'élément de la grille, mais les PISTES restent : la
   liste glisse alors dans la première, celle qui est taillée pour une colonne
   de filtres de 240 px. Les cent six cartes se retrouvaient dans un couloir où
   « Aucun comité · Aucune main levée · Aucun intérêt » tenait sur trois lignes,
   à côté d'un volet qui occupait tout le reste.

   Le gabarit ne peut pas trancher — il ne connaît pas la largeur — donc c'est
   la même règle qui cache et qui redonne les deux pistes. */
@media (min-width: 900px) and (max-width: 1279px) {
  .annuaire-split.has-detail {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  }

  .annuaire-split.has-detail .annuaire-facets { display: none; }
}

/* ── TROIS COLONNES ───────────────────────────────────────────────────────── */
@media (min-width: 1280px) {
  .annuaire-split {
    grid-template-columns: minmax(0, var(--annuaire-facets-w)) minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-6);
  }

  /* La liste est à gauche, à portée de regard : un lien qui « y ramène »
     mènerait à ce qu'on voit déjà. Rendu côté serveur et retiré ici, comme
     l'onglet « Le besoin » des comités et pour la même raison — la largeur de
     l'écran n'est pas une donnée que le serveur connaît. */
  .annuaire-back { display: none; }
}

/* LA DATE DE MISE À JOUR N'A PLUS DE RÈGLE À ELLE, et c'est la correction.

   Elle en avait deux, qui défaisaient chacune un morceau de `.list-row-actions`
   — la classe des rangées qui portent un BOUTON : le `margin-left: auto` qui en
   fait une colonne, le `width: 100%` qui la met sur sa ligne, le `z-index` qui
   la relève au-dessus du lien étiré. Trois propriétés annulées pour un élément
   qui n'est pas ce que la classe décrit.

   Elle est maintenant DANS la ligne des compteurs (voir `person-card`), où elle
   n'a besoin que d'`margin-left: auto` pour garder son bord droit. Le reste des
   deux règles est parti avec.  */

/* ════════════════════════════════════════════════════════════════════════════
   L'ANNUAIRE REPREND LE DESSIN DE `mil-membres` (Eric, 2026-08-13)

   Les trois volets existaient déjà ; ce qui manquait est ce qui les fait LIRE
   comme trois surfaces. La demande est une capture de l'ancienne app, écran
   « Coordonnateur », et elle apporte quatre choses :

     · trois volets ENCADRÉS, chacun coiffé d'une barre qui le nomme ;
     · les filtres éclatés en trois cartes au lieu d'un formulaire d'un bloc ;
     · une grille 7×3 pour demander des disponibilités — le service savait déjà
       les intersecter, il n'y avait simplement rien pour les cocher ;
     · une fiche à vignettes plutôt qu'à listes à puces.

   ── CE QUE ÇA DÉFAIT, ET POURQUOI C'EST ASSUMÉ ──────────────────────────────
   §4 de la charte dit « la carte encadre, le trait sépare », et c'est pour ça
   que le volet de droite n'était qu'un filet. La règle vaut DANS une page ; ici
   les trois colonnes ne sont pas trois parties d'un même texte, ce sont trois
   surfaces qui font trois métiers — on règle à gauche, on choisit au milieu, on
   lit à droite. Le filet ne dit pas ça, la barre de titre si : elle donne un nom
   à chaque colonne et un endroit où poser son compte.

   Les JETONS sont les mêmes des deux côtés — `mil-membres` et la plateforme
   descendent du même `DESIGN_SYSTEM.md`. Rien n'est réglé en dur ici : le
   portage est un changement de dessin, pas de palette, et une coop qui remplace
   `brand.css` repart avec ses couleurs.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── LE VOLET ENCADRÉ, ET SA BARRE DE TITRE ──────────────────────────────────
   `overflow: hidden` fait suivre les coins à ce qui touche le bord : la barre
   en haut, la dernière rangée de la liste en bas. Sans lui, un fond carré
   dépasse d'un rayon de 4 px — invisible tant qu'on ne le cherche pas, et c'est
   ce genre de détail qui fait « vieux » sans qu'on sache dire pourquoi. */
.annuaire-panel {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* L'AIR ENTRE DEUX CARTES DE FILTRES, ET SEULEMENT LÀ.
   La règle valait pour tout `.annuaire-panel` qui en suit un autre — or il y en
   a deux dans le document qui se suivent SANS être empilés : la liste et la
   fiche, qui sont des colonnes côte à côte. La fiche héritait donc de 16 px de
   marge haute, et démarrait plus bas que la liste à toutes les largeurs où les
   deux sont à l'écran. Le sélecteur dit maintenant ce que la règle voulait
   dire : les cartes de la colonne des filtres. */
.annuaire-facets .annuaire-panel + .annuaire-panel { margin-top: var(--space-4); }

.annuaire-panel-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--c-section-bar);
  border-bottom: 1px solid var(--c-border);
}

/* ── LA BARRE DE LA LISTE EST UNE LIGNE DE COMMANDE (Eric, 2026-08-13) ───────
   Elle ne porte plus une étiquette, elle porte les deux réglages de la liste :
   le compte à gauche, le tri à droite, et le bouton des filtres entre eux quand
   la colonne est repliée. `wrap` est le repli du repli : à 375 px les trois ne
   tiennent pas sur une ligne (302 px de contenu pour 259 px utiles), et deux
   rangées de commandes valent mieux qu'un tri sorti de l'écran — c'est encore
   70 px de moins que les quatre rangées d'avant.

   La hauteur minimale est celle du doigt (règle 9 du système de design) : la
   barre porte maintenant des cibles, et le rembourrage seul lui donnait 52 px
   avec le segment mais 36 sans. */
.annuaire-list .annuaire-panel-head {
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  min-height: calc(44px * var(--mil-fs, 1));
}

/* LE CADENAS DE CONFIDENTIALITÉ N'A PLUS DE RÈGLE À LUI, ET C'EST LA FIN DE
   `.annuaire-confid` (Eric, 2026-08-16 : « Now put the privacy pill back in the
   bar »).

   Elle ne disait qu'une chose — `flex: 0 0 auto` — parce que le cadenas vivait
   dans le bandeau de la liste, qui se replie (`flex-wrap`) et l'aurait étiré.
   Il est remonté dans la zone des états de la barre d'écran, où `.apphead-etats`
   règle déjà la question pour tous les écrans qui en portent un ; une classe de
   plus ici serait la divergence que le composant partagé existe pour éviter.

   Rien ne reste : une règle sans gabarit qui la pose est ce qu'on retrouve deux
   mois plus tard en se demandant ce qu'elle tenait. Le raisonnement du cadenas
   lui-même — pourquoi un signe et pas une phrase, pourquoi ce n'est pas un
   survol — vit dans `members/index.blade.php` et dans
   `components/infobulle.blade.php`, qui sont les fichiers qu'on ouvre pour le
   changer. */

/* ── L'INFOBULLE PARTAGÉE ────────────────────────────────────────────────────
   Un signe, et son panneau. Le signe est un vrai bouton : il porte donc la
   cible tactile de 44 px de la règle 9, alors qu'il ne PEINT que 28 px — le
   rembourrage fait le reste, et deux cadenas côte à côte ne se toucheraient
   jamais. C'est le même compromis que les autres commandes rondes de l'app :
   petit à l'œil, large au doigt. */
.infobulle { position: relative; display: inline-flex; }

.infobulle-signe {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(44px * var(--mil-fs, 1));
  height: calc(44px * var(--mil-fs, 1));
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  font-size: var(--mil-fs-1);
  line-height: 1;
  cursor: pointer;
}

.infobulle-signe:hover { background: var(--c-hover-bg); }

/* Le ton se voit sur le SIGNE, pas seulement dans le panneau : un cadenas qui
   annonce une restriction n'est pas une note en passant, et il doit se
   distinguer d'un « ℹ️ » posé à côté d'un champ. Le rond coloré est discret et
   suffit — le mot, lui, est dans le panneau (règle 6 : l'état ne vit pas dans
   une couleur, et ici la couleur ne porte rien toute seule). */
.infobulle-signe.is-attention { background: var(--c-warning-soft); }
.infobulle-signe.is-attention:hover { background: var(--c-warning-soft); filter: brightness(1.08); }

/* LE TON « OK » DIT « TU PEUX LIRE », ET C'EST UN ÉTAT DE QUI REGARDE
   (Eric, 2026-08-14 : « For those who can see the content, the indicator should
   be an opened green lock »).

   Le cadenas OUVERT et vert ne dit pas que la donnée est publique — elle ne
   l'est pas, et le panneau continue de dire qui d'autre peut la lire. Il dit
   que TOI, tu es du bon côté de la porte. C'est pour ça que les deux états
   partagent le même composant et le même texte de marque : ce qui change est
   le lecteur, pas la donnée.

   La couleur ne porte rien toute seule (règle 6) : le cadenas est ouvert ou
   fermé, ce qui se voit en noir et blanc et en daltonien, et le mot est dans le
   panneau. */
.infobulle-signe.is-ok { background: var(--c-success-soft); }
.infobulle-signe.is-ok:hover { background: var(--c-success-soft); filter: brightness(1.08); }

/* LE PANNEAU. `position: fixed` parce qu'il vit dans la couche supérieure : ses
   coordonnées sont celles de la fenêtre, et c'est `infobulle.js` qui les pose.
   Sans le script, `inset` reste au défaut de l'API et il s'ouvre au centre. */
.infobulle-panneau {
  position: fixed;
  inset: auto;
  max-width: min(320px, calc(100vw - 2 * var(--space-4)));
  margin: 0;
  padding: var(--space-4);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  box-shadow: 0 4px 16px var(--c-shadow);
  color: var(--c-text-mid);
  font-size: var(--mil-fs-1);
  line-height: 1.45;
  overflow-wrap: normal;
}

.infobulle-panneau.is-attention { border-color: var(--c-warning); }
.infobulle-panneau.is-ok { border-color: var(--c-success); }

/* LA FLÈCHE — un carré tourné plutôt qu'un triangle en bordures, parce qu'un
   triangle n'a pas de bord : posé sur un panneau qui en a un, il laisse un
   trait qui s'arrête net de chaque côté. Le carré porte les deux mêmes bords
   que le panneau, et on cache les deux autres sous son fond.

   `--infobulle-fleche` est posée par le script : c'est la distance entre le
   bord droit du panneau et le CENTRE du bouton, donc la flèche reste sur sa
   cible même quand le panneau a été borné au bord de la fenêtre. Sans script,
   elle vaut 20 px — le coin, qui est l'endroit où le panneau s'ouvre par
   défaut. */
.infobulle-panneau::after {
  content: '';
  position: absolute;
  right: calc(var(--infobulle-fleche, 20px) - 5px);
  width: 10px;
  height: 10px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  transform: rotate(45deg);
}

.infobulle-panneau.is-attention::after { border-color: var(--c-warning); }
.infobulle-panneau.is-ok::after { border-color: var(--c-success); }

.infobulle-panneau[data-sens="bas"]::after {
  top: -6px;
  border-right: 0;
  border-bottom: 0;
}

.infobulle-panneau[data-sens="haut"]::after {
  bottom: -6px;
  border-left: 0;
  border-top: 0;
}

/* L'OUVERTURE SE VOIT, ET ELLE DURE 120 ms. Un panneau qui apparaît d'un coup
   se lit comme un défaut d'affichage — c'est la moitié de ce que « modern
   looking » veut dire. `@starting-style` donne l'état de DÉPART d'un élément
   qui vient d'entrer dans le rendu, et `allow-discrete` fait attendre
   `display` et `overlay` la fin de la transition : sans eux, le panneau
   disparaît à l'instant zéro de la sortie et l'animation ne se voit qu'à
   l'ouverture.

   Ni l'un ni l'autre n'est requis pour que ça marche : un navigateur qui les
   ignore ouvre et ferme sec. */
.infobulle-panneau {
  opacity: 0;
  transform: translateY(-4px);
  transition:
    opacity 120ms ease,
    transform 120ms ease,
    overlay 120ms ease allow-discrete,
    display 120ms ease allow-discrete;
}

.infobulle-panneau:popover-open {
  opacity: 1;
  transform: none;
}

@starting-style {
  .infobulle-panneau:popover-open {
    opacity: 0;
    transform: translateY(-4px);
  }
}

/* Le mouvement est une préférence système, pas une décision de dessin. */
@media (prefers-reduced-motion: reduce) {
  .infobulle-panneau { transition: none; transform: none; }
  .infobulle-panneau:popover-open { transform: none; }
}

.infobulle-titre {
  display: block;
  margin-bottom: var(--space-1);
  color: var(--c-text);
}

.infobulle-panneau.is-attention .infobulle-titre { color: var(--c-warning-text); }
.infobulle-panneau.is-ok .infobulle-titre { color: var(--c-success-text); }

.infobulle-lien { display: inline-block; margin-top: var(--space-2); }

/* Le repli, et il est la raison pour laquelle ce composant est acceptable sous
   la §3 : un navigateur qui ne connaît pas `popover` ignore l'attribut, donc le
   panneau est un `<span>` ordinaire — visible, à la suite du bouton. On y
   défait ce que le placement suppose, sinon il resterait posé en `fixed` dans
   un coin de la fenêtre, sans que rien ne l'ouvre ni ne le ferme. */
@supports not selector(:popover-open) {
  .infobulle-panneau {
    position: static;
    max-width: none;
    box-shadow: none;

    /* CE DEUX-LÀ SONT LE REPLI LUI-MÊME. L'état fermé du panneau est
       `opacity: 0` (voir la transition d'ouverture), et il n'est invisible
       sans conséquence que parce que l'API lui met `display: none` par-dessus.
       Là où l'API n'existe pas, rien ne le pose : le panneau serait présent,
       occupant sa place, et transparent — c'est-à-dire la seule façon de
       perdre pour de bon une mention que la charte exige. */
    opacity: 1;
    transform: none;
  }

  .infobulle-panneau::after { display: none; }
}

/* ── UN TITRE QUI PORTE SON CADENAS AU BOUT ──────────────────────────────────
   Le coin, pour les écrans qui n'ont NI barre d'écran ni en-tête de comité :
   le CA, la fiche, un registre, les appréciations. Ces quatre-là n'ont qu'un
   `h1` en haut d'un panneau, et le cadenas se pose au bout de sa ligne — c'est
   le seul endroit qui existe déjà à toutes les largeurs et qui ne coûte pas une
   rangée de plus. Les écrans qui ont une barre mettent le leur dans sa zone
   d'états, qui est faite pour ça ; ceux d'un comité, sur la ligne des onglets.

   Le repli à 700 px annule le `margin-left` et remet le cadenas sous le titre,
   à GAUCHE — la même décision que `.committee-head-actions`, prise pour la même
   raison : un signe seul contre le bord opposé au titre qu'il qualifie se lit
   comme la marque d'autre chose. */
.titre-ligne {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
}

.titre-ligne > * { min-width: 0; }
.titre-ligne > h1 { margin-bottom: 0; }
.titre-ligne > .infobulle { margin-left: auto; }

@media (max-width: 700px) {
  .titre-ligne > .infobulle { margin-left: 0; }
}

/* Le titre du volet est une ÉTIQUETTE, pas un titre de page : petit, en
   capitales, gris. Il nomme la colonne pour qu'on la retrouve du coin de l'œil,
   et il ne doit pas disputer la lecture au contenu — c'est le contenu qu'on est
   venu lire. Il reste un `h2` pour le plan du document : un lecteur d'écran
   annonce toujours trois sections nommées. */
.annuaire-panel-head h2 {
  margin: 0;
  font-size: var(--mil-fs-0);
  font-weight: 500;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--c-text-mid);
}

.annuaire-panel-icon { font-size: var(--mil-fs-2); line-height: 1; }

/* LE COMPTE OUVRE LA BARRE, et il n'est plus gris (2026-08-13).

   Il était poussé au bout par `margin-left: auto`, en petit et en gris, derrière
   une étiquette « Liste des membres filtrés » qui, elle, avait la place. Les
   deux ont échangé : l'étiquette est passée au lecteur d'écran, et le nombre —
   la seule chose de la barre qui RÉPONDE à ce qu'on vient de cocher à gauche —
   a pris la première place et le ton du texte. */
.annuaire-count {
  font-size: var(--mil-fs-1);
  font-weight: 500;
  color: var(--c-text);
}

.annuaire-panel-body { padding: var(--space-4); }

/* ════════════════════════════════════════════════════════════════════════════
   LES FILTRES SONT DES PASTILLES (Eric, 2026-08-14)

   « Les filtres devraient être super simples ; juste des grosses pastilles that
   toggle on/off, pas de header. »

   Ce qui part : trois barres de titre dans une colonne qui ne contient QUE des
   filtres, un `<select>` au milieu de deux blocs qu'on clique, et des cases à
   cocher de 13 px qu'on vise avec un doigt.

   Ce qui arrive : un objet unique, la pastille — un `<label>` qui habille sa
   case. La case reste dans le document (`visually-hidden`, jamais
   `display: none`) : le clavier la trouve, Espace la coche, un lecteur d'écran
   annonce « …, case à cocher, cochée ». `:checked + span` fait le reste pour
   l'œil, et sans feuille de style il reste une liste de cases nommées.

   Deux formes, une seule règle : en PILE pour les intérêts (des phrases, une
   par ligne, comme la maquette les dessine) et en RANGÉE pour les comités (des
   noms courts qui se rangent à deux ou trois par ligne).
   ════════════════════════════════════════════════════════════════════════════ */

/* La carte sans sa barre : le trait qui la sépare de la voisine reste, le
   rembourrage aussi. Ce qui disparaît est l'étiquette qui nommait ce que son
   contenu dit déjà. */
.annuaire-panel-nu { padding: var(--space-3); }

.pastilles-pile {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.pastilles-rangee {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.pastilles-rangee + .pastilles-rangee { margin-top: var(--space-2); }

/* LES COMITÉS DÉFILENT DANS LEUR BOÎTE. Chez MIL il y en a vingt-deux : en
   pastilles dans une colonne de 240 px, ça fait un mur de onze cents pixels
   qu'il faut traverser pour atteindre la grille des disponibilités en dessous.
   La borne est celle des intérêts (`.annuaire-filter-list`), pour la même
   raison et avec le même remède — le reste à la molette.

   La HAUTEUR est fixe, le nombre de rangées ne l'est pas : depuis que la
   pastille de comité a rétréci (voir `.pastille-compacte`), il en tient cinq
   dans la même boîte à la souris, et quatre au téléphone où elle reprend ses
   44 px. C'est le bon sens de la borne — elle plafonne le mur, elle ne compte
   pas les briques. */
.pastilles-rangee.est-longue {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  max-height: calc(216px * var(--mil-fs, 1));
  overflow-y: auto;
  padding-right: var(--space-1);
}

/* LA PASTILLE. 44 px de haut : c'est une cible tactile (règle 9), et c'est
   aussi ce que « grosse » veut dire dans la demande. Le curseur en main, le
   trait en bordure — pas de fond tant qu'elle est éteinte, sinon une colonne de
   quinze pastilles devient un mur. */
.pastille {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--mil-fs-1);
  line-height: 1.3;
  cursor: pointer;
}

.pastille-large { border-radius: var(--radius); }

/* LES COMITÉS SONT PLUS PETITES QUE LES AUTRES (Eric, 2026-08-14).

   Elles ne sont pas du même genre que les intérêts : chez MIL il y en a
   VINGT-DEUX, et vingt-deux cibles de 44 px dans une colonne de 240 px font un
   mur qu'on fait défiler au lieu de le lire. La question qu'elles posent — « à
   quel comité ? » — se répond en balayant une liste de noms, pas en visant. Les
   intérêts, eux, restent gros : ce sont des phrases, une par ligne.

   ⚠️ LA RÈGLE DES 44 px N'EST PAS ANNULÉE, ELLE EST LUE COMME ELLE EST ÉCRITE :
   « phone-first sur les pages membre : cible tactile ≥ 44 px »
   (`DESIGN_SYSTEM.md`, §9). Au téléphone la pastille reprend donc sa taille
   entière, juste en dessous — et c'est là que la colonne devient un volet
   pleine largeur, où la place ne manque plus. Ce qui rétrécit ne rétrécit qu'à
   la souris. */
.pastille-compacte {
  min-height: 32px;
  padding: var(--space-1) var(--space-3);
  font-size: var(--mil-fs-0);
}

@media (max-width: 700px) {
  .pastille-compacte {
    min-height: 44px;
    padding: var(--space-2) var(--space-4);
    font-size: var(--mil-fs-1);
  }
}

.pastille:hover { background: var(--c-hover-bg); }

/* ALLUMÉE : le fond d'accent ET le trait. Deux signaux, parce qu'un état ne vit
   pas dans une couleur (règle 6) — en fort contraste et en daltonien, c'est le
   bord épaissi qui reste lisible. */
.pastille:has(input:checked) {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 500;
}

/* Le focus est celui de la CASE, mais il se dessine sur la pastille : l'input
   est visuellement caché, donc son anneau le serait aussi — et une cible qu'on
   atteint au clavier sans voir où l'on est n'est pas atteignable. */
.pastille:has(input:focus-visible) {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* La réinitialisation dans la barre : une commande comme les autres, en plus
   discret — c'est un geste de sortie, pas un réglage.

   DEPUIS QU'ELLE EST UN DESSIN (2026-08-14), elle est CARRÉE et non plus large :
   `min-width` rejoint le `min-height: 44px` de `.annuaire-commande`, sans quoi
   la cible ferait 18 px de large pour 44 de haut — la règle 9 se lit dans les
   deux sens. Le dessin, lui, tient dans 18 : c'est la taille du texte qu'il
   remplace, pas celle de sa boîte. */
.annuaire-reset {
  justify-content: center;
  min-width: 44px;
  color: var(--c-text-muted);
}

.annuaire-reset svg { width: 18px; height: 18px; }

.annuaire-reset:hover { color: var(--c-text); }

/* CE QUI ÉTAIT ICI, ET POURQUOI CE N'EST PLUS LÀ.

   « Tout effacer » n'apparaît qu'au premier filtre allumé, et ses ~85 px de
   texte faisaient alors passer la barre de une ligne à deux — sous le doigt de
   qui venait de cliquer, ce qui est le pire moment pour qu'un écran bouge. Ce
   qui descendait était le cadenas, la seule chose de la barre qui doive rester
   au même endroit. Le champ de recherche cédait donc 30 px de base tant que la
   commande était là.

   La commande fait maintenant 44 px et non 85, et le saut de ligne qu'elle
   provoquait n'existe plus : la compensation est partie avec sa cause. Le
   champ garde sa base pleine, ce qui est ce qu'on voulait au départ.

   Gardé en toutes lettres parce que le symptôme reviendra si un jour cette
   barre gagne un objet : c'est la RECHERCHE qui cède, jamais le cadenas. */

/* Les deux raccourcis de la grille, en texte : ils font en un geste ce qui en
   demande sept, et ils n'ont pas à peser autant que ce qu'ils règlent. */
.annuaire-filter-raccourcis {
  display: flex;
  gap: var(--space-4);
  margin: var(--space-2) 0 0;
  font-size: var(--mil-fs-0);
}

/* ── LES FILTRES ─────────────────────────────────────────────────────────────
   La liste des intérêts défile dans sa propre boîte. Sans borne, dix intérêts
   poussent la grille des disponibilités hors de l'écran, et le filtre qu'on
   voulait atteindre est celui qu'on ne voit plus.

   La hauteur n'est PAS celle de l'ancienne app (220 px pour des rangées de
   25) : `.check` porte ici une cible tactile de 44 px, qui est une décision
   d'accessibilité de cette charte-ci et pas un accident. Six rangées visibles
   plutôt que huit, et on garde le doigt. */
.annuaire-filter-list {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  max-height: calc(264px * var(--mil-fs, 1));
  overflow-y: auto;
  padding-right: var(--space-1);
}

.annuaire-filter-hint {
  margin: var(--space-2) 0 0;
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
}

/* Deux boutons côte à côte tant qu'ils tiennent, l'un sous l'autre sinon.
   `flex: 1` seul les serrait à 90 px dans une colonne de 240, et
   « Réinitialiser tout » s'y coupait en « Réinitiali- / ser tout » — un mot
   césuré au milieu d'un bouton se lit comme un défaut d'affichage. La base de
   `flex` est donc la largeur du texte : ils passent à la ligne au lieu de le
   couper. */
.annuaire-filter-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.annuaire-filter-actions > * { flex: 1 1 auto; }

/* Les raccourcis sont des LIENS et pas des boutons : ils ne font rien de plus
   que changer la query string — « les mêmes filtres, sans les plages » pour
   Effacer, « … plus les sept soirs » pour Soirées. Un bouton promettrait une
   action ; ceux-ci mènent quelque part, et le clic milieu ouvre un onglet.
   Dessinés en bouton parce qu'ils se pressent, comme `.btn` ailleurs. */
.annuaire-filter-btn {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  color: var(--c-text-mid);
  font-size: var(--mil-fs-1);
  font-weight: 500;
  text-align: center;
  text-decoration: none;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}

.annuaire-filter-btn:hover {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-accent);
}

.annuaire-section-label {
  margin-bottom: var(--space-2);
  font-size: var(--mil-fs-0);
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--c-text-muted);
}

/* ── LA GRILLE DES DISPONIBILITÉS, SANS UNE LIGNE DE JAVASCRIPT ──────────────
   Vingt-et-une cases à cocher, une par jour et par période. Ce qu'on voit est
   le `<span>` qui suit la case ; la case elle-même est masquée à l'œil mais
   PAS au clavier ni au lecteur d'écran (`clip`, jamais `display: none`), donc
   la grille se parcourt en tabulation et s'annonce « Mardi soir, case à
   cocher ». C'est la règle 4 de la charte : rien n'attend un script.

   Chaque case vaut UNE heure-repère — 9 h, 13 h, 18 h — et non toute la
   période. C'est la convention de `mil-membres`, et elle tient parce que le
   profil enregistre l'heure : qui est là le matin a 9 h. Le format `jour:heure`
   de l'URL ne bouge donc pas, et les signets d'avant marchent encore. */
.annuaire-minical {
  display: grid;
  grid-template-columns: calc(44px * var(--mil-fs, 1)) repeat(7, minmax(0, 1fr));
  gap: var(--space-1);
  font-size: var(--mil-fs-0);
}

.annuaire-minical-head,
.annuaire-minical-period {
  color: var(--c-text-muted);
  font-weight: 500;
}

.annuaire-minical-head { text-align: center; padding: 2px 0; }

.annuaire-minical-period {
  align-self: center;
  padding-right: var(--space-2);
  text-align: right;
  color: var(--c-text-mid);
}

.annuaire-minical-cell { display: block; cursor: pointer; }

/* La case est retirée du FLUX mais reste dans l'ordre de tabulation et dans
   l'arbre d'accessibilité. `appearance: none` seul laisserait un carré natif
   dessiné par le système par-dessus la cellule. */
.annuaire-minical-cell input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.annuaire-minical-cell span {
  display: block;
  height: calc(26px * var(--mil-fs, 1));
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  transition: background var(--t-fast), border-color var(--t-fast);
}

.annuaire-minical-cell:hover span {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}

.annuaire-minical-cell input:checked + span {
  background: var(--c-accent);
  border-color: var(--c-accent);
}

/* Le focus se voit SUR la cellule : c'est elle qu'on regarde, la case est
   invisible et son contour ne serait nulle part. */
.annuaire-minical-cell input:focus-visible + span {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* ════════════════════════════════════════════════════════════════════════════
   LA BARRE UNIQUE DE LA LISTE (Eric, 2026-08-14, maquette)

   « [ Chercher un membre × ]  106      Filtres | Prénom | Nom   (🔓) »

   Tout ce qui commande la liste sur UNE ligne. L'histoire de cette barre en un
   jour dit à elle seule ce qu'on a appris : quatre rangées (barre de titre,
   phrase de compte, recherche, tri) → deux → une. 203 px de chrome au départ,
   ~56 aujourd'hui, sur la colonne où la hauteur est la seule ressource rare.

   L'ordre est celui du geste : on cherche (à gauche, le champ), on regarde
   combien il en reste (le nombre, juste à côté), et on règle ce qui reste à
   régler (à droite, poussé par `auto`). Le cadenas ferme la ligne : il n'est
   pas une commande, il dit ce que l'écran laisse lire.
   ════════════════════════════════════════════════════════════════════════════ */

/* LE CHAMP DE RECHERCHE OUVRE LA BARRE. `flex: 1 1 …` avec une base et un
   plafond : il prend la place disponible, cède quand les commandes en ont
   besoin, et ne s'étire jamais au point de faire voyager l'œil du champ au
   nombre. */
/* LA BASE EST PETITE EXPRÈS — 140 px, alors que le champ en fait 200 à
   l'écran. Dans un conteneur qui se replie, le navigateur assigne les LIGNES
   sur les tailles de base, et ne rétrécit qu'ensuite : une base de 200 px
   suffisait à faire descendre le cadenas d'une ligne dans un volet de 500 px,
   alors qu'il y tenait très bien une fois tout le monde serré. Le champ
   REPREND la place ensuite par `flex-grow`, jusqu'à son plafond. */
.annuaire-recherche {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 calc(140px * var(--mil-fs, 1));
  max-width: calc(340px * var(--mil-fs, 1));
  min-width: calc(120px * var(--mil-fs, 1));
}

/* La place du bouton d'effacement est RÉSERVÉE dans le champ, sinon le dernier
   caractère tapé passe dessous. */
.annuaire-recherche input {
  width: 100%;
  min-width: 0;
  padding-right: calc(36px * var(--mil-fs, 1));
}

.annuaire-search-clear {
  position: absolute;
  top: 50%;
  right: var(--space-2);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  color: var(--c-text-muted);
  text-decoration: none;
  line-height: 1;
}

.annuaire-search-clear:hover { background: var(--c-hover-bg); color: var(--c-text); }

/* `[hidden]` DOIT GAGNER, ET IL FAUT L'ÉCRIRE — le `display: inline-flex` du
   dessus pèse plus lourd que la feuille de l'agent utilisateur. C'est la règle
   de `.flash[hidden]` et de `.list-row[hidden]` ; ici la croix s'affichait sur
   un champ VIDE — repérée sur `/invitations`, qui emprunte ces classes, mais
   le défaut était d'abord celui de l'annuaire. */
.annuaire-search-clear[hidden] { display: none; }

/* LES COMMANDES, POUSSÉES AU BOUT ET SÉPARÉES D'UNE BARRE VERTICALE.

   Le séparateur est posé par le CSS entre deux items, jamais écrit dans le
   gabarit : « Filtres » n'existe pas sans script, et un séparateur écrit
   resterait devant « Prénom » comme une ponctuation orpheline. Sur le second
   item ET sur le groupe du tri, parce que celui-ci est un `role="group"` : ce
   sont deux frères, pas trois. */
.annuaire-commandes {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 0 auto;
  padding: 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* `margin: 0` n'est pas de la précaution : le groupe du tri est un `<p>`, et la
   marge par défaut d'un paragraphe (1 em en haut et en bas) faisait 70 px d'une
   ligne de 44 — invisible à l'œil, puisque tout reste centré, et 26 px de liste
   perdus à chaque écran. */
.annuaire-commandes-tri {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

.annuaire-commandes > * + *::before,
.annuaire-commandes-tri > * + *::before {
  content: '|';
  margin-right: var(--space-2);
  color: var(--c-border-strong);
}

/* UNE COMMANDE DE BARRE : du texte, et une cible de 44 px autour (règle 9). Le
   segment encadré qu'elle remplace était l'objet le plus contrasté d'une ligne
   qui porte aussi un nombre et un champ — deux options qu'on choisit une fois
   par session n'ont pas à peser plus lourd que ce qu'on lit cent fois. */
.annuaire-commande {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-1);
  border: 0;
  background: transparent;
  color: var(--c-text-mid);
  font: inherit;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}

.annuaire-commande:hover { color: var(--c-text); }

/* L'ORDRE EN COURS est un ÉTAT, pas une couleur (règle 6) : `aria-pressed` le
   porte pour l'oreille, le poids et l'encre pour l'œil. Deux signaux, dont un
   qui survit au daltonisme et à la feuille de style coupée. */
.annuaire-commande[aria-pressed="true"] {
  color: var(--c-text);
  font-weight: 600;
}

/* La liste défile dans le volet, pas dans la page : la barre de titre et la
   recherche restent en place pendant qu'on parcourt cent six personnes. */
.annuaire-rows {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 70vh;
  overflow-y: auto;
}

/* ── UNE PERSONNE DANS LA LISTE ──────────────────────────────────────────────
   La rangée redevient pleine largeur, sans le padding de `.card` : le filet du
   bas est ce qui sépare deux personnes, et le volet ce qui les contient. */
/* Elle garde `.row-card` pour le lien étiré (`.row-card-title::after`) et pour
   `cartes.js`, mais elle n'en garde pas la BOÎTE : dans un volet encadré, une
   carte par personne ferait un cadre dans un cadre, et cent six coins arrondis
   les uns sous les autres. Le filet du bas sépare, le volet contient. */
/* ⚠️ LE SÉLECTEUR PASSE PAR `.annuaire-rows`, ET CE N'EST PAS DÉCORATIF (Eric,
   2026-08-16 : « the UI could also be gapless and tighter overall »).
   La rangée porte AUSSI `.row-card` — pour le lien étiré et pour `cartes.js` —
   et `.row-card` s'écrit trois mille lignes plus bas : à poids égal (0,1,0)
   c'est la dernière qui gagne. Tout ce que ce bloc-ci défaisait était donc
   remis derrière son dos, et cent six personnes se lisaient en cent six cartes
   encadrées, arrondies, séparées de 8 px — exactement le « cadre dans un
   cadre » que le commentaire au-dessus dit d'éviter, et la moitié de l'air que
   la liste dépensait. Deux classes (0,2,0), et l'intention écrite ici gagne.

   `box-shadow: none` avec : `.row-card.is-open` pose un trait à gauche en ombre
   intérieure, et le nôtre est un `::before` — les deux ensemble faisaient un
   double filet de 6 px sur la personne ouverte. */
.annuaire-rows .annuaire-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* LE MÊME RETRAIT QUE LA BARRE AU-DESSUS (`.annuaire-panel-head`), et c'est
     ce qui aligne l'avatar sur le champ de recherche — la colonne du volet se
     lit d'un seul bord de haut en bas. Serré à 12 px, il manquait aussi les
     3 px que la personne OUVERTE prend à gauche pour son trait : sa vignette
     s'y appuyait presque. La hauteur, elle, reste serrée. */
  padding: var(--space-2) var(--space-4);
  margin: 0;
  border: 0;
  border-bottom: 1px solid var(--c-border);
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

/* ⚠️ ET IL EN FAUT UNE DEUXIÈME POUR LA GOUTTIÈRE (Eric, 2026-08-16 : « remove
   the top margin »). Le `margin: 0` du bloc au-dessus pèse (0,2,0) — et
   `.row-card + .row-card` aussi. À poids égal, la dernière écrite gagne, et
   c'est elle : les cent six rangées gardaient donc leurs 8 px d'écart, sur une
   liste dont tout le reste venait d'être recollé. Monter en poids ne suffit
   pas ici, il faut la MÊME forme — un sélecteur de voisin, un cran plus lourd
   (0,3,0). C'est le seul endroit de la refonte où la règle à défaire n'est pas
   sur l'élément mais sur la paire. */
.annuaire-rows .annuaire-row + .annuaire-row { margin-top: 0; }

.annuaire-rows .annuaire-row:last-child { border-bottom: none; }

/* LA DATE FINIT LA LIGNE DES COMPTEURS, poussée au bout par `auto`. C'est tout
   ce qu'il lui faut : elle partage la largeur de la ligne au lieu de la prendre
   à la colonne d'à côté, et son bord droit — sa raison d'être, « qui n'a jamais
   rempli son profil » se balayant sur une colonne alignée — vient de là.

   `text-align: right` en plus de la poussée, pour le jour où elle se replie sur
   deux lignes : une date alignée à gauche sous une ligne alignée à droite se
   lit comme une autre donnée. */
.annuaire-stamp {
  margin-left: auto;
  padding-left: var(--space-2);
  text-align: right;
}

/* `:focus-within` avec le survol : c'est ce que `.row-card` faisait et que la
   montée en poids ci-dessus lui reprend — sans lui, la tabulation traverserait
   cent six personnes sans qu'on voie où l'on est. */
.annuaire-rows .annuaire-row:hover,
.annuaire-rows .annuaire-row:focus-within { background: var(--c-hover-bg); }

/* La carte ouverte porte un fond ET un trait à gauche — deux signaux, parce
   qu'un fond seul est une couleur, et qu'un état ne vit pas dans une couleur
   (règle 6). Le trait tient encore en daltonien et en fort contraste.

   ── LES DEUX LIGNES QUI SUIVENT REPRENNENT CE QUE `.row-card.is-open` POSAIT
      DERRIÈRE (Eric, 2026-08-16 : « fix selected membre card padding and
      active class ») ────────────────────────────────────────────────────────
   Le bloc au-dessus vaut (0,2,0) et `.row-card.is-open` aussi, mais il s'écrit
   trois mille lignes plus bas : à poids égal, c'est lui qui gagnait sur la
   personne OUVERTE, et sur elle seule. Deux effets, tous les deux faux ici :

   · son `border-color: var(--c-accent)` repeignait en bleu le filet du BAS de
     la rangée — le seul bord qu'elle ait gardé. La personne ouverte se
     retrouvait soulignée d'un trait d'accent pleine largeur, qui se lit comme
     une séparation de groupe et non comme un état ;
   · son `box-shadow: inset 3px` redessinait, en ombre, le trait de gauche que
     le `::before` ci-dessous dessine déjà. Invisible au pixel près puisque les
     deux se superposent — et c'est bien le problème : deux mécaniques pour un
     trait, dont l'une n'appartient pas à cet écran.

   Ce que l'état dit reste ce que ce bloc-ci écrit : un fond pâle et un trait à
   gauche. Le filet du bas redevient celui de toutes les autres. */
.annuaire-rows .annuaire-row.is-open {
  background: var(--c-accent-soft);
  border-color: var(--c-border);
  box-shadow: none;
}

.annuaire-rows .annuaire-row.is-open::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--c-accent);
}

.annuaire-avatar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(36px * var(--mil-fs, 1));
  height: calc(36px * var(--mil-fs, 1));
  border-radius: var(--radius);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-1);
  font-weight: 500;
}

/* LE BLOC D'INFORMATION EST UNE GRILLE À DEUX ÉTAGES, ET C'EST CE QUI TIENT LA
   COLONNE DES DATES.

   Le nom prend l'étage du haut, entier. En dessous, deux colonnes : les
   compteurs, qui se replient sous eux-mêmes (`minmax(0, 1fr)` — le `0` est ce
   qui les autorise à devenir plus étroits que leur contenu), et la date, à sa
   largeur.

   ── POURQUOI UNE GRILLE ET PAS UN `flex-wrap` ───────────────────────────────
   Essayé, et faux d'une façon qui ne se voit que sur certaines rangées : dans
   un conteneur flexible, le REPLI passe avant le rétrécissement. Les compteurs
   les plus longs poussaient donc la date à la ligne suivante au lieu de se
   serrer, et la colonne des dates — sa raison d'être — ondulait d'une rangée à
   l'autre selon la longueur du texte d'à côté. Une grille ne peut pas faire ça :
   ses pistes sont posées avant le contenu.

   `align-items: baseline` fait le dernier morceau : sur une rangée dont les
   compteurs tiennent deux lignes, la date reste posée sur la PREMIÈRE, à la
   même hauteur que chez la voisine. */
.annuaire-row-info {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: var(--space-2);
}

.annuaire-part { min-width: 0; }

.annuaire-row-name {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: var(--mil-fs-2);
  font-weight: 500;
}

/* Le numéro d'appartement est une ÉTIQUETTE et pas une suite du nom : c'est le
   second repère pour reconnaître quelqu'un dans une coop de cent six, et il se
   balaie mieux détaché. */
.annuaire-apt {
  padding: 2px var(--space-2);
  border-radius: var(--radius);
  background: var(--c-hover-bg);
  font-size: var(--mil-fs-0);
  font-weight: 500;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--c-text-mid);
}

.annuaire-part {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-1);
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
}

/* Le point médian est POSÉ PAR LE CSS entre deux compteurs, et pas écrit dans
   le gabarit : un séparateur qui reste quand le voisin disparaît est la façon
   la plus courante de finir une ligne par « · ». */
.annuaire-stat + .annuaire-stat::before {
  content: '·';
  margin-right: var(--space-2);
  color: var(--c-border-strong);
}

.annuaire-stat.is-none { color: var(--c-text-muted); }

/* ── LA RANGÉE AU TÉLÉPHONE : CE QUI RESTE, ET CE QUI PART ───────────────────
   Eric, 2026-08-13 : « fix the mobile row height ». La rangée faisait 208 px à
   375 px de large — deux personnes par écran sur cent six. La moitié du mal
   était structurelle (la date en troisième colonne, corrigée plus haut) ; le
   reste est du CONTENU qui ne tient pas dans 211 px utiles, et qui se replie
   en quatre lignes de gris.

   Deux retraits, et ce sont deux JUGEMENTS — pas des ajustements de largeur :

   1. LES COMPTEURS D'APPOINT QUAND ILS VALENT ZÉRO. « Aucune main levée ·
      Aucun intérêt » se répète sur les quatre-vingt-douze rangées de gens qui
      n'ont rien rempli : c'est la même phrase partout, donc elle ne distingue
      personne, et elle coûte deux lignes à chacune. Le compte des COMITÉS
      reste, à zéro comme au reste : « Aucun comité » désigne quelqu'un de
      disponible, et c'est la question du coordo qui recrute.

   2. LA DATE DE MISE À JOUR. Ce n'est pas ce qu'on vient faire ici au doigt —
      on cherche « Aline du 302 », on l'ouvre, on l'appelle. Balayer la
      fraîcheur des profils est un travail de coordo à l'écran large, où la
      colonne alignée existe. Et elle n'est pas perdue : la fiche la porte en
      pied, à une tape d'ici.

   Ce qui reste, à 375 px : le visage, le nom, l'appartement, et les faits qui
   ne valent pas zéro. 208 px → 66. */
@media (max-width: 560px) {
  .annuaire-stat.is-appoint.is-none { display: none; }
  .annuaire-row .annuaire-stamp { display: none; }
}

/* ── LA FICHE : LA TÊTE QUI NE PART PAS ──────────────────────────────────────
   Le retour, la carte de visite et les comités : qui c'est, en un bloc. Elle
   est COLLANTE dans la zone qui défile, et elle remplace à ce poste la barre
   grise « Détails du membre » — qui tenait la même place en ne disant rien que
   la fiche ne dise mieux (Eric, 2026-08-13, maquette « Membre — Real estate »).

   Le fond est explicite, et ce n'est pas de la décoration : un élément collant
   sans fond laisse passer sous lui les vignettes qui remontent, et le nom
   devient illisible au premier défilement. `z-index` pour la même raison,
   l'ordre du document ne suffisant pas dès qu'un enfant se peint sur sa propre
   couche.

   Elle est bornée à la moitié de la hauteur : une fiche ouverte sur un
   téléphone à 375 px a un en-tête de ~180 px, et le laisser coller y garderait
   quatre lignes de contenu visibles. Passé la borne, elle se contente de
   défiler avec le reste — ce qui est le bon comportement quand elle EST le
   reste. */
.annuaire-fiche-tete {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
}

@media (max-height: 640px) {
  .annuaire-fiche-tete { position: static; }
}

/* ── LE CADENAS DU VOLET DE LA FICHE ─────────────────────────────────────────
   Il n'existe QUE sous 900 px, et c'est tout ce qu'il fait de différent : à
   cette largeur-là, une fiche ouverte fait disparaître le volet de la liste, et
   le cadenas de la barre s'en va avec elle. Au-dessus, les deux volets sont à
   l'écran ensemble et un second cadenas serait un doublon — celui de la liste
   suffit.

   Même mécanique que `.annuaire-back` juste en dessous : le serveur rend les
   deux, la feuille en montre un. Une requête média sait quelle largeur il fait ;
   un gabarit, non.

   La rangée est mince et poussée à droite : elle n'annonce rien, elle POSE un
   signe dans le coin — c'est ce qu'Eric a demandé le 2026-08-13, et le coin
   d'un volet qui prend tout l'écran est celui-là. */
.annuaire-fiche-confid {
  display: flex;
  justify-content: flex-end;
  margin: 0;
  padding: var(--space-1) var(--space-3) 0;
}

@media (min-width: 900px) {
  .annuaire-fiche-confid { display: none; }
}

/* Le retour est dans la tête collante : c'est le seul chemin vers la liste sous
   1 280 px, et il ne se paie une rangée qu'à cette largeur-là. Sa `display` est
   laissée à la règle qui le retire au-delà — deux propriétés différentes ne se
   disputent pas, et la sienne est plus haut dans le fichier. */
.annuaire-back {
  margin: 0;
  padding: var(--space-3) var(--space-5) 0;
}

/* ── LA FICHE : L'EN-TÊTE ────────────────────────────────────────────────────
   Le visage, le nom, la façon de joindre, et deux compteurs. C'est la carte de
   visite : ce qu'un coordo lit avant de décider s'il continue. */
.annuaire-hero {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  border-bottom: 1px solid var(--c-border);
}

.annuaire-hero-avatar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(56px * var(--mil-fs, 1));
  height: calc(56px * var(--mil-fs, 1));
  border-radius: var(--radius);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-4);
  font-weight: 500;
}

.annuaire-hero-id { min-width: 0; }

/* Le nom d'une personne n'est pas une étiquette de section : il défait les
   capitales et le gris que `h2` pose à tout l'app (§3 du système de design).
   Il reste un `h2` parce qu'il EST le titre de ce volet — c'est le plan du
   document, pas le dessin, qui décide du niveau. */
.annuaire-hero-name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--mil-fs-4);
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--c-text);
}

.annuaire-hero-meta {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-muted);
}

.annuaire-hero-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.annuaire-dp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
}

.annuaire-dp strong { color: var(--c-text); font-weight: 600; }

/* Le geste est poussé au bout : on lit à gauche, on agit à droite, comme sur
   toutes les rangées de l'app. `.btn` est dessiné pour un `<button>` : posé sur
   un `<a>` il garde le soulignement des liens, qui barre le mot au milieu du
   bouton. */
.annuaire-hero-actions { margin: 0 0 0 auto; flex-shrink: 0; }

/* LE CADENAS DE SA PROPRE FICHE prend le même coin que le bouton « Écrire »,
   et jamais en même temps que lui : l'un ne s'affiche que sur la fiche de
   quelqu'un d'autre, l'autre que sur la sienne. Pas de repli à 700 px ici,
   contrairement à `.titre-ligne` — ce volet est déjà étroit à toutes les
   largeurs, et sa rangée ne se replie pas. */
.annuaire-hero > .infobulle { margin-left: auto; }

.annuaire-hero-actions .btn {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  text-decoration: none;
}

/* ── LA CARTE DE VISITE AU TÉLÉPHONE ─────────────────────────────────────────
   Le bandeau est une rangée : le visage, l'identité, et le bouton poussé au
   bout. À 293 px de large — la fiche ouverte sur un téléphone — les trois ne
   tiennent pas, et « ne pas tenir » ne veut pas dire déborder : le nom se
   coupait en « Lo-rena Ma-ram-bio », le courriel en quatre morceaux, et les
   deux pastilles devenaient des rubans verticaux d'un mot par ligne.

   Le bouton passe donc SOUS l'identité et prend la ligne. C'est le seul des
   trois qui peut descendre sans rien perdre : le visage sert de repère à côté
   du nom, et l'identité est ce qu'on est venu lire.

   Le rembourrage passe de 24 à 16 px pour la même raison — 48 px de marge sur
   293 de large, c'est un sixième de l'écran donné au vide. */
@media (max-width: 560px) {
  .annuaire-hero {
    flex-wrap: wrap;
    padding: var(--space-4);
  }

  .annuaire-hero-actions {
    flex: 0 0 100%;
    margin: var(--space-2) 0 0;
  }

  .annuaire-hero-actions .btn { width: 100%; justify-content: center; }
}

/* ── LA FICHE : LES COMITÉS, EN JETONS SOUS L'EN-TÊTE ────────────────────────
   Trois noms de comité tiennent sur une ligne ; en section titrée avec une
   liste à puces, ils prenaient une rangée chacun. Ils sont remontés dans la
   tête parce qu'ils appartiennent à l'identité de la personne et non à son
   dossier — c'est la seule chose du palier PUBLIC de cette fiche, et la limite
   se voit maintenant à l'œil : au-dessus du filet ce que les cent six lisent,
   en dessous ce que quatorze lisent.

   ── TROIS RANGÉES, PUIS ÇA DÉFILE ───────────────────────────────────────────
   La médiane est de un comité — 58 px de section avant, 52 de jetons après,
   c'est-à-dire rien — et c'est le CAS RARE qui décide de la borne. Chez MIL une
   personne siège à vingt et un : sa section faisait 538 px, et comme la tête est
   maintenant collante, ses jetons garderaient la moitié du volet pendant qu'on
   descend dans ses disponibilités. Trois rangées visibles, le reste à la molette
   dans la même boîte — le même remède que `.annuaire-filter-list`.

   Ils ne reprennent pas `.annuaire-pill` : les vignettes d'intérêt sont des
   étiquettes de catalogue, cochées dans un formulaire et cherchables à gauche,
   d'où leur fond d'accent. Un comité est un FAIT — où cette personne siège — et
   il se dessine comme la carte de visite qu'il prolonge : le trait, pas la
   couleur (charte, §4). */
.annuaire-comites {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  max-height: calc(124px * var(--mil-fs, 1));
  overflow-y: auto;
}

.annuaire-comite {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 4px var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-size: var(--mil-fs-0);
  color: var(--c-text);
}

/* Le rang en gris : le nom du comité est ce qu'on cherche, le rang ce qui le
   qualifie. Et il n'est écrit que lorsqu'il dit quelque chose — « membre » est
   ce que le jeton signifie déjà (voir le gabarit). */
.annuaire-comite-rang { color: var(--c-text-muted); }

/* ── LA FICHE : LE CORPS ─────────────────────────────────────────────────────
   Les sections se suivent, séparées d'un filet. Chacune est coiffée d'une
   étiquette de même dessin que la barre des volets — c'est la même idée à un
   niveau plus bas, donc c'est le même signal. */
.annuaire-detail-body { padding: var(--space-5); }

.annuaire-section + .annuaire-section {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--c-border);
}

.annuaire-section-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font-size: var(--mil-fs-0);
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--c-text-mid);
}

.annuaire-pills { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.annuaire-pill {
  padding: 4px var(--space-3);
  border-radius: var(--radius);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-size: var(--mil-fs-0);
  font-weight: 500;
}

/* Le vide se dit en toutes lettres, avec un tiret devant plutôt qu'une pastille
   grise : « rien ici » n'est pas un état à décoder. */
.annuaire-none {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-muted);
}

.annuaire-none::before {
  content: '–';
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: var(--radius);
  background: var(--c-border-strong);
  color: var(--c-on-accent);
  font-size: var(--mil-fs-0);
  line-height: 1;
}

/* ── LA GRILLE DES DISPONIBILITÉS, EN LECTURE ────────────────────────────────
   Même forme que celle des filtres, à ceci près qu'on la lit au lieu de la
   cocher — donc pas de survol, pas de curseur, pas de cible tactile.

   La rangée « Flex » est celle de qui est là toute la journée. Elle a sa
   couleur à elle (`--c-orange`) et son MOT à gauche : un jour flexible se
   distingue d'un jour où la personne est là matin, après-midi et soir, et ces
   deux-là dessineraient sinon exactement la même colonne pleine. */
.annuaire-rocal {
  display: grid;
  grid-template-columns: calc(52px * var(--mil-fs, 1)) repeat(7, minmax(0, 1fr));
  gap: var(--space-1);
  max-width: 540px;
  font-size: var(--mil-fs-0);
}

.annuaire-rocal-head {
  padding: 3px 0;
  text-align: center;
  color: var(--c-text-muted);
  font-weight: 500;
}

.annuaire-rocal-period {
  align-self: center;
  padding-right: var(--space-2);
  text-align: right;
  color: var(--c-text-mid);
  font-weight: 500;
}

.annuaire-rocal-period.is-flex { color: var(--c-orange); }

.annuaire-rocal-cell {
  height: calc(28px * var(--mil-fs, 1));
  border: 1px solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-bg);
}

.annuaire-rocal-cell.is-on { background: var(--c-accent); border-color: var(--c-accent); }
.annuaire-rocal-cell.is-on.is-flex { background: var(--c-orange); border-color: var(--c-orange); }

.annuaire-pref-axis + .annuaire-pref-axis { margin-top: var(--space-3); }

.annuaire-pref-lbl {
  margin: 0 0 var(--space-2);
  font-size: var(--mil-fs-0);
  font-weight: 500;
  color: var(--c-text-mid);
}

.annuaire-lastupdate {
  margin: var(--space-4) 0 0;
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
}

/* ════════════════════════════════════════════════════════════════════════════
   L'EN-TÊTE D'ÉCRAN — la barre qui coiffe un panneau (Eric, 2026-08-13)

   « A standard app header with fixed height […] a pattern that can be reused
   elsewhere and uses the horizontal real estate properly. »

   Le raisonnement est dans `components/app-header.blade.php` ; ici, les trois
   décisions qui sont de la mise en page.

   ── ELLE SAIGNE JUSQU'AUX BORDS DU PANNEAU ─────────────────────────────────
   Marges négatives de la valeur exacte du rembourrage de `.panel`, puis le
   rembourrage rendu à l'intérieur. Sans elles, la barre serait un bloc en
   retrait de 24 px de chaque côté, avec un filet qui s'arrête avant les bords —
   ce qui se lit comme une carte dans une carte, pas comme la barre d'un écran.

   `border-radius` en haut : elle touche les coins arrondis du panneau, donc
   elle doit les épouser, sinon un angle droit dépasse d'un rayon.

   ── LA HAUTEUR EST FIXE, LA LARGEUR FAIT LE TRAVAIL ────────────────────────
   `--apphead-h` suit `--mil-fs` (voir le composant). Les trois zones sont sur
   UNE ligne, et c'est ce qui rend les 150 px de l'ancien empilement à la liste.

   ── QUI SE SERRE QUAND ÇA MANQUE ───────────────────────────────────────────
   Le titre se tronque en dernier recours, les états défilent, les actions ne
   bougent JAMAIS. C'est l'ordre inverse de l'importance visuelle, et c'est
   voulu : un bouton à moitié coupé n'est plus un bouton, alors qu'un titre à
   l'ellipse reste un titre — on sait déjà où l'on est, on vient de cliquer
   pour y venir.
   ════════════════════════════════════════════════════════════════════════════ */
:root {
  --apphead-h: calc(56px * var(--mil-fs, 1));
}

.apphead {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: calc(-1 * var(--space-5)) calc(-1 * var(--space-5)) 0;
  padding: 0 var(--space-5);
  min-height: var(--apphead-h);
  border-bottom: 1px solid var(--c-border);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--c-surface);
}

/* Le titre garde sa taille d'écran (`h1`) et perd sa marge : dans une barre,
   c'est l'alignement vertical qui espace, pas les marges du bloc.

   ── ET IL PORTE SA POLICE LUI-MÊME DEPUIS LE 2026-08-13 ────────────────────
   Elle venait de la règle `h1`, ce qui allait tant que la barre ÉTAIT le titre
   de la page. Le cadre des Comités la met au-dessus d'un écran qui a déjà son
   `<h1>` — le nom du comité —, et elle s'y rend en `<p>` pour ne pas faire deux
   titres (`titreDePage`). Sans ces deux déclarations ici, le nom de la section
   passerait à la taille du corps de texte selon la balise, et la barre
   changerait d'allure d'un écran à l'autre pour une raison purement
   sémantique. */
.apphead-titre {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--mil-fs-6);
  font-weight: 700;
  letter-spacing: -.4px;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Les états prennent la place qui reste — c'est la « horizontal real estate »
   de la demande. Ils défilent plutôt que de replier : la hauteur est fixe, donc
   une deuxième ligne déborderait au lieu de descendre.

   `scrollbar-width: none` parce qu'une barre de défilement de 8 px dans une
   rangée de 56 en mangerait le septième et sauterait à l'apparition. Ce qui
   dépasse reste atteignable au doigt, à la molette et au clavier. */
.apphead-etats {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` dans app.css */
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  overflow-x: auto;
  scrollbar-width: none;
}

.apphead-etats::-webkit-scrollbar { display: none; }

/* Les actions ne se serrent pas et ne se coupent pas. `margin-left: auto` les
   pousse au bout même quand il n'y a aucun état — sans lui, une barre sans état
   collerait son bouton au titre. */
.apphead-actions {
  flex: 0 0 auto;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* L'interrupteur ENFONCÉ de la barre — le même dessin que celui de la ligne
   des onglets (`.tabs-actions [aria-pressed="true"]`), parce que c'est le même
   objet à un étage près : « Tous les bris » enfoncé dit qu'on regarde le
   registre, et le mot porte déjà l'état (règle 6) — la couleur le confirme. */
.apphead-actions [aria-pressed="true"] {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-text);
}

/* `.btn` est dessiné pour un `<button>` : posé sur un `<a>`, il garde le
   soulignement des liens, qui barre le mot au milieu du bouton. Même correctif
   que dans l'en-tête de la fiche, et il vaut pour toute action de barre —
   celles-ci sont des liens parce qu'elles MÈNENT quelque part. */
.apphead-actions .btn {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  text-decoration: none;
}

.apphead-etat {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
  white-space: nowrap;
}

/* `[hidden]` PERD CONTRE `display: inline-flex` — la feuille de l'agent est
   battue par n'importe quelle règle d'auteur. Sans cette ligne, une pastille
   qu'un écran masque par l'attribut reste à l'écran, vide, et rien ne le
   signale (payé une première fois sur la boîte de refus de `/profil`). Elle
   sert au reste à faire des appréciations, qui apparaît et disparaît sans
   rechargement. */
.apphead-etat[hidden] { display: none; }

.apphead-etat.is-attention {
  border-color: var(--c-warning);
  background: var(--c-warning-soft);
  color: var(--c-warning-text);
}

.apphead-etat.is-info {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
}

/* Cliquable, mais pas déguisé en bouton : c'est un renvoi vers la page qui
   explique, et le soulignement au survol est ce qui le dit. */
.apphead-etat-lien { text-decoration: none; }
.apphead-etat-lien:hover { text-decoration: underline; }

/* ── SOUS 900 PX, LA BARRE RESTE UNE BARRE (Eric, 2026-08-16 : « fix the narrow
      regime so it stays one row ») ──────────────────────────────────────────
   Elle redevenait un BLOC : le titre prenait une rangée entière
   (`flex: 1 0 100%`), les états descendaient sous lui, et la hauteur fixe
   cédait. Sur `/membres` — un titre et un cadenas, deux objets qui tiennent
   dans 200 px — ça coûtait 60 px de liste au téléphone pour empiler ce qui
   tenait côte à côte. Mesuré à 555 px : 124 px de barre au lieu de 64.

   Le raisonnement d'origine était juste et visait le mauvais levier. « Une
   seule ligne pour un titre, deux états et un bouton demande environ 700 px »
   décrit la barre la plus CHARGÉE de l'app, et la règle l'appliquait à toutes —
   y compris à celles qui portent deux objets. Une requête média mesure la
   fenêtre ; elle ne peut pas savoir ce qu'il y a DANS la barre.

   Ce qui décide, à la place, c'est la politique de débordement que le composant
   documente déjà et qui vaut au large : **le titre se tronque en dernier
   recours, les états défilent, les actions ne bougent jamais**. Elle est faite
   pour ça — chacun cède dans l'ordre inverse de son importance visuelle, et
   rien n'est jamais coupé. Il n'y a donc rien à réécrire ici : il suffit de ne
   plus la défaire. Les trois surcharges qui la défaisaient sont parties, et une
   barre trop pleine se serre au lieu de s'empiler.

   Ce qui RESTE propre à l'étroit est la seule chose qui dépende vraiment de la
   fenêtre : le rembourrage du panneau descend d'un cran, et la saignée de la
   barre descend AVEC lui — les deux comptes vivent dans le même bloc parce
   qu'ils meurent ensemble.

   ── LE PANNEAU À 16, ET C'EST UNE RÉPARATION (2026-08-18) ──────────────────
   Ces marges de −16 px ont supposé pendant quatre jours un panneau à 16 px que
   personne n'avait écrit : `.panel` restait à 24 partout, donc sous 900 px la
   barre et son filet s'arrêtaient à 8 px des bords — « une carte dans une
   carte », le défaut exact que la saignée existe pour éviter, sur tous les
   écrans à panneau. Le Cahier l'a payé le premier et s'était réparé seul ;
   la règle du panneau ci-dessous rend l'accord général, et donne au passage
   16 px de colonne aux écrans de gestion sur téléphone — c'est le cran que
   `.ecran-defile` et `--fiche-pad` prennent déjà sous 700 px, appliqué dès
   que la fenêtre se serre.

   La marge du HAUT descend aussi : elle mange le rembourrage haut du panneau,
   et à −24 sur un panneau à 16 la barre dépasserait de 8 px au-dessus du bord.
   `.ecran > .apphead` et `.annuaire-plein > .apphead` ne bougent pas : leur
   `margin: 0` est plus spécifique (0,2,0) que tout ce bloc, et là-bas il n'y a
   pas de panneau à saigner. `.fiche-plein`, lui, garde son propre rembourrage :
   sa règle vient plus loin dans la feuille et gagne l'égalité.

   `flex-wrap` reste, en filet : une barre qu'aucun serrage ne sauverait
   descend plutôt que de se faire couper. C'est le cas qu'on ne veut pas rendre
   impossible — juste rare. */
@media (max-width: 899px) {
  .panel { padding: var(--space-4); }

  .apphead {
    flex-wrap: wrap;
    margin-top: calc(-1 * var(--space-4));
    margin-left: calc(-1 * var(--space-4));
    margin-right: calc(-1 * var(--space-4));
    padding-left: var(--space-4);
    padding-right: var(--space-4);
  }

  /* Le titre CÈDE au lieu de pousser : il se serre, et s'ellipse s'il le faut.
     `flex: 1 0 100%` en faisait l'inverse — il exigeait toute la largeur, donc
     tout le reste passait dessous, même quand il restait 300 px à côté de lui. */
  .apphead-titre { flex: 0 1 auto; }
}

/* ════════════════════════════════════════════════════════════════════════════
   L'ÉCRAN PREND LA FENÊTRE — « This is a search UI, not a web page » (Eric,
   2026-08-13)

   La page ne défile plus. Le panneau fait exactement la hauteur de la fenêtre,
   et ce sont les TROIS COLONNES qui défilent, chacune pour son compte.

   Ce que ça règle, et ce n'est pas du confort : sur une page qui défile d'un
   bloc, descendre dans la liste emporte les filtres hors de l'écran. On coche,
   on descend, on veut décocher — il faut remonter. À cent six personnes, ce
   va-et-vient EST le travail. Ici les filtres restent sous la main, la fiche
   reste en face, et seule la colonne qu'on parcourt bouge.

   ── LA HAUTEUR NE SE CALCULE PLUS ICI, ELLE SE REÇOIT (2026-08-16) ─────────
   Cet écran posait sa propre hauteur de fenêtre — `100vh`, puis `100dvh`, moins
   la gouttière de `.wrap`. Les deux moitiés de ce calcul sont mortes le même
   jour, et pour deux raisons différentes.

   La soustraction d'abord : elle valait `2 * --space-5`, la gouttière que
   `.wrap` posait en haut et en bas, sans quoi le panneau dépassait d'exactement
   48 px. `.wrap` n'a plus de gouttière (Eric, « the padding on .wrap is
   superfluous »), donc la garder aurait laissé 48 px de VIDE sous le panneau —
   le même défaut retourné. Ce qui compense une gouttière meurt avec elle.

   Le `100dvh` ensuite, et c'est le vrai défaut — il ne venait pas de la
   gouttière et il est plus ancien que ces deux tranches. **`.shell-main` porte
   au téléphone la compensation des deux barres FIXES du rail** (`padding-top:
   --rail-bar-h`, `padding-bottom: --rail-tabbar-h`), soit 140 px. Un `100dvh`
   posé sur un ENFANT de cette boîte s'ajoute à ces rembourrages au lieu de s'y
   fondre : sous 700 px, le panneau dépassait la fenêtre de la hauteur des deux
   barres, et la fenêtre figée redéfilait — exactement ce que la directive du
   2026-08-13 interdit. Le calcul précédent le faisait déjà : 16 + (100dvh − 32)
   + 16 vaut 100dvh tout autant, donc le débordement était le même avant.

   `.shell-main-ecran` ferme ça pour de bon, parce qu'il pose la hauteur sur
   l'élément qui PORTE les rembourrages — avec `box-sizing: border-box`, les
   140 px sont dedans — et distribue le reste par la chaîne flexible. C'est ce
   que `/profil` a mis en place le 2026-08-16 et ce que la section Comités
   faisait déjà ; cet écran-ci était le dernier à hauteur figée resté en dehors,
   et le seul à qui il manquait. `layouts/app` l'ajoute donc à `$estFenetreFigee`
   (les deux routes de l'écran, `members` et `members.show`, comme le fait déjà
   le chargement d'`annuaire.js`), et il ne reste ici qu'un `height: 100%` : la
   place qu'on lui donne, sans un seul nombre à connaître.

   `min-height: 0` est la pièce non évidente, et c'est la même qu'ailleurs : sans
   lui un enfant flexible garde la hauteur de son contenu, l'`overflow` n'a
   jamais rien à écrêter, et la fenêtre redéfile en silence.

   ── ET MAINTENANT À TOUTES LES LARGEURS (Eric, 2026-08-13) ─────────────────
   Ce bloc s'arrêtait à 900 px : en dessous, la page redevenait une page. La
   raison tenait — trois zones de défilement dans 375 px donnent trois fenêtres
   de six lignes — mais elle supposait que les trois colonnes restent trois.
   Elles ne le sont plus : à l'étroit, les filtres se replient en volet et la
   fiche prend la place de la liste, donc il n'y a JAMAIS qu'une seule zone à
   l'écran. La borne n'avait plus d'objet, et « as little scrolling as
   possible » a tranché le reste.

   `dvh` fait le travail que `vh` ne faisait pas sur téléphone : il suit la
   barre d'adresse quand elle se rétracte, au lieu de laisser le bas de l'écran
   passer dessous.
   ════════════════════════════════════════════════════════════════════════════ */
.annuaire-plein {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

/* LA CHAÎNE FLEXIBLE VAUT À TOUTES LES LARGEURS, et c'est ce qui manquait
   quand la hauteur figée est descendue sous 900 px : le panneau était bien
   borné, mais la grille dedans gardait la hauteur de son contenu, débordait, et
   la barre d'écran se retrouvait poussée hors du panneau. Rien ne le signalait
   — la page ne défilait pas, elle était simplement coupée.

   `min-height: 0` est la pièce non évidente : il autorise un élément flexible à
   être PLUS PETIT que son contenu. Sans lui, la grille garde la hauteur de sa
   plus longue colonne, le panneau la suit, et les `overflow-y` ne défilent
   jamais parce qu'il n'y a rien à déborder. C'est le piège classique du flex
   imbriqué, et il ne lève aucune erreur. */
.apphead { flex: 0 0 auto; }

/* PAS DE GOUTTIÈRE SOUS LA BARRE, et c'est le filet qui sépare. La barre est
   revenue le 2026-08-16 (voir `.annuaire-plein > .apphead` plus bas) ; lui
   rendre les 24 px qu'elle avait à sa naissance rouvrirait le vide que le
   « gapless and tighter » du même jour vient de fermer — et son `border-bottom`
   fait déjà le travail, exactement comme sous la barre des Comités. */
.annuaire-plein .annuaire-split {
  flex: 1 1 auto;
  min-height: 0;
}

/* Le volet de la liste devient une pile : sa barre, son compte, sa recherche et
   son tri restent en place, et seule la liste défile dessous. C'est ce qui fait
   qu'on peut taper une lettre de plus sans remonter chercher le champ. */
.annuaire-plein .annuaire-list,
.annuaire-plein .annuaire-detail {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.annuaire-plein .annuaire-liste,
.annuaire-plein .annuaire-fiche {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* CE QUI EST AU-DESSUS DE LA LISTE NE SE LAISSE PAS ÉCRASER.

   Un élément flexible vaut `flex: 0 1 auto` par défaut : il RÉTRÉCIT sous la
   hauteur de son contenu dès que la colonne est bornée, ce qu'elle est ici par
   construction. Tant que la barre tenait sur une ligne, ça ne se voyait pas ;
   depuis qu'elle porte le tri, elle se replie à 375 px — et la deuxième ligne
   était coupée par ce qui suit, à moitié visible dessous. Un défaut qui ne se
   voit qu'au téléphone, et qui ressemble à un défaut de dessin plutôt qu'à ce
   qu'il est. */
.annuaire-plein .annuaire-list .annuaire-panel-head { flex: 0 0 auto; }

/* La borne de la liste appartient à la page qui défile ; ici c'est la colonne
   qui borne, et 70 % d'une fenêtre à l'intérieur d'une colonne déjà bornée
   couperait la liste avant son fond. */
.annuaire-plein .annuaire-rows {
  max-height: none;
  overflow-y: visible;
}

/* Le corps de la fiche respire dans sa propre zone de défilement : le
   rembourrage du panneau ne s'applique pas à un enfant qui défile, sinon le bas
   du contenu colle au bord pendant qu'on descend. */
.annuaire-plein .annuaire-fiche { padding-bottom: var(--space-5); }

/* À l'étroit, une seule colonne est à l'écran à la fois — elle prend donc toute
   la grille. Au-delà, `height: 100%` les met à la même hauteur côte à côte. */
@media (min-width: 900px) {
  .annuaire-plein .annuaire-list,
  .annuaire-plein .annuaire-detail { height: 100%; }

  /* Les filtres cessent d'être collants : ils ne suivent plus une page qui
     défile, ils SONT une colonne qui défile.

     `relative` et non `static` : `static` défaisait aussi le confinement écrit
     plus haut — et cette règle-ci est la DERNIÈRE, donc c'est elle qui
     gagnait, précisément à la largeur où les trois colonnes existent et où le
     défaut se voyait.

     ── ET `top: auto`, PARCE QUE `relative` GARDE LE DÉCALAGE DU `sticky` ────
     Le `top: var(--space-5)` écrit pour la version collante ne s'en va pas
     avec elle : un élément `relative` DÉCALE de ses offsets au lieu de s'y
     accrocher. La colonne des filtres partait donc 24 px plus bas que la
     liste — et, gardant sa hauteur de 100 %, dépassait d'autant par le bas.
     Un défaut qui se lit comme un choix de dessin : trois colonnes à trois
     hauteurs de départ, sans rien qui dise pourquoi. */
  .annuaire-plein .annuaire-facets {
    position: relative;
    top: auto;
    max-height: none;
    height: 100%;
    overflow-y: auto;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   TROIS VOLETS QUI SE TOUCHENT, ET DES FILTRES QUI PRENNENT LA FENÊTRE
   (Eric, 2026-08-16 : « the left side filters should take advantage of the
   viewport / the UI could also be gapless and tighter overall »)

   Mesuré à 1 504 × 1 575 avant : les trois colonnes vivaient dans un panneau
   qui les rentrait de 24 px, séparées par 32 px de vide, chacune encadrée et
   arrondie — un dessin de PAGE, sur un écran qui a cessé d'en être une le
   2026-08-13 (`.annuaire-plein`). Trois cadres dans un cadre, et 88 px de
   largeur dépensés en gouttières.

   Ce qui reste pour séparer : un filet, entre deux volets et nulle part
   ailleurs. Il est porté par le bord DROIT des filtres et le bord GAUCHE de la
   fiche, jamais par la liste — c'est ce qui le fait disparaître avec le volet
   qui le pose. Entre 900 et 1 280 px, fiche ouverte, les filtres s'en vont
   (`has-detail`) : posé sur la liste, le filet resterait accroché au bord de
   l'écran, à séparer la liste du rail.

   ── ET LES FILTRES CESSENT DE DÉFILER TROIS FOIS ───────────────────────────
   Les intérêts étaient bornés à 264 px et les comités à 216, chacun avec sa
   molette. Ces bornes ont été écrites quand la colonne suivait une PAGE qui
   défilait, où sans elles la grille des disponibilités partait sous la ligne de
   flottaison. La colonne défile pour son compte depuis, et les bornes ne
   protégeaient donc plus rien : elles rendaient cinq intérêts sur neuf et sept
   comités sur vingt-deux dans deux fenêtres grandes comme des timbres, pendant
   que 684 px de la colonne restaient vides dessous. Une seule zone de
   défilement maintenant — celle de la colonne — et tout est à l'écran d'un
   coup sur un portable.

   ── ET LE TÉLÉPHONE Y GAGNE PLUS QUE LE PORTABLE (Eric, 2026-08-16, deuxième
      passe : « do the phone regime too ») ─────────────────────────────────────
   Mesuré à 375 px : 24 px de rembourrage de panneau de chaque côté, son filet,
   celui de la liste — 52 px de la largeur du téléphone, soit 14 %, dépensés à
   dessiner deux cadres l'un dans l'autre sur un écran où UN SEUL volet est
   visible à la fois. Un cadre qui n'entoure qu'une chose n'entoure rien : c'est
   le bord de l'écran qui fait ce travail. Les rangées passent de 323 à 375 px.

   Les trois déclarations qui l'obtiennent sont donc SANS condition, et seules
   les gouttières entre volets restent affaire de largeur — il n'y a rien à
   séparer là où il n'y a qu'une colonne.
   ════════════════════════════════════════════════════════════════════════════ */

/* Le panneau ne cadre plus rien : c'est la fenêtre qui cadre. */
.annuaire-plein {
  padding: 0;
  border: 0;
  border-radius: 0;
}

/* LA BARRE D'ÉCRAN SORT DE SON PANNEAU — LE MÊME CAS QUE `.ecran`, PAR L'AUTRE
   BOUT (Eric, 2026-08-16, quand le titre est revenu).

   `<x-app-header>` est dessinée pour vivre DANS un `.panel` : elle annule les
   24 px de rembourrage par des marges négatives, puis les rend en rembourrage à
   elle, ce qui la fait saigner jusqu'aux bords. Ici le panneau n'a PLUS de
   rembourrage — la déclaration juste au-dessus le lui a retiré le matin même —
   donc il n'y a rien à annuler et les marges négatives poussent la barre 24 px
   HORS du panneau : par-dessus le rail à gauche, et au-dessus du haut de la
   fenêtre, où `overflow: hidden` lui coupe la moitié du titre.

   Le tell coûte cher à lire, et c'est pour ça qu'il est écrit ici : le CSS est
   juste, le HTML est juste, la barre est là — elle est simplement décalée d'un
   rembourrage qui n'existe plus. À l'écran ça ressemble à un titre tronqué,
   c'est-à-dire à un défaut de police ou de hauteur de ligne.

   `.ecran > .apphead` porte les deux mêmes déclarations pour la raison inverse
   (elle y coiffe la section entière, hors de tout panneau). Le composant reste
   le même des deux côtés : une seconde version de la barre serait la première
   divergence. Le rayon part avec, puisque le panneau n'a plus de coins. */
.annuaire-plein > .apphead {
  margin: 0;
  border-radius: 0;
}

.annuaire-plein .annuaire-facets,
.annuaire-plein .annuaire-list,
.annuaire-plein .annuaire-detail {
  border: 0;
  border-radius: 0;
}

/* Les deux bornes des filtres tombent à TOUTES les largeurs : le volet du
   téléphone défile pour son compte lui aussi, donc elles y font le même mal
   qu'au large — deux molettes imbriquées dans une troisième.

   `flex-shrink: 0` est la pièce non évidente qui vient avec : sous 900 px le
   volet est une pile flexible, et un élément flexible RÉTRÉCIT sous la hauteur
   de son contenu par défaut. Les bornes le cachaient (le contenu tenait) ;
   sans elles, le formulaire se serait fait comprimer au lieu de faire défiler
   le volet, et le bas des comités serait devenu inatteignable. */
.annuaire-facets > * { flex-shrink: 0; }

.annuaire-plein .annuaire-filter-list,
.annuaire-plein .pastilles-rangee.est-longue {
  max-height: none;
  overflow-y: visible;
  padding-right: 0;
}

@media (min-width: 900px) {
  .annuaire-plein .annuaire-split { gap: 0; }

  .annuaire-plein .annuaire-facets { border-right: 1px solid var(--c-border); }
  .annuaire-plein .annuaire-detail { border-left: 1px solid var(--c-border); }

  /* Les trois cartes de filtres deviennent trois SECTIONS d'une même colonne :
     le filet du bas les sépare, le volet les contient. C'est la même phrase que
     pour les cent six rangées de la liste, un niveau plus haut. */
  .annuaire-plein .annuaire-facets .annuaire-panel {
    margin-top: 0;
    border: 0;
    border-radius: 0;
    border-bottom: 1px solid var(--c-border);
  }
}

/* Sous 900 px la colonne des filtres n'est plus dans le flux (elle est en
   volet), et la grille n'a qu'une piste : la liste, ou la fiche quand elle est
   ouverte, occupe toute la hauteur.

   ── `align-items: stretch`, ET C'EST UN BOGUE QUI SE VOYAIT SUR LA MOITIÉ DE
      L'ÉCRAN ────────────────────────────────────────────────────────────────
   `.annuaire-split` porte `align-items: start`, écrit pour la GRILLE : en
   grille, ça colle les trois volets en haut de leur piste au lieu de les
   étirer à la hauteur du plus long. Bon là-bas, faux ici — dans une pile
   flexible en colonne, l'axe secondaire est la LARGEUR, et `start` dit donc
   « chaque volet prend la largeur de son contenu » au lieu de celle de
   l'écran.

   Mesuré à 375 px, fiche ouverte : le volet faisait 395 px dans un conteneur
   de 293, et le bouton « Contacter », le courriel et les jetons de comité
   étaient coupés par le bord droit. Rien ne défilait pour aller les chercher —
   `.annuaire-panel` a `overflow: hidden`. Un défaut qui ne se voit qu'au
   téléphone, une fiche ouverte, et qu'aucune mesure de hauteur ne rapporte.

   La ligne s'écrit ici plutôt que sur la règle de base : en grille, `start`
   reste ce qu'on veut. */
@media (max-width: 899px) {
  .annuaire-plein .annuaire-split {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }
  .annuaire-plein .annuaire-list,
  .annuaire-plein .annuaire-detail { flex: 1 1 auto; min-width: 0; }
}

/* Le volet vide dit quoi faire, et il le dit au MILIEU : posée en haut à
   gauche, la phrase ressemble à un contenu qui n'a pas fini de charger. */
.annuaire-empty {
  padding: calc(72px * var(--mil-fs, 1)) var(--space-6);
  text-align: center;
  color: var(--c-text-muted);
}

.annuaire-empty-icon {
  font-size: var(--mil-fs-6);
  opacity: 0.4;
  margin-bottom: var(--space-3);
}

/* « Flexible » et « Pas disponible » prennent la PLACE de la grille — voir le
   gabarit. Dessinés en encadré et non en phrase nue : ils répondent à la même
   question que sept colonnes de cases, donc ils occupent le même genre de
   surface, et le filet d'accent à gauche dit que c'est une réponse et pas une
   remarque en passant. */
.annuaire-dispo-box {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--c-border);
  border-left: 3px solid var(--c-accent);
  border-radius: var(--radius);
  background: var(--c-bg);
  font-size: var(--mil-fs-2);
}

/* LA MESURE DE LECTURE DU TEXTE D'UN COMITÉ.

   ── LE NOMBRE ───────────────────────────────────────────────────────────────
   Le texte sortait sur 1 064 px de large, soit environ cent cinquante
   caractères par ligne : à ce compte l'œil ne retrouve pas le début de la ligne
   suivante, et c'est un défaut qu'aucune capture ne montre — il se mesure. La
   maquette borne d'ailleurs toute sa prose à 720 px, et le corps de ses
   écrans étroits à 520.

   68 caractères, parce que c'est de la prose : une mission, des objectifs en
   puces. La borne vit sur le BLOC et non sur `.card-lead`, parce que le même
   texte reparaît dans le tiroir sous un autre nœud — le borner deux fois aurait
   laissé la troisième occasion (« Pour le joindre ») dehors.

   ── CE QU'ELLE NE BORNE PAS, ET POURQUOI ────────────────────────────────────
   La liste du travail. Une rangée n'est pas de la prose : c'est un titre, une
   ligne de précisions et un bouton poussé à droite. La borner rapprocherait le
   bouton du titre — mais elle mettrait aussi le geste au milieu de l'écran, là
   où il n'est plus au même endroit d'une rangée à l'autre. Le bord droit est un
   repère ; une colonne de boutons alignés se balaie plus vite qu'une colonne
   qui se déplace. */
.card-about { max-width: 68ch; }

/* L'EN-TÊTE DE LA LISTE DE TRAVAIL. Le `h4` suit le plan du document (sous le
   `h3` du comité, lui-même sous le `h2` du groupe) ; le dessin, lui, est celui
   d'un en-tête de section — les mêmes caps discrètes que `h2`, qu'aucune règle
   ne lui donnait puisqu'il n'est pas un `h2`. C'est le partage que
   `.card-title` documente en sens inverse : le rang pour le lecteur d'écran, la
   classe pour l'œil.

   Il porte aussi la gouttière qui séparait les deux blocs, `h4` n'ayant ici
   aucune marge propre : sans elle la liste colle au tiroir. */
.card-work > h4 {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--mil-fs-1);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
}

/* LA LIGNE QU'ON LIT D'UN COUP D'ŒIL, et souvent la seule lue. Elle est
   au-dessus du tiroir, jamais dedans — c'est ce qui rend le tiroir
   honnête : ce qu'il cache est du détail, pas la réponse.

   Un `<div>` et non un `<p>` : `SpaceText::toHtml` sort du HTML de BLOC
   (h4/ul/p), donc les marges viennent de ses enfants. On mange la
   première et la dernière, sinon la carte a deux gouttières là où elle
   en veut une. */
.card-lead { margin: 0 0 var(--space-3); }
.card-lead > :first-child { margin-top: 0; }
.card-lead > :last-child { margin-bottom: 0; }

/* Les commandes de la carte, en ligne, à 44 px de haut (règle 9). C'est
   `.committee-actions` sous son nom de composant : les liens du comité
   n'étaient pas les seuls à en avoir besoin. */
.card-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-3) 0 0;
}

.card-actions a,
.card-actions button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* LE MENU DE LA CARTE — le `···` qui range les gestes rares.

   ── POURQUOI IL N'EST PAS `.drawer`, ALORS QUE C'EST LE MÊME `<details>` ────
   Parce que la MÊME carte porte déjà un tiroir. Deux `<details>` habillés
   pareil sur une seule carte, ce serait deux triangles identiques pour deux
   choses qui n'ont rien à voir : l'un déplie du texte à lire, l'autre ouvre un
   geste sans retour. Le triangle du tiroir promet « il y a la suite » ; ici il
   promettrait la même chose et livrerait un bouton de démission. D'où un
   habillage à lui — pas de triangle, trois points, la cible de 44 px de tout le
   reste — et c'est la raison écrite que la règle des composants demande avant
   d'en ajouter un.

   ── AUCUN POSITIONNEMENT, ET C'EST LE CHOIX ─────────────────────────────────
   Un vrai menu flottant s'ancrerait sur sa pastille, et la pastille est le
   dernier élément d'une rangée qui se replie : à 375 px elle atterrit où le
   repli la met, donc un panneau de 280 px ancré dessus sortirait de l'écran
   d'un côté ou de l'autre selon la carte. C'est exactement la panne que le rail
   a payée, en plus difficile à borner — là-bas le repère était fixe.

   Ouvert, le menu prend donc la LARGEUR de la rangée et pousse ce qui suit,
   comme le tiroir. `flex-basis: 100%` sur `[open]` : l'attribut vit sur le
   `<details>` lui-même, donc le CSS seul suffit à faire passer l'élément d'une
   pastille en bout de ligne à un bloc pleine largeur. Fermé, rien ne bouge. */
.card-menu[open] { flex-basis: 100%; }

.card-menu > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius);
  /* Les trois points sont un dessin, pas une phrase : le nom du geste vit dans
     l'`aria-label` du gabarit, et la couleur sourde est ce qui le range
     derrière les commandes qu'il accompagne. */
  color: var(--c-text-mid);
  letter-spacing: .1em;
}

.card-menu > summary::-webkit-details-marker { display: none; }
.card-menu > summary:hover { background: var(--c-hover-bg); color: var(--c-text); }
.card-menu[open] > summary { background: var(--c-hover-bg); color: var(--c-text); }

/* Le panneau est encadré parce qu'il porte un geste définitif au milieu d'une
   carte qui n'en porte aucun autre : sans son cadre, la phrase de conséquence
   se lirait comme la suite du texte du comité. */
.card-menu-panel {
  margin-top: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface-2);
}

.card-menu-panel > :first-child { margin-top: 0; }
.card-menu-panel > :last-child { margin-bottom: 0; }

/* LA RANGÉE — anciennement `.hand-row`, et son nom mentait depuis neuf vues.

   ── CE QUE LA VÉRIFICATION A DONNÉ, AVANT DE RENOMMER ───────────────────
   Dix points d'usage : un registre, une appréciation, un travail, un
   membre de comité, une main levée, un membre de la coop, un départ, une
   invitation, une réengagée. La question était de savoir s'ils voulaient
   la même rangée ou deux rangées qui s'étaient contentées d'une classe.
   Ils ont tous exactement la même forme : un bloc de TÊTE (un nom ou un
   titre, puis ses précisions en gris) et, dans six cas sur dix, des
   commandes poussées à droite. C'est une rangée mal nommée, pas deux
   rangées confondues — le renommage ne change donc pas un pixel.

   Une rangée de LISTE, pas une carte flottante (§4) : le trait du bas
   sépare, la carte encadre. Sur une liste de quarante-trois personnes,
   quarante-trois cadres ne séparent plus rien. */
.list-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--c-border);
}

/* `[hidden]` DOIT GAGNER, ET IL FAUT L'ÉCRIRE — le `display: flex` du dessus
   pèse plus lourd que la feuille de l'agent utilisateur, donc sans cette ligne
   une rangée masquée s'affiche quand même. C'est la règle de `.flash[hidden]`,
   au même mot près ; les filtres de « Tous les bris » masquent par cet
   attribut, et le bogue s'est VU à l'écran avant de s'écrire ici. */
.list-row[hidden] { display: none; }

/* Exactement la même chose que `.wrap > *` plus haut, et pour la même raison :
   un élément flex a lui aussi `min-width: auto`, donc sa largeur min-content
   pour plancher. Le repli de la rangée ne suffit donc pas — il choisit où
   couper la LIGNE, pas ce qui se passe quand un seul enfant est déjà trop
   large.

   Ce qui l'a révélé : /invitations à 320px et 160 %, où la rangée porte le nom
   ET l'adresse courriel. Une adresse n'a aucun point de coupure — pas d'espace,
   et le navigateur ne coupe ni après l'arobase ni après un point — donc le
   min-content de la rangée valait l'adresse entière, et le document défilait de
   cinq pixels. `min-width: 0` rend la main à `overflow-wrap`, qui la coupe. */
.list-row > * { min-width: 0; }

/* Le bloc de tête prend la place qui reste : sans lui, une rangée dont le
   nom est court laisse ses commandes collées au milieu de la ligne. */
.list-row > :first-child { flex: 1; }

/* `flex-wrap` pour la même raison que sur la rangée au-dessus, et l'oubli se
   payait sur /comites/…/appreciations : les quatre boutons de cran tenaient sur
   une ligne unique, plus large que la colonne. Le document faisait 402px à
   375px de large dès 125 % de zoom, 440px à 150 % — et à 320px il débordait
   DÈS 100 %, sans zoom du tout. Le quatrième cran vivait donc hors de l'écran,
   sur le geste que la coop répète tous les mois.

   Deux boutons tiennent par ligne ; le repli en met deux par ligne au lieu d'en
   cacher deux. Rien ne bouge tant que la ligne tient, donc rien au bureau. */
.list-row-actions {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* 44 PX ICI, ET SEULEMENT ICI (§0, règle 9).

   Le plancher général d'un contrôle est 36 px, et il est délibéré : les vues
   coordo denses posent trois contrôles par ligne de tableau, et 44 px les
   rendraient illisibles à l'écran où l'on compare quarante-trois lignes.

   Mais une rangée n'est pas une ligne de tableau. C'est la forme que prend le
   geste MEMBRE — « Ça m'intéresse » sur un besoin, « Finalement, non » sur un
   engagement — et il se pose au pouce, sur un téléphone. Un bouton de 39 px
   mesuré sur l'Accueil, c'est cinq pixels sous la cible, sur le seul geste que
   quatre-vingt-quatorze personnes font. La règle s'applique donc au composant
   qui porte le geste, sans déranger celui qui porte la donnée. */
.list-row-actions .btn,
.list-row-actions button,
.list-row-actions select {
  min-height: 44px;
}

@media (max-width: 700px) {
  .list-row-actions { margin-left: 0; width: 100%; }
}

/* UNE LISTE DE RANGÉES, quand la liste est une vraie liste.

   `<ul>` plutôt que des `<div>` empilés là où le nombre compte : un
   lecteur d'écran annonce « liste, 3 éléments » avant de lire la
   première, ce qui est exactement l'information qu'un voyant prend d'un
   coup d'œil. Le prix est de deux déclarations — la puce et le retrait
   du navigateur, qui n'ont rien à faire dans une rangée. */
.rows { list-style: none; margin: 0; padding: 0; }

/* LA CHIP — et elle s'appelle `.pill`, elle ne change pas de nom.

   ── POURQUOI PAS DE `.chip` ─────────────────────────────────────────────
   Parce qu'elle existe. La phase 6 l'a posée sous le nom « badge » et
   elle fait déjà ce que la maquette appelle une chip : un mot, une
   bordure, un rayon plein, sur une ligne qui ne se coupe pas. Ajouter
   `.chip` donnerait deux noms pour une chose et, dans six mois, deux
   dessins — c'est la dette qu'on vient de payer sur `.hand-row`, prise
   à l'envers.

   ── CE QU'ELLE NE FAIT PAS ──────────────────────────────────────────────
   Porter un état à elle seule. `is-warn` colore un mot déjà écrit
   (« non-membre ») ; elle ne le remplace pas (règle 6). Un état qui
   demande une phrase reste une phrase : « Le comité en a besoin
   bientôt » ne rentre pas dans une chip, et le mettre en « Urgent »
   pour qu'il y rentre serait dessiner en enlevant du sens. */
.pill {
  display: inline-block;
  padding: 1px var(--space-2);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
  white-space: nowrap;
}

.pill.is-coordo { border-color: var(--c-teal); color: var(--c-teal); }
.pill.is-warn { border-color: var(--c-warning); color: var(--c-warning-text); background: var(--c-warning-soft); }

/* Plusieurs chips à la suite. Le repli n'est pas un confort : trois chips
   à 150 % de zoom texte ne tiennent pas sur 375 px. */
.chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* DIT SANS ÊTRE MONTRÉ — la contrepartie de la règle 6, et son seul outil.

   La règle interdit qu'un dessin porte seul un sens ; il faut donc pouvoir
   poser les mots à côté du dessin sans les dessiner deux fois. La pastille du
   compte des comités est le premier cas : le rond montre « 4 », cette classe
   porte « 4 besoins ».

   ── POURQUOI PAS `display: none` NI `visibility: hidden` ────────────────
   Parce que les deux retirent l'élément de l'arbre d'accessibilité aussi : le
   texte ne serait alors caché à personne en particulier, il n'existerait plus.
   La recette ci-dessous est celle qui laisse le texte lisible aux lecteurs
   d'écran — un rectangle de 1 px, écrêté, sorti du flux.

   `white-space: nowrap` n'est pas décoratif : sans lui, un texte replié dans
   une boîte de 1 px de large peut rallonger la page de sa hauteur. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* LE TIROIR — et c'est un `<details>`, ce qui tranche la question d'Alpine.

   ── CE QU'IL RÉPARE, MESURÉ ─────────────────────────────────────────────
   `/comites` rend vingt et un comités et mesurait **25 157 px de haut** :
   trente-cinq écrans de portable, sur la page que tout le monde ouvre. Le
   coupable n'est pas le nombre de comités — c'est leur texte éditorial,
   429 caractères d'objectifs en moyenne, treize fois. Le tiroir le range
   sans le retirer : « trier, jamais filtrer » vaut pour les comités, et
   replier n'est ni l'un ni l'autre — rien ne quitte la page, et Ctrl+F le
   déplie.

   ── POURQUOI PAS DE JAVASCRIPT ──────────────────────────────────────────
   `<details>` ouvre, ferme, se pilote au clavier, s'annonce comme un
   groupe repliable et survit à une feuille de style coupée. Alpine y
   aurait ajouté une animation et quarante-six kilooctets sur chaque page.
   Le rail avait déjà tranché pareil pour ses deux menus ; ici c'était le
   dernier candidat sérieux, et il ne demande rien non plus — d'où le
   retrait d'Alpine dans cette tranche.

   Le triangle natif est GARDÉ : c'est le seul indicateur d'ouverture qui
   soit dessiné, annoncé et tourné par le navigateur sans qu'on l'écrive.
   D'où `display: list-item` implicite — pas de `display: flex` ici, qui
   l'effacerait et demanderait de le redessiner à la main. */
.drawer > summary {
  cursor: pointer;
  /* 12 px au-dessus et en dessous d'une ligne de ~20 px : la cible fait
     ses 44 px sans `min-height`, qui décentrerait le texte dans sa boîte. */
  padding: var(--space-3) 0;
  font-size: var(--mil-fs-2);
  color: var(--c-accent);
}

.drawer > summary:hover { color: var(--c-accent-dark); }

/* Le contenu du tiroir est du texte de bloc : on lui reprend la marge du
   haut, que le `<summary>` porte déjà, et on la lui rend en bas. */
.drawer-body > :first-child { margin-top: 0; }
.drawer-body { margin-bottom: var(--space-3); }

/* LE CHAMP — un libellé, son aide, son contrôle, empilés.

   ── LE NOM VIENT D'`auth.css`, ET C'EST DÉLIBÉRÉ ────────────────────────
   Les écrans d'avant-connexion ont `.field`, `.field-help` et `.check`
   depuis la phase 4. Les deux feuilles ne peuvent pas fusionner — `auth.css`
   sert des pages sans rail ni personne à nommer, c'est la raison de la
   scission — mais rien n'oblige à inventer un second vocabulaire pour la
   même chose. Le formulaire de connexion et celui d'un besoin se lisent
   donc pareil, et qui connaît l'un connaît l'autre.

   ── 44 PX ICI, 36 AILLEURS ──────────────────────────────────────────────
   Même partage que sur la rangée : le plancher général d'un contrôle est
   36 px pour les vues coordo denses, où trois contrôles se suivent sur une
   ligne de tableau. Un champ de formulaire n'est jamais dans ce cas — c'est
   la chose qu'on vient toucher, et /membres est un écran que cent six
   personnes ouvrent au téléphone. */
.field { margin-top: var(--space-4); }
.field:first-child { margin-top: 0; }

.field > label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--mil-fs-2);
}

/* `44ch` : au-delà, un champ de recherche large comme la page suggère qu'on
   y écrit une phrase, et le curseur part si loin du libellé que les deux
   cessent de se lire ensemble.

   ── LE `:not()` N'EST PAS UN CAPRICE, IL FAIT GAGNER LA RÈGLE ───────────
   Écrite `.field > input`, elle PERDAIT. La règle des contrôles plus bas
   sélectionne `input:not([type="checkbox"]):not([type="radio"])`, et un
   `:not()` compte la spécificité de son argument : deux sélecteurs
   d'attribut, donc (0,2,1) contre les (0,1,1) de `.field > input`. Les deux
   déclarations que ce bloc existe pour poser — 44 px et `44ch` — étaient
   donc écrasées par les 36 px et le `max-width: 100%` d'à côté, sur TOUS
   les écrans habillés (/membres, le travail d'un comité, son texte).
   Mesuré : 39 px de haut et 1064 px de large dans une carte de 1064.

   Rien ne le signalait, et rien ne pouvait : une règle sans effet ne casse
   pas de test, elle laisse juste croire que la décision est appliquée.

   Le `<select>`, lui, monte à 44 px sans rien changer à sa largeur (bloc
   suivant) : la sienne est celle de sa plus longue option, et la borner ne
   se décide pas ici. */
.field > input:not([type="checkbox"]):not([type="radio"]),
.field > textarea {
  width: 100%;
  max-width: 44ch;
  min-height: 44px;
}

/* Un `<select>` DANS un champ est un champ, et il montait à 36 px là où ses
   voisins montent à 44 : un formulaire de cinq contrôles en affichait deux
   hauteurs. C'est le défaut même que la règle des contrôles avait été écrite
   pour fermer, rouvert d'un cran plus bas. La largeur ne bouge pas — voir
   le commentaire de la règle des contrôles, c'est la plus longue option qui
   décide, et `max-width: 100%` l'empêche de sortir. */
.field > select { min-height: 44px; }

.field-help {
  display: block;
  margin: var(--space-1) 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* UN GROUPE DE CASES À COCHER.

   Le `<fieldset>` RESTE, et ce n'est pas de la décoration : c'est lui qui
   fait annoncer « Intérêts » avant chaque case par un lecteur d'écran, donc
   la seule chose qui distingue « Réparer & bricoler » comme intérêt de la
   même phrase comme compétence. Ce qui s'en va est sa bordure en relief,
   dessinée par le navigateur en 1995 et jamais depuis.

   Les cases vivent dans un enfant qui se replie, pas dans le `<fieldset>`
   lui-même : une `<legend>` dans un conteneur flex se rend de travers, et
   ce sont les CASES qu'on veut mettre côte à côte, pas la légende avec. */
.check-group {
  border: 0;
  padding: 0;
  margin: var(--space-4) 0 0;
  min-width: 0;
}

.check-group > legend {
  padding: 0;
  font-size: var(--mil-fs-0);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
}

/* `column-gap` plus large que `row-gap` : sans ça, deux cases voisines sur
   une ligne se lisent comme une seule étiquette coupée en deux — vu sur
   /membres, où « Accueillir & soutenir » et la case suivante se touchaient. */
.checks {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-5);
  row-gap: 0;
}

/* LA BARRE D'ONGLETS d'un comité. Des liens, pas des boutons : chaque onglet
   est une URL, donc il se met en signet et revient par le bouton Précédent.

   `flex-wrap` et pas de défilement horizontal : trois onglets à 150 % de zoom
   texte sur un téléphone passent à la ligne, ce qui reste lisible — une barre
   qui déborde cache le troisième, et c'est celui des mains levées. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: var(--space-4) 0 var(--space-5);
  border-bottom: 1px solid var(--c-border);
}

.tabs .tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  min-height: 44px;
  color: var(--c-text-mid);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: color var(--t-fast), border-color var(--t-fast);
}

.tabs .tab:hover { color: var(--c-text); background: var(--c-hover-bg); }

/* L'onglet actif se distingue par la COULEUR ET le trait — et le gabarit pose
   `aria-current="page"` en plus, parce qu'aucun des deux ne se lit à voix
   haute (règle 6). */
.tabs .tab.is-current {
  color: var(--c-text);
  font-weight: 600;
  border-bottom-color: var(--c-accent);
}

/* Le compte, en retrait du libellé sans être une pastille de couleur : c'est un
   nombre, il se lit comme un nombre. */
.tab-count {
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* ---------------------------------------------------------------
   Messages
   --------------------------------------------------------------- */

/* LE BANDEAU DE PERSONNIFICATION — « tu regardes l'app comme X », et la sortie.

   ── IL RESSEMBLE À UN AVERTISSEMENT PARCE QUE C'EN EST UN ───────────────────
   Mêmes jetons que `.note` : ce n'est pas un état normal de l'app, et c'est
   même le seul où ce qu'on lit à l'écran n'est pas ce que la personne connectée
   verrait. Le ⚠ n'est pas repris — la phrase le dit déjà, et le bouton de
   sortie est à côté (§0, règle 6 : l'état est écrit, jamais porté par la seule
   couleur, et ici il l'est deux fois).

   ── IL N'EST PAS `sticky` ───────────────────────────────────────────────────
   Tentant, et non : le rail l'est déjà, la barre du bas du téléphone aussi, et
   un troisième meuble collé mangerait le tiers d'un écran court. La sortie
   reste atteignable en haut de chaque page, ce qui est un défilement — pas une
   chasse. */
.impersonation {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: var(--space-4) var(--space-5) 0;
  padding: var(--space-3);
  border-left: 3px solid var(--c-warning);
  background: var(--c-warning-soft);
  color: var(--c-warning-text);
}

/* `min-width: 0` : sans lui, un nom long empêche l'élément de rétrécir et
   pousse le bouton de sortie hors de la colonne sur un téléphone. */
.impersonation-who {
  margin: 0;
  min-width: 0;
  font-size: var(--mil-fs-2);
}

/* `.flash` NE PORTE PLUS DE MESSAGE DE SESSION, et sa règle est restée.
   Les confirmations et les refus sont partis en surimpression (`toasts.css`) ;
   ce qui garde cette classe est le RÉCAPITULATIF D'ERREURS en tête de
   formulaire — `profile/show`, `invitations/index` — et il n'a pas suivi
   exprès : il doit rester à l'écran PENDANT qu'on corrige les champs, donc il
   ne peut pas être une chose qui s'efface au bout de huit secondes.

   `.flashes`, la boîte qui les empilait dans `.shell-main`, est partie avec
   eux : ce qui reste est un bloc dans le flux d'une vue, qui porte sa propre
   marge. */
/* ── IL PORTAIT ENCORE LE DESSIN DES AVIS D'AVANT (2026-08-12) ──────────────
   Cadre de 1 px dans la teinte, bloc entièrement coloré : la recette que les
   avis viennent d'abandonner, et qu'Éric a nommée en la voyant — « an old php
   thing ». Elle a survécu ici un jour de plus parce que ce bloc n'a pas suivi
   les avis en surimpression.

   Il ne les suit pas plus aujourd'hui, et c'est le point : ce qui FLOTTE
   (`toasts.css`) est arrondi, ombré, posé sur une surface neutre ; ce qui est
   DANS la page prend le vocabulaire du voisin qui l'accompagne déjà —
   `.note`, quinze lignes plus bas : un TRAIT sur le bord, un fond adouci, pas
   d'ombre, pas de coin arrondi. La couleur se concentre sur l'arête au lieu de
   faire le tour, et les deux blocs d'un même écran cessent de se contredire.

   ── LE `flex` RÉPARE UN DÉFAUT VISIBLE, PAS SEULEMENT UN GOÛT ──────────────
   Le signe était un `::before` sur un conteneur dont les enfants sont des
   `<p>` : il ouvrait donc SA PROPRE LIGNE, tout seul au-dessus du message.
   Posé en élément de `flex`, il revient à gauche de la première ligne, qui est
   sa place. `align-items: flex-start` le garde en tête quand le message fait
   trois lignes. */
.flash {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  border-left: 3px solid;
  padding: var(--space-3);
  margin-bottom: var(--space-3);
}

/* Le statut n'est JAMAIS porté par la couleur seule (§0, règle 8) :
   chaque état porte aussi un signe, que la palette daltonien et les
   écrans en niveaux de gris rendent tout aussi bien.

   `line-height: inherit` aligne le glyphe sur la PREMIÈRE LIGNE du message :
   sans lui, l'élément de flex prend sa propre hauteur de ligne et le signe se
   décale d'un ou deux pixels vers le haut — assez pour se voir, pas assez pour
   qu'on trouve pourquoi. */
.flash::before { flex: none; font-weight: 700; line-height: inherit; }
.flash.is-ok { border-color: var(--c-success); background: var(--c-success-soft); color: var(--c-success-text); }
.flash.is-ok::before { content: "✓"; }
.flash.is-error { border-color: var(--c-danger); background: var(--c-danger-soft); color: var(--c-danger-text); }
.flash.is-error::before { content: "⚠"; }

/* `[hidden]` DOIT GAGNER, ET IL FAUT L'ÉCRIRE.

   L'attribut `hidden` du navigateur pose `display: none` dans la feuille de
   l'agent, qui perd contre n'importe quelle règle d'auteur — `display: flex`
   trois lignes plus haut, ici. La boîte de refus de `/profil`, rendue vide et
   `hidden` pour que `profil.js` la remplisse sans recharger, s'affichait donc à
   CHAQUE ouverture de la fiche : un encadré rouge, avec son ⚠, et pas un mot
   dedans. Mesuré à 48 px de haut sur un téléphone, juste sous le nom.

   C'est le mode de défaillance habituel de cette paire, et il ne se signale
   nulle part : le HTML est juste, le script est juste, la page ment. */
.flash[hidden] { display: none; }

/* LE CORPS, ET IL N'EST OBLIGATOIRE QUE QUAND IL Y A PLUSIEURS MESSAGES.
   Deux des trois `.flash` de l'app portent UNE phrase, en texte nu — le refus
   d'`invitations/index`, et la boîte que `profil.js` remplit par
   `textContent`. Un texte nu devient tout seul l'élément de flex qu'il faut,
   donc ces deux-là n'ont rien à envelopper.

   Le récapitulatif de `profile/show`, lui, empile des `<p>` : sans ce
   conteneur ils deviendraient des éléments de flex CÔTE À CÔTE, et deux
   refus s'afficheraient sur une ligne à se partager la largeur. */
.flash-body { flex: 1; min-width: 0; }
.flash-body p { margin: 0; }
.flash-body p + p { margin-top: var(--space-2); }

.note {
  border-left: 3px solid var(--c-warning);
  background: var(--c-warning-soft);
  color: var(--c-warning-text);
  padding: var(--space-3);
  margin: var(--space-4) 0;
  font-size: var(--mil-fs-2);
}

.note::before { content: "⚠ "; font-weight: 700; }

/* La même phrase-règle, sans l'alarme. Un membre qui ne peut pas changer le
   roster n'est pas dans un état anormal : le ⚠ et le fond « warning » de
   `.note` lui annonceraient un problème qui n'existe pas. Le cadre reste — la
   phrase doit se lire comme une règle, pas se perdre dans la page (charte,
   règle 6 : l'état est écrit, jamais porté par la seule couleur). */
.note-plain {
  border-left: 3px solid var(--c-border-strong);
  background: var(--c-surface-2);
  color: var(--c-text-mid);
  padding: var(--space-3);
  margin: var(--space-4) 0;
  font-size: var(--mil-fs-2);
}

/* `.account` VIVAIT ICI — le bas de sa propre fiche, la coop où l'on est
   connecté et la sortie. Retiré le 2026-08-16 avec la section qu'il habillait
   (Eric : « remove déconnexion (mon compte), it's already in the menu »).

   Son argument est gardé parce qu'il resservira au premier bloc de SESSION
   posé dans un écran de PERSONNE : un trait au-dessus, parce que ce n'est plus
   la même chose qu'on regarde, et un geste de sortie qui ne croise pas le pouce
   de quelqu'un venu cocher une disponibilité. */

/* ---------------------------------------------------------------
   Contrôles
   --------------------------------------------------------------- */

/* `max-width` est là pour le `<select>`, et c'est le contrôle le plus
   dangereux de la page au téléphone : sa largeur naturelle est celle de sa PLUS
   LONGUE OPTION, pas celle de l'option affichée. Un menu de quatre-vingt-dix
   membres est donc large comme le plus long nom de la coop — et personne ne le
   voit venir, puisque l'écran a l'air normal tant que ce nom-là n'existe pas.

   Mesuré sur /departs avec « Marie-Christine Beauchemin-Lafontaine » : 304px de
   sélecteur à 100 % de zoom, 412px à 150 %, dans une colonne de 293. C'est le
   seul débordement de la revue qui frappait DÈS 100 % — le zoom texte ne faisait
   que l'aggraver. Et rien ne peut le couper : un `<option>` n'a pas de point de
   césure, `overflow-wrap` n'a aucune prise dessus. Le borner est la seule issue ;
   le nom se tronque à l'affichage, la liste déroulante le rend en entier. */
/* ── LES CHAMPS DE SAISIE ARRIVENT DANS CETTE RÈGLE, ET C'ÉTAIT UN TROU ────
   `input` et `textarea` n'avaient AUCUNE déclaration : ils sortaient au
   dessin du navigateur, à côté d'un `<select>` qui, lui, était habillé. Sur
   /membres la recherche et le sélecteur de comité se suivent sur deux
   lignes — deux hauteurs, deux bordures, deux rayons — et sur /comites/…/
   travail le formulaire de déclaration en aligne six. Ce n'est pas passé
   inaperçu par indulgence : personne n'avait encore habillé un formulaire.

   `:not()` plutôt qu'une liste de types : une case à cocher et un bouton
   radio ne sont pas des boîtes de texte et ne veulent ni la hauteur, ni le
   rembourrage, ni la bordure. Écrire la liste des types qui EN veulent
   (`text`, `search`, `email`, `date`, `password`, `number`…) oblige à
   penser à `tel` le jour où quelqu'un l'utilise ; l'exclusion, non. */
.btn,
button,
select,
input:not([type="checkbox"]):not([type="radio"]),
textarea {
  font: inherit;
  font-size: var(--mil-fs-2);
  min-height: 36px;
  max-width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
}

/* Verticalement SEULEMENT. Une zone de texte qu'on élargit à la souris
   dépasse sa colonne et emporte le document avec elle — le même débordement
   latéral que `min-width: 0` ferme partout ailleurs, rouvert par une
   poignée. La hauteur ne coûte rien à personne. */
textarea { resize: vertical; }

button { cursor: pointer; transition: background var(--t-fast); }
button:hover { background: var(--c-hover-bg); }

/* ── UN BOUTON QUI EST UN LIEN EST UN BOUTON, ET PAS SEULEMENT DE COULEUR ───
   Mesuré à l'écran le 2026-08-16, sur `/administration` puis sur `/bris` : le
   même `<a class="btn">` sortait souligné ici et pas là. Ce n'est pas `.btn`
   qui retirait le trait — il ne l'a jamais retiré — ce sont deux règles de
   LIEU, `.liste-head > .btn` et `.tabs-actions > .btn`, qui le faisaient pour
   les boutons qu'elles hébergent. Quinze `<a class="btn">` vivent dans les
   gabarits ; ceux qui tombent ailleurs gardaient le soulignement d'un lien.

   Et le trait n'était que la moitié visible. Ces deux règles posent aussi
   `display: inline-flex`, sans quoi l'ancre reste `inline` : une boîte en ligne
   ignore `min-height` et ne prend pas la hauteur de son rembourrage vertical,
   donc le « bouton » se réduit à un filet de texte bordé. Là encore, le dessin
   correct dépendait de l'endroit où on l'avait posé.

   Ce qui appartient au TYPE se déclare donc ici, une fois. `a.btn` et non
   `.btn` : `display: inline-flex` sur la règle de base emporterait `select`,
   `input` et `textarea`, qui la partagent. Ce que les deux règles de lieu
   gardent en propre est leur `min-height: 44px` — une cible de rangée, donc une
   décision de mise en page, pas une propriété du bouton. */
a.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.btn-primary {
  border-color: var(--c-accent);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-weight: 600;
}

.btn-primary:hover { background: var(--c-accent-dark); border-color: var(--c-accent-dark); }

/* ── UN BOUTON QUI EST UN LIEN PERD SON ENCRE DANS UN PANNEAU ───────────────
   `.panel a { color: var(--c-accent) }` a DEUX crans de spécificité, `.btn` et
   `.btn-primary` un seul. Un `<a class="btn btn-primary">` dans un panneau
   sortait donc en accent SUR l'accent : un rectangle bleu plein, sans libellé
   visible. Vu à l'écran sur « Nouveau à faire » le 2026-08-12 — et c'est le
   genre de défaut qu'une capture montre mieux qu'une relecture, parce que le
   texte est bien là, dans le DOM, dans l'ordre de tabulation, annoncé par un
   lecteur d'écran. Seul l'œil ne le trouve pas.

   Même redéclaration que `.head-menu > summary.btn-primary`, et pour la même
   raison : une classe ne gagne que sur ce qu'elle réécrit, à spécificité
   égale. Ces deux règles-ci existent parce qu'un bouton d'action peut avoir de
   bonnes raisons d'être un `<a>` — ici, sauter au formulaire quand le script
   des modales ne tourne pas. */
.panel a.btn,
.ecran a.btn { color: var(--c-text); }
.panel a.btn-primary,
.ecran a.btn-primary { color: var(--c-on-accent); }

.btn-danger { color: var(--c-danger-text); border-color: var(--c-danger); background: var(--c-danger-soft); }
.btn-danger:hover { background: var(--c-danger-soft); }

/* ── IL N'Y A PLUS DE BOUTON QUI SE LIT COMME UN LIEN (2026-08-16) ──────────
   `.btn-link` déshabillait un bouton jusqu'à le faire passer pour un lien :
   bordure, fond, rembourrage et hauteur retirés, encre d'accent, souligné.
   Trois points d'usage, et les trois disaient la même chose de travers.

   · « Ouvrir » sur l'aperçu de l'administration et « Retirer » sur la liste
     des administrateurs s'écrivaient `class="btn btn-link"` — une paire qui
     se contredit. `.btn` et `.btn-link` ont la même spécificité, `.btn-link`
     était déclaré plus bas, donc il gagnait et ne laissait de `.btn` que le
     nom. Le gabarit de l'aperçu, lui, appelle la chose « un bouton » dans son
     propre commentaire : l'intention était écrite, seul le rendu ne suivait
     pas.
   · La déconnexion était le cas d'origine, et sa raison d'être — un POST ne
     peut pas être une balise <a> — explique pourquoi c'est un `<button>`,
     jamais pourquoi il devrait RESSEMBLER à un lien. C'est même l'inverse :
     un geste qu'on ne défait pas d'un retour arrière a tout à gagner à porter
     le dessin de ce qu'il est.

   Ce que la classe coûtait dépasse le style : une cible sans rembourrage ni
   `min-height` tombe sous les 44 px que la règle 9 exige des pages membre, et
   la déconnexion vivait sur la fiche de profil, qui est une page membre.

   Ce qu'il faut savoir si le besoin revient : ce qui doit se lire comme un
   lien EST un lien (`<a>`), et ce qui agit porte `.btn`. La variante discrète
   d'un bouton n'est pas « sans cadre » — c'est `.btn` nu, qui est déjà le
   secondaire du système (bordure fine sur surface). */

/* L'anneau de focus signature (§0, règle 7), sur TOUT ce qui se focalise —
   y compris `.table-scroll`, qui n'est pas un contrôle mais reçoit le focus
   pour pouvoir défiler au clavier. Ajouté ICI plutôt qu'à côté de sa règle :
   deux définitions de l'anneau, c'est une de trop. */
button:focus-visible,
select:focus-visible,
input:focus-visible,
a:focus-visible,
summary:focus-visible,
.table-scroll:focus-visible {
  outline: none;
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-soft);
}

.inline { display: inline; }

/* ---------------------------------------------------------------
   Tableaux — réservés aux vues coordo denses (§4)
   --------------------------------------------------------------- */

table { width: 100%; border-collapse: collapse; }

th, td {
  text-align: left;
  padding: var(--space-2);
  border-bottom: 1px solid var(--c-border);
  vertical-align: middle;
}

th {
  font-size: var(--mil-fs-0);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
  background: var(--c-section-bar);
}

tbody tr:hover { background: var(--c-hover-bg); }

.cell-actions { text-align: right; }

/* C'EST LE TABLEAU QUI DÉFILE, PAS LA PAGE.

   Une colonne de tableau ne descend jamais sous le min-content de ses cellules
   — c'est le seul endroit de la mise en page où « rétrécis » n'est pas une
   réponse possible. Et `overflow-wrap` n'y peut rien : `break-word` est
   explicitement ignoré dans le calcul du min-content, et `anywhere`, qui y
   compte, redistribue les colonnes au prorata de leur max-content. Essayé : à
   320px et 150 %, la colonne « Qui » de /confidentialite tombait à 58px et
   rendait « Les per son nes qui coo rdo nne nt un co mit é ». Un débordement
   réparé en illisibilité n'est pas réparé.

   Le roster tranche la question pour de bon : sa deuxième colonne porte un
   `<select>`, la troisième un bouton. Ni l'un ni l'autre n'offre le moindre
   point de coupure — aucune stratégie de césure, quelle qu'elle soit, ne les
   fera tenir dans 98px. Il ne reste que défiler, ou changer la forme du
   tableau ; la forme est une décision de la phase 7.

   Confiner le défilement au tableau garde donc les DEUX propriétés : la page
   entière ne part plus de côté (personne ne devine qu'il faut la pousser), et
   le tableau reste un tableau, lisible en le faisant glisser. C'est aussi
   l'exception que la WCAG 1.4.10 nomme : un contenu qui a besoin de deux
   dimensions a le droit de les demander — dans sa propre boîte.

   `tabindex` et `role` vivent dans le gabarit, parce qu'une région qui défile
   doit être atteignable au clavier (WCAG 2.1.1) et nommée pour être annoncée.

   Au-dessus d'environ 700px il n'y a rien à faire défiler : `auto` ne montre
   pas de barre, et les vues coordo denses sont exactement comme avant. */
.table-scroll { position: relative; overflow-x: auto; }  /* écrête ⇒ contient, cf. `.ecran` */

/* ---------------------------------------------------------------
   Préférences d'affichage
   ---------------------------------------------------------------
   Le système de design impose de VÉRIFIER chaque écran en clair, en
   sombre ET en daltonien. Sans un endroit où basculer, cette
   vérification demande de bricoler le navigateur — donc elle ne se
   fait pas. D'où ces trois contrôles.

   ILS N'ONT PLUS DE PASTILLE À EUX : le `<details>` qui les portait
   dans l'en-tête a été remplacé par celui des réglages, dans le pied
   du rail. Deux disclosures imbriquées demanderaient deux clics et
   deux Échap pour un seul geste — le partiel ne porte donc plus que
   ses rangées, et le panneau est `.rail-menu-panel`.
   --------------------------------------------------------------- */

.prefs-row + .prefs-row { margin-top: var(--space-3); }

.prefs-label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--mil-fs-0);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
}

.seg { display: flex; gap: var(--space-1); }

.seg-opt {
  flex: 1;
  min-height: 36px;
  font-size: var(--mil-fs-1);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text-mid);
  cursor: pointer;
}

.seg-opt[aria-pressed="true"] {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 600;
}

/* Le contrôle qui RÈGLE le zoom ne se redimensionne pas lui-même —
   sinon il fuit sous le doigt à mesure qu'on l'utilise (§3). C'est
   l'exception documentée à la règle 5, et `fs-exempt` est ce qui la
   distingue d'un oubli pour `DesignSystemRulesTest`.

   C'est aussi la SEULE taille de la feuille qui ne lit pas un cran, et
   ça ne peut pas être autrement : un cran porte le multiplicateur, et
   c'est précisément le multiplicateur qu'il faut retirer ici. Le 13 px
   en dur est la valeur du cran 1 figée à 100 % — si le cran 1 change
   de valeur dans `tokens.css`, ce nombre-ci doit suivre à la main.
   `.seg-opt` juste au-dessus, lui, est bien sur le cran. */
.seg[data-pref="fontscale"] .seg-opt { font-size: 13px; /* fs-exempt */ }

.check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  font-size: var(--mil-fs-2);
  cursor: pointer;
}

.check input { width: 18px; height: 18px; accent-color: var(--c-accent); }

/* Une LISTE de cases sous un choix qui les commande — les comités qui voient
   une tuile, sur `/applications`.

   Le retrait à gauche est la seule chose que cette règle apporte, et c'est
   elle qui porte le sens : les cases dépendent du bouton « Certains comités »
   juste au-dessus, et sans décalage la grappe se lit comme trois choix de même
   niveau. Une coop cocherait alors des comités en croyant la tuile publique.

   Elle DÉFILE au-delà d'une dizaine de comités : MIL en a vingt-deux, et un
   formulaire qui pousse son bouton d'envoi hors de l'écran se fait abandonner
   au milieu. */
.check-list {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  margin-left: var(--space-5);
  max-height: 15rem;
  overflow-y: auto;
}

/* ---------------------------------------------------------------
   REMPLIR SA FICHE — les trois formes que le formulaire ajoute
   ---------------------------------------------------------------
   Tout le reste de l'écran est déjà là : `.field`, `.check-group`,
   `.checks`, `.check`, `.btn-primary`. Ces trois blocs-ci n'existaient
   pas parce qu'aucun autre écran ne les demandait.
   --------------------------------------------------------------- */

/* Les intérêts sur mesure — des JETONS, et un champ pour en poser (l'arbitrage
   est sur le bloc du gabarit). Le jeton reprend la pastille du catalogue
   (`.tag`) : même bord, même état coché, parce que c'est la même chose — un
   intérêt qui compte dans le même plafond. Sa croix est un DESSIN, pas un
   bouton : le jeton entier est la case, la croix ne fait que dire ce qu'un
   clic y fait. */
.custom-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Sans jeton, pas de rangée : la marge d'une boîte vide écarterait le champ
   de sa légende pour rien. */
.custom-chips:empty { display: none; }

.custom-chip .tag-face { gap: var(--space-2); }

.custom-chip-x { display: flex; color: var(--c-text-muted); }

.custom-chip-x svg { width: 14px; height: 14px; }

/* Le champ et son « + ». La largeur est bornée pour la même raison qu'une
   colonne de lecture l'est : un champ pleine page invite à écrire un
   paragraphe dans une colonne qui en accepte 120 caractères. */
.custom-ajout {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  max-width: 30rem;
}

/* `[type="text"]` N'EST PAS DU ZÈLE : la règle de base des champs
   (`input:not([type="checkbox"]):not([type="radio"])`) pèse (0,2,1) — les
   deux `:not()` comptent — et un simple `.custom-ajout input` (0,1,1) lui
   perdrait le `min-height`. À poids égal, c'est l'ordre qui tranche, et
   cette règle-ci vient après. 44 px : la cible de la maison (règle 9), là
   où la base s'arrête à 36. */
.custom-ajout input[type="text"] {
  flex: 1;
  min-width: 0;
  min-height: 44px;
}

/* Le « + » est un vrai bouton d'envoi (voir le gabarit : c'est son repli sans
   script), rond pour se dire d'un coup d'œil « un geste », et gris — pas
   l'accent d'un bouton principal : le formulaire s'enregistre tout seul, ce
   bouton ne fait que poser un mot. */
.custom-ajouter {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text-mid);
  cursor: pointer;
}

.custom-ajouter:hover { background: var(--c-hover-bg); color: var(--c-text); }

.custom-ajouter:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* LE STATUT DE DISPO EN TROIS CARTES (2026-08-18, maquette d'Eric) — le même
   dessin que les trois questions de Préférences, parce que c'est la même
   sorte de question : la réponse EST le geste, la carte entière se touche.
   Une grille et non `.choices` : trois réponses qui se comparent d'un coup
   d'œil sont trois colonnes ÉGALES, pas des pilules à largeur de mot. Sous
   700 px elles s'empilent, comme toute carte de choix. */
.dispo-seg {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

@media (min-width: 700px) {
  .dispo-seg { grid-template-columns: repeat(3, 1fr); }
}

/* 56 px : la carte est seule porteuse de son mot (l'aide vit en dessous), et
   c'est la première cible de la colonne — un cran au-dessus des 52 de
   `.choice-face`. */
.dispo-seg .choice-face { min-height: 56px; }

/* LA PHRASE D'AIDE SUIT LE CHOIX. Les trois sont rendues ; celle du statut
   coché s'affiche, et avant le premier choix aucune (le moins de texte
   possible — la phrase arrive quand elle parle de TON état). Sans `:has()`
   elles restent TOUTES visibles — le repli ne perd pas un mot, il en montre
   trop — et chaque radio garde la sienne par `aria-describedby`, que
   l'affichage soit là ou pas. L'option « je préfère ne pas dire » est partie
   le 2026-08-18 (Eric : « same as not filling anything »), sa ligne d'aide
   avec elle. */
.dispo-aide { margin-top: var(--space-2); }

.dispo-aide > p { margin: 0; }

@supports selector(:has(input)) {
  .dispo-aide > p { display: none; }

  fieldset:has(.dispo-seg input[value="flexible"]:checked) .dispo-aide > p[data-statut="flexible"],
  fieldset:has(.dispo-seg input[value="occupe"]:checked) .dispo-aide > p[data-statut="occupe"],
  fieldset:has(.dispo-seg input[value="pas_dispo"]:checked) .dispo-aide > p[data-statut="pas_dispo"] { display: block; }
}

/* UN CHOIX QUI PORTE SON EXPLICATION — le libellé au-dessus, la phrase en
   dessous, et la puce alignée sur la première ligne.

   `.check-stack` est le nom GÉNÉRIQUE, ajouté le 2026-08-13 avec les
   réparations : « dans mon logement / dans un espace commun » a besoin
   exactement de ce dessin. `.dispo-choice`, qui partageait ces déclarations,
   est partie le 2026-08-18 — le statut de dispo est passé aux cartes
   ci-dessus, et la promesse du commentaire d'origine (« elle se retirera »)
   est tenue. */
.check-stack { align-items: flex-start; padding: var(--space-2) 0; }

.check-stack input { margin-top: 3px; }

.check-stack > span { display: flex; flex-direction: column; }

.check-stack .muted { font-size: var(--mil-fs-1); }

/* LA GRILLE DES DISPOS — un vrai tableau, et il DÉFILE plutôt que de
   serrer. Quatre colonnes à 375 px de large tiennent tout juste ; à 160 %
   de zoom texte elles ne tiennent plus, et le comportement par défaut
   d'un tableau est alors de comprimer les colonnes jusqu'à couper les
   mots des en-têtes. Le conteneur qui défile garde les cases à 44 px —
   c'est-à-dire cliquables au pouce — et laisse partir la largeur.

   C'est le seul `overflow` de la feuille en dehors du panneau du rail, et
   il porte la même contrainte à l'envers : il est posé sur un conteneur
   DÉDIÉ, pas sur un parent qui emballe autre chose. */
.avail-wrap {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` dans app.css */
  overflow-x: auto;
  margin: var(--space-4) 0;
}

/* LA GRILLE ÉTEINTE (Eric, 2026-08-18) : « Plutôt flexible » et « Pas
   disponible » n'ont rien à y cocher, donc elle se met en retrait. C'est
   `inert` — posé par le serveur, rejoué par `profil.js` — qui la rend
   vraiment inactive (ni clic, ni clavier, ni lecteur d'écran) ; l'opacité ne
   fait que le MONTRER, et elle n'est pas seule à le dire : le statut coché
   juste au-dessus est le mot (règle 6). Les cases restent dans l'envoi —
   c'est toute la différence avec `disabled`, expliquée dans le gabarit. */
.avail-wrap[inert] {
  opacity: .45;
}

/* `width: auto` EST ÉCRIT, et ce n'est pas la valeur par défaut qu'on
   répète : une table dans un conteneur qui défile prend la largeur de son
   contenu, et le contenu le plus large décide. La phrase d'aide a vécu en
   `<caption>` et poussait les trois colonnes à six cents pixels du nom du
   jour — mesuré. Elle est sortie du tableau ; cette ligne dit pourquoi il
   ne faut pas l'y remettre. */
.avail {
  border-collapse: collapse;
  width: auto;
}

.avail th {
  font-weight: 500;
  text-align: left;
  padding: var(--space-2) var(--space-3);
}

/* Le nom du jour ne se coupe pas — « MER- / CREDI » sur une grille de
   sept rangées se lit deux fois. Les en-têtes de COLONNE, eux, ont le
   droit : c'est ce qui fait tenir la grille dans un téléphone. Mesuré à
   375 px, la table faisait 378 px dans un panneau de 293 ; elle fait
   293 px pile une fois les en-têtes libres de passer à la ligne, donc
   le conteneur qui défile ne défile plus dans le cas courant. Il reste
   pour les crans de zoom, où plus rien ne tient. */
.avail tbody th { white-space: nowrap; }

/* `hyphens: manual` CONTRE l'`auto` hérité de la page : la césure
   automatique est le bon réglage pour de la prose et le mauvais pour
   trois étiquettes de trois syllabes — dans une colonne étroite elle
   rendait « MA- / TIN » et « SOI- / RÉE ». En manuel, seul « APRÈS-MIDI »
   se coupe, à son vrai trait d'union. */
.avail thead th { color: var(--c-text-mid); hyphens: manual; }

/* Le sous-titre d'heures sous le nom de la période : en bloc, sinon
   « Matin 8 h – 12 h » compte comme un seul mot long et impose sa largeur
   à la colonne entière. */
.avail thead th .muted { display: block; font-size: var(--mil-fs-0); font-weight: 400; }

/* LES TROIS COLONNES À LARGEUR ÉGALE dès 700 px (Eric, 2026-08-18 : « make
   calendar checkable boxes equal width ») : en largeur auto, chaque colonne
   prend celle de son en-tête — « Après-midi » plus large que « Matin » — et
   des tuiles inégales se lisent comme des poids inégaux. En rem, pour suivre
   le zoom texte. PAS sous 700 : la table y compresse ses colonnes pour tenir
   dans le panneau d'un téléphone (la mesure du 293 px, plus haut), et une
   largeur imposée la ferait défiler. */
@media (min-width: 700px) {
  .avail thead th { width: 7rem; }
}

.avail tbody tr + tr { border-top: 1px solid var(--c-border); }

/* La case remplit sa cellule : la cible est le RECTANGLE, pas les 18 px
   du carré. Sur un tableau, viser la case elle-même au pouce est le
   genre de détail qui décide si la grille se remplit ou pas.

   ── ET C'EST UNE TUILE DEPUIS LE 2026-08-14 ──────────────────────────
   La cible faisait déjà 44 px ; rien ne le DISAIT. Un rectangle
   transparent autour d'une case de 18 px se lit comme une case de
   18 px, donc on visait la case — et on la ratait une fois sur trois au
   pouce. La tuile n'agrandit pas la cible, elle la montre.

   Elle sert deux fois : cochée, elle se relit en diagonale (« mes
   mardis sont pris ») là où vingt et une petites cases se lisent une
   par une. C'est la seule grille de l'app où l'on coche vite et où l'on
   ne revérifie pas. */
/* 48 px depuis le 2026-08-18 (la tranche « colonnes ») : c'est la grille
   qu'on remplit le plus vite, donc celle où quatre pixels de cible se
   sentent le plus. Le coût — 28 px sur la hauteur des sept rangées — se paye
   sur l'écran qui vient d'en gagner mille. */
/* LA TUILE VIDE SE DESSINE (Eric, 2026-08-18 : « needs a box when
   unselected ») : tant que la case de 24 px vivait dedans, c'était elle qui
   dessinait la place à cocher, et le bord de la tuile pouvait rester
   transparent. La case partie, une tuile vide sans bord était un trou dans la
   rangée — rien ne disait « ici, ça se coche ». Même bord et même fond que la
   pastille non cochée : une seule grammaire pour « pas encore choisi ». */
.avail-cell {
  position: relative;  /* la case hors flux et le libellé muet restent CHEZ ELLE */
  justify-content: center;
  min-height: 48px;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
}

/* LA CASE NE SE VOIT PLUS — LE CROCHET LA REMPLACE (Eric, 2026-08-18 : une
   case dans une tuile allumée disait deux fois la même chose). L'`input`
   reste un vrai `<input>`, seulement hors flux — le patron des pastilles :
   le clavier le trouve, le lecteur d'écran l'annonce « case à cocher,
   cochée ». */
.avail-cell input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* LE CROCHET EST LE FRÈRE DE LA CASE (`input:checked + span`), PAS UN
   `:has()` : c'est lui qui porte l'état quand le fond de la tuile n'est pas
   peint — un navigateur sans `:has()`, une impression, un mode à contraste
   forcé. Un état qui ne vit que dans une couleur d'arrière-plan est un état
   qui disparaît (règle 6). Réservé en opacité nulle plutôt qu'absent, comme
   tout signe qui apparaît : une tuile qui gagne un nœud changerait de
   hauteur. */
.avail-coche {
  font-size: var(--mil-fs-4);
  font-weight: 700;
  color: var(--c-accent-dark);
  opacity: 0;
}

.avail-cell input:checked + .avail-coche { opacity: 1; }

/* `:has()` PLUTÔT QU'UNE CLASSE POSÉE AU SCRIPT : la tuile suit la case
   même quand c'est le clavier qui la coche, et la page n'a toujours pas
   besoin d'une ligne de JavaScript. C'est déjà le sélecteur qui décide
   de `--toast-bottom` plus haut dans cette feuille. */
.avail-cell:has(input:checked) {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}

.avail-cell:has(input:focus-visible) {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* La gouttière de la cellule tombe à l'espace d'une tuile : les 12 px de
   `--space-3` étaient l'air autour d'une case de 18 px, et ils
   sépareraient maintenant deux tuiles qui doivent se lire comme une
   grille. */
.avail td { padding: 2px; }

/* SOUS 600 px, LA COLONNE DES JOURS REND SES CAPITALES.

   C'est le seul endroit de la feuille où une casse se défait, et c'est un
   arbitrage mesuré, pas une préférence. À 375 px, « MERCREDI » en petites
   capitales espacées occupe 94 px sur les 293 du panneau : la grille
   déborde alors de son conteneur, et une colonne « Soirée » hors écran
   sur un téléphone ne se découvre que par accident.

   Les trois autres façons de gagner ces pixels coûtaient plus cher :
   couper les mots (« MA- / TIN », vu à l'écran, se lit comme un bogue),
   abréger les jours (« Mer. » demande un second jeu de libellés que rien
   ne tient en accord avec le premier), ou retirer les heures des en-têtes
   (elles sont la seule chose qui dise ce qu'est une « soirée »).

   599 ET NON 700, qui est pourtant le seuil du reste de la feuille : 700
   est celui du CHROME — la largeur où le rail passe en barre du bas. Ici
   la contrainte est la table dans son panneau, et elle tient encore
   largement à 650 px. Aligner les deux nombres déferait les capitales sur
   une tablette qui a la place, c'est-à-dire changerait un style sans
   raison. Un seuil se justifie par ce qu'il mesure. */
@media (max-width: 599px) {
  .avail tbody th {
    text-transform: none;
    letter-spacing: normal;
    padding-left: 0;
    padding-right: var(--space-2);
  }
}

/* ---------------------------------------------------------------
   L'escale de bienvenue
   ---------------------------------------------------------------
   Une colonne étroite malgré le gabarit large : c'est du TEXTE qu'on
   lit une fois, pas un tableau qu'on parcourt. Au-delà d'environ
   65 caractères par ligne, l'œil perd le début de la suivante — et
   c'est la première page que l'app adresse à quelqu'un.
   --------------------------------------------------------------- */

/* Centrée : pinnée à gauche dans une grille pleine largeur, la colonne de
   lecture laisse un grand vide à droite et l'écran a l'air cassé plutôt que
   sobre. */
.bv { max-width: 60ch; margin-inline: auto; }

/* 17 → 18 px, le cran 4, qui s'appelle « chapeau ». Le chapeau, c'est lui.
   L'arbitrage se décidait de toute façon avec `body` : à 16 px de texte
   courant, un chapeau de 17 n'est plus un chapeau, c'est du texte courant qui a
   l'air d'avoir raté sa cible d'un pixel. Le cran 4 lui rend l'écart qu'il
   avait sur un corps de 15. */
.bv-lead {
  font-size: var(--mil-fs-4);
  color: var(--c-text-mid);
  margin: 0 0 var(--space-5);
}

/* Une remarque qui concerne CETTE personne, et pas la coop : accent doux,
   doublé d'une bordure — jamais la couleur seule (§0, règle 8). */
.bv-aside {
  border-left: 3px solid var(--c-accent);
  background: var(--c-accent-soft);
  padding: var(--space-3);
  margin: 0 0 var(--space-5);
  border-radius: 0 var(--radius) var(--radius) 0;
}

/* LE MENU DU LOGO — des paragraphes, et plus une liste (2026-08-16).

   `.bv-quoi` est partie avec la liste d'écrans qu'elle portait, et
   `.bv-thing-name`, `.bv-thing-what` et `.bv-list` étaient parties avec les
   cartes que la liste avait remplacées (2026-08-14). Trois dessins pour un
   contenu qui a fini par ne plus exister : la page n'énumère plus des écrans,
   elle apprend un geste.

   Un geste ne se met pas en `<dl>` ni en cartes — il s'écrit en phrases, dans
   la colonne de 60ch, à la couleur du texte courant. La seule règle qui reste
   est donc l'espacement entre elles ; le reste est le corps de la page, et
   c'est exactement ce qu'on veut de trois phrases qui se lisent d'affilée. */
.bv-menu { margin: 0 0 var(--space-4); }

/* LA MARQUE DE LA COOP, EN PLEIN TEXTE — le bouton du menu, montré là où la
   phrase en parle (Eric, 2026-08-16).

   ── DIMENSIONNÉE EN `em`, ET C'EST CE QUI LA TIENT DANS LA LIGNE ────────────
   1.6em plutôt que 26px : à 200 % de zoom texte, un `px` reste un timbre au
   milieu de mots devenus deux fois plus gros, et le sujet de la phrase cesse
   d'être ce qu'on voit en premier. Elle grandit donc avec ce qui l'entoure.

   `vertical-align: -.45em` et pas `middle` : `middle` aligne sur le milieu des
   MINUSCULES, ce qui fait flotter un carré d'1.6em au-dessus de la ligne de
   base. Le décalage négatif le pose dessus, où il a l'air d'un mot.

   ── ET ELLE POUSSE SA LIGNE ────────────────────────────────────────────────
   Un carré plus haut que l'interligne écarte SA ligne des voisines, et la
   phrase se met à respirer par le milieu. `line-height: 1.9` sur le paragraphe
   fait la place d'avance : toutes les lignes s'écartent un peu plutôt qu'une
   seule beaucoup. */
.bv-menu:has(.bv-marque) { line-height: 1.9; }

/* ── ET C'EST UNE TUILE, PAS UNE IMAGE POSÉE ────────────────────────────────
   Le logo nu sur le panneau est un petit dessin au milieu d'une phrase : exact,
   et ça ne ressemble à rien de cliquable. Le bouton réel est une tuile sur
   l'ardoise du rail, et c'est la TUILE qu'on cherche du regard en haut à
   gauche. L'enveloppe la reproduit — `--c-rail`, le même rayon — pour que la
   phrase montre l'objet et pas seulement ce qu'il y a dedans. Le monogramme
   tient dans la même, comme dans le rail.

   ── LE CRAN 0 EST CE QUI DIMENSIONNE LA TUILE, ET C'EST VOULU ──────────────
   `--mil-fs-0` n'est pas ici la taille d'un texte : c'est le monogramme du
   repli, et c'est SURTOUT l'unité dont tout le reste se déduit. Le cran porte
   déjà le multiplicateur du zoom texte (`calc(12px * var(--mil-fs))`), donc
   une tuile mesurée en `em` par-dessus grandit avec les mots qui l'entourent
   — ce qu'un `px` ne fait pas, et un timbre au milieu de mots deux fois plus
   gros cesse d'être le sujet de la phrase.

   Le rapport est celui du rail : `--mil-fs-5` dans 44 px, soit un peu moins de
   la moitié. Ici 12 px dans 24, et les initiales tombent au même endroit. */
.bv-marque {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--mil-fs-0);
  width: 2em;
  height: 2em;
  padding: .2em;
  vertical-align: -.6em;
  border-radius: .45em;
  background: var(--c-rail);
  color: var(--c-on-rail);
  font-weight: 700;
  line-height: 1;
}

.bv-marque img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: .3em;
  object-fit: cover;
}

/* ── LES DEUX PORTES ────────────────────────────────────────────────────────
   « Communauté » et « Outils », côte à côte, chez une coop qui a un Hub. La
   demande d'Eric (2026-08-14) dit « deux gros boutons », et gros est ici une
   contrainte de fond : ces deux-là ne se choisissent pas au libellé, ils se
   choisissent au dessin plus une phrase — on ne suppose pas la lecture.

   `auto-fit` et 15rem plutôt qu'un point de rupture : les deux boîtes passent
   l'une sous l'autre quand elles n'ont plus la place, ce qui arrive à 375px
   comme à 100 % de zoom dans une fenêtre étroite comme à 200 % de zoom texte
   sur un portable. Un `@media` aurait choisi UNE de ces trois causes. */
.bv-portes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-4);
  margin: 0 0 var(--space-3);
}

.bv-porte {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  width: 100%;
  padding: var(--space-4);
  text-align: left;
  text-decoration: none;
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--mil-fs-2);
}

/* `.panel a { color: var(--c-accent) }` a deux crans de spécificité et
   repeindrait la porte-lien en bleu, texte de détail compris. Même
   redéclaration que `.panel a.btn`, et pour la même raison. */
.panel a.bv-porte { color: var(--c-text); }

/* `button:hover` a un cran de plus que `.bv-porte` seule et reprendrait le
   fond : la porte doit donc redire le sien au survol. */
.bv-porte:hover { background: var(--c-hover-bg); }

/* CELLE OÙ L'ON EST DÉJÀ — bordure d'accent et fond doux, JAMAIS l'accent
   plein. Un rectangle bleu porterait ici quatre lignes de texte en blanc sur
   `--c-accent`, soit 3,4:1 : le rapport que `.btn-primary` peut se permettre
   sur un libellé court et gras, et pas sur une phrase de 13px. L'emphase se
   fait donc par la bordure et le fond, qui ne portent aucun texte.

   L'`inset` double le trait sans déplacer d'un pixel ce qu'il y a dedans : une
   bordure de 2px sur une seule des deux boîtes les décalerait l'une par
   rapport à l'autre, ce qui se voit tout de suite sur une grille. */
.bv-porte.is-here {
  border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
  background: var(--c-accent-soft);
}

.bv-porte.is-here:hover { background: var(--c-accent-soft); }

/* Les dessins du catalogue partagé, à 32px : assez gros pour se reconnaître
   avant d'être lus, assez petits pour ne pas devenir l'objet de la boîte. */
.bv-porte-dessin {
  line-height: 0;
  color: var(--c-accent);
  margin-bottom: var(--space-1);
}

.bv-porte-dessin svg { width: 32px; height: 32px; }

.bv-porte-nom {
  font-size: var(--mil-fs-4);
  font-weight: 600;
}

/* « Ce qui se fait » / « Ce qui se dit » — la ligne qui fait le choix, donc
   elle est plus lisible que le détail qui la suit. */
.bv-porte-quoi { font-weight: 500; }

.bv-porte-detail {
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* Le geste, poussé en bas de la boîte : `margin-top: auto` aligne les deux
   verbes sur la même ligne même quand les deux détails n'ont pas le même
   nombre de lignes — sinon « Aller voir » flotte au milieu de sa boîte à côté
   d'un « Commencer ici » collé au bas de la sienne. */
.bv-porte-geste {
  margin-top: auto;
  padding-top: var(--space-3);
  font-weight: 600;
  color: var(--c-accent);
}

/* Ce qui rend les deux portes sans danger — le même compte des deux côtés, et
   le logo pour revenir. Elle s'appelait `.bv-thing-how` : le dernier reste
   d'une famille `.bv-thing-*` partie avec les cartes, et un préfixe orphelin
   est ce qui fait qu'on cherche deux fois où vit une règle. */
.bv-portes-note {
  margin: var(--space-3) 0 0;
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

.bv-actions { margin-top: var(--space-6); }

/* Un SEUL primaire par zone d'action (§4) : ici, il n'y a qu'un geste. */
.bv-start {
  min-height: 44px;
  padding: var(--space-3) var(--space-6);
  font-size: var(--mil-fs-3);
}

.bv-foot {
  margin: var(--space-5) 0 0;
  font-size: var(--mil-fs-1);
  color: var(--c-text-muted);
}

/* Un contrôle qui occupe SA ligne, à la différence des contrôles segmentés qui
   se partagent la leur : le bouton du Mode super-admin, et depuis le
   2026-08-10 le menu déroulant de la personnification.

   Le sélecteur a perdu son `.seg-opt` en gagnant ce second point d'usage : un
   `<select>` n'est pas une option de segment, et écrire `select.is-wide` à
   côté aurait fait deux règles pour « prends toute la largeur ». */
.is-wide { width: 100%; }

/* LE MENU DÉROULANT DE LA PERSONNIFICATION ET SON BOUTON S'EMPILENT.

   Côte à côte dans un panneau de 260 px, le menu recevrait moins de largeur que
   le plus court des noms de la coop — et c'est précisément le contrôle dont la
   largeur naturelle est celle de sa PLUS LONGUE option (cf. le commentaire des
   contrôles, plus haut : mesuré à 412 px sur /departs). */
.prefs-row select + button { margin-top: var(--space-2); }

/* LA PHRASE QUI DIT CE QUE LE CONTRÔLE VA FAIRE — sous le Mode super-admin et
   sous la personnification, les deux gestes du panneau qui changent ce que
   l'app montre à quelqu'un d'autre que soi.

   `--c-text-mid` et non `--c-text-muted` (2026-08-14) : 2,64:1 en clair, 4,23
   en sombre, sous le plancher AA du §5 ; le gris moyen rend 6,05 et 7,38.

   C'est de l'aide, donc c'est bien du second rang — mais le second rang se dit
   par le cran 0 et par la place sous le bouton, pas en rendant la phrase
   difficile à lire. Une aide qu'on renonce à déchiffrer ne range pas son
   contrôle en arrière : elle le laisse sans mode d'emploi, et ces deux-là sont
   précisément ceux qu'on ne veut pas voir actionnés au jugé. */
.prefs-help {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
}

/* ==========================================================================
   LA RANGÉE D'APPS VOISINES — l'aperçu de /applications
   ==========================================================================
   Ce n'est PAS le menu — c'est son APERÇU. Le menu appartient au Hub (actif
   partagé `mil-destinations`, demande 7 appliquée le 2026-08-11) et porte ses
   propres règles, sous `.mil-dest-apps`.

   Les deux existent parce qu'ils ne montrent pas la même chose : le menu
   n'affiche à chacun que les tuiles dont il peut ouvrir la porte, l'aperçu les
   montre TOUTES — c'est ce que la personne qui branche une app a besoin de
   voir. Et l'écran d'administration ne charge pas la feuille du composant
   partagé pour trois tuiles. */

.app-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0 var(--space-4);
  padding: 0;
  list-style: none;
}

/* Une tuile est une CIBLE AU POUCE avant d'être une vignette : 64 px de large
   tient les deux mots les plus longs de l'échelle (« Infolettre ») sans les
   couper, et 44 px de haut est le plancher tactile du reste de l'app. */
.app-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  min-width: 64px;
  min-height: 44px;
  padding: var(--space-2);
  border-radius: var(--radius);
  background: var(--c-surface-2);
}

/* Le mot sous la marque : petit, mais jamais en dessous du cran 0 de
   l'échelle — c'est lui qui porte le sens quand le logo ne dit rien
   (fournisseur « autre »), et il doit suivre le zoom texte comme le reste. */
.app-tile-label {
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
  text-align: center;
}

/* La marque garde sa taille quand le texte grossit : c'est un dessin, pas un
   mot. La laisser suivre `--mil-fs` la ferait déborder de sa tuile à 200 %,
   et un logo à 52 px n'aide personne à le reconnaître plus vite. */
.app-mark {
  display: block;
  width: 26px;
  height: 26px;
  flex: none;
}

/* ==========================================================================
   MA PHOTO — l'aperçu et les deux gestes, sur la fiche
   ==========================================================================
   L'aperçu n'est pas décoratif : il montre ce que les AUTRES voient, y compris
   quand ce n'est pas nous qui l'avons déposé (l'image du Hub, celle du compte
   de connexion). Sans lui, on règle sa photo à l'aveugle — et la phrase en
   dessous, qui dit d'où elle vient, n'aurait rien à commenter. */

.photo-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin: var(--space-3) 0;
}

/* 64 px et pas 32 : c'est ici qu'on JUGE le cadrage, alors que le rail ne fait
   que rappeler qui on est. Le repli texte y tient un numéro à quatre chiffres
   sans se serrer. */
.photo-apercu {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-pill);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-size: var(--mil-fs-2);
  font-weight: 700;
  overflow: hidden;
}

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

/* LE VISAGE EST LA PORTE DE SA PHOTO (Eric, 2026-08-16).

   « Remove the photo picker section and integrate it within the top profile
   image: click on image, opens modal. » Le lien ENVELOPPE l'aperçu au lieu de
   le remplacer : le rond écrête son image (`overflow: hidden`), et l'appareil
   photo posé dans son coin serait coupé net s'il vivait dedans.

   ── L'APPAREIL PHOTO N'EST PAS UNE DÉCORATION ──────────────────────────────
   C'est la seule chose qui dit qu'une photo ronde s'ouvre. Un visage sans
   signe, dans un en-tête où rien d'autre ne se clique, ne se distingue pas d'une
   vignette — et le geste qu'Eric demande serait alors à deviner. Le dessin est
   monochrome comme toute icône d'action de l'app, et `aria-label` porte le mot
   pour qui ne le voit pas.

   ── LE DÉBORD EST VOULU, ET IL EST BORNÉ ───────────────────────────────────
   Le signe déborde du rond de 2 px pour rester lisible à 22 px de dessin ; le
   lien, lui, n'écrête rien. Sa pastille reprend le fond du panneau, sinon le
   trait se perd sur une photo claire. */
.photo-porte {
  position: relative;
  display: inline-flex;
  flex: none;
  border-radius: var(--radius-pill);
  text-decoration: none;
}

.photo-porte-signe {
  position: absolute;
  right: -2px;
  bottom: -2px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text-mid);
}

/* Le dessin est rangé dans une pastille de 28 px : les 22 px du magasin
   d'icônes y déborderaient. C'est la seule règle de l'app qui redimensionne une
   icône, et elle vise `svg` plutôt qu'une classe parce que le partiel n'en pose
   aucune. */
.photo-porte-signe svg { width: 16px; height: 16px; }

.photo-porte:hover .photo-porte-signe {
  border-color: var(--c-border-strong);
  color: var(--c-text);
}

/* L'anneau se pose sur le LIEN, pas sur le rond : il doit entourer la cible
   entière, signe compris. `offset` pour qu'il ne mange pas le bord de l'image. */
.photo-porte:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 3px;
}

/* L'APERÇU DE LA SECTION PHOTO, EN GRAND — 96 px contre les 64 de l'en-tête.
   C'est le seul endroit de l'app où l'on JUGE un cadrage, et un visage de 64 px
   ne dit pas si la photo coupe le menton. Le repli texte y tient un numéro à
   quatre chiffres au cran d'accessibilité maximal. */
.photo-apercu-lg {
  width: 96px;
  height: 96px;
  font-size: var(--mil-fs-4);
}

/* LE SÉLECTEUR DE FICHIER — habillé, et c'est la seule façon de le faire.

   Le bouton que dessine le navigateur porte SA langue, pas celle de l'app : sur
   un macOS en anglais, la fiche d'une coop francophone affichait « Choose File
   / No file chosen » au milieu de ses phrases. `::file-selector-button` est le
   seul point d'entrée sur ce dessin ; le mot, lui, reste celui du système et ne
   se remplace pas — d'où le bouton « Déposer » à côté, qui est celui qui dit ce
   qu'on fait.

   44 px, comme tout ce qui se touche (§0, règle 9) : c'est un contrôle qu'on
   vise une fois par an, donc jamais avec application. */
.photo-actions input[type="file"] {
  max-width: 100%;
  font-size: var(--mil-fs-1);
}

.photo-actions input[type="file"]::file-selector-button {
  min-height: 44px;
  margin-right: var(--space-3);
  padding: 0 var(--space-4);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface-2);
  color: var(--c-text);
  font-size: var(--mil-fs-1);
  cursor: pointer;
}

.photo-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* ------------------------------------------------------------------
   LE TABLEAU DE BORD DE L'ACCUEIL (2026-08-12)
   ------------------------------------------------------------------ */

/* L'EN-TÊTE — LA SALUTATION, LA PENSÉE, LE BOUTON (maquette du 2026-08-18).

   Trois choses, trois places : « Bonjour Éric » à gauche, la pensée du jour au
   centre, le bouton de ma fiche à droite. Le dessin d'Eric défait deux
   décisions du 2026-08-16 en connaissance de cause — le bouton quitte le
   groupe de la salutation pour le coin opposé (à l'autre bout de la bande, il
   n'est plus « à côté du h1 », donc plus à son rang), et la boîte des comités
   quitte la bande pour sa propre rangée, dessous.

   `center` et non `flex-start` : un `h1` de deux lignes, une citation d'une ou
   deux, un bouton de 36 px — alignés par le haut, trois départs sur trois
   lignes différentes. Centrés, la bande se lit comme une bande.

   ── L'HISTORIQUE DE LA BANDE, EN TROIS MAQUETTES ───────────────────────────
   Le bilan « ce qui est à moi » (2026-08-13), puis deux adresses — le bouton
   sous la salutation, la boîte des comités en face (2026-08-15 et 16) —, puis
   ceci. `.dash-tete-mot`, `.dash-tete-comites` et `.dash-tete-titre` sont
   partis avec la troisième, comme `.moi-bilan` avec la deuxième : une classe
   qui survit à sa vue est un dessin que la prochaine maquette croira
   disponible. */
.dash-tete {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
}

.dash-tete > * { min-width: 0; }
.dash-tete > h1 { margin-bottom: 0; }

/* LA PENSÉE DU JOUR — la voix de `mil-membres`, à la même place relative :
   le centre de ce que l'écran a comme bande du haut (là-bas la barre de nav,
   ici l'en-tête du tableau de bord).

   `flex: 1` est ce qui la CENTRE : la salutation et le bouton prennent leur
   largeur de texte, elle prend tout l'entre-deux et se centre dedans. Pas
   d'`absolute` comme là-bas — la barre de nav avait des largeurs imprévisibles
   de chaque côté, l'en-tête n'a que deux blocs courts, et un `absolute` ici
   pourrait la faire chevaucher la salutation au premier prénom long.

   Italique, sourde, deux lignes au plus (`line-clamp` — la plus longue des
   quarante-six fait trois lignes sur un écran étroit) : elle se lit si on la
   regarde et ne dispute rien à personne. `flex-basis: 16rem` est le seuil du
   repli — plus étroit, elle passerait sous la salutation et pousserait le
   tableau ; sous 700 px elle disparaît plutôt, comme là-bas sous 1100. */
.dash-pensee {
  flex: 1 1 16rem;
  /* `relative` avec l'`overflow` : la règle de la suite — une zone qui écrête
     doit CONTENIR, sinon un descendant en `absolute` échappe à l'écrêtage et
     rallonge la page d'un pixel invisible. */
  position: relative;
  margin: 0;
  text-align: center;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--mil-fs-1);
  font-style: italic;
  line-height: 1.3;
  color: var(--c-text-muted);
}

.dash-pensee-src { font-style: normal; opacity: 0.8; }

@media (max-width: 700px) {
  .dash-pensee { display: none; }
}

/* Le bouton ne se coupe pas : « Compléter ma fiche » sur deux lignes au bout
   d'une bande se lit comme un bouton cassé. */
.dash-tete-fiche { white-space: nowrap; }

/* LA RANGÉE DES COMITÉS — sous la bande, en pleine largeur (2026-08-18).

   Sans boîte et sans fond : la boîte se justifiait à droite d'un `h1`, où un
   bloc sans cadre se lisait comme la suite du chapeau. Sur sa propre ligne,
   une rangée de pastilles se comprend sans meuble autour — et le fond sourd
   aurait fait une deuxième bande au-dessus d'un tableau qui en a déjà une.

   `baseline` plutôt que `center` : « Tous les comités » est du texte nu à côté
   d'objets à bord — centré, il flotte ; sur la ligne de base des pastilles, il
   se lit comme la fin de leur phrase. */
.dash-comites {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-4);
}

.dash-comites > * { min-width: 0; }
.dash-comites > p { margin: 0; }

.dash-comites > .comites-fil { flex: 1 1 auto; }

/* La porte du bout : poussée au bord droit même quand les pastilles sont
   courtes — c'est sa place de fin de rangée qui la distingue du fil. */
.dash-comites-tous {
  margin-left: auto;
  white-space: nowrap;
  font-size: var(--mil-fs-1);
}

/* LES PASTILLES EN FIL, PAS EN COLONNE. Eric siège à quatorze comités : une
   liste verticale ferait quatorze lignes en haut de l'accueil. En fil, elles
   remplissent la largeur de la rangée — deux lignes dans le pire cas réel.

   `gap` et non des virgules : ce sont des cibles, et deux cibles séparées d'une
   virgule se touchent — au doigt, on en attrape une pour l'autre. Le `row-gap`
   est ce qui empêche deux rangées de pastilles de se coller. */
.comites-fil {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.comites-fil > li { min-width: 0; }

/* LA PASTILLE D'UN COMITÉ — la forme de `.pill`, le métier d'un lien.

   ── POURQUOI ELLE N'EST PAS `.pill` TOUT COURT ─────────────────────────────
   `.pill` étiquette un ÉTAT qu'on ne clique pas : un rang (« Coordo »), une
   mise en garde (« Maquette »). Celle-ci est une porte. Elle en emprunte la
   silhouette — le rayon plein, la bordure, le mot qui ne se coupe pas — et
   change les trois choses qui distinguent un objet qu'on prend d'une étiquette
   qu'on lit : la couleur d'accent, le fond au survol, et 32 px de haut, qui
   est ce qu'un doigt attrape dans un en-tête (les 44 px sont pour un geste, et
   six pastilles de 44 feraient une boîte de trois centimètres).

   `.panel a` peint déjà le texte en accent avec deux crans de spécificité —
   on ne le redéclare donc pas ici, et on lui retire seulement le SOULIGNÉ :
   une pastille soulignée a deux traits qui disent la même chose, dont un qui
   coupe le mot. */
.comites-fil .comite-pastille {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  font-size: var(--mil-fs-1);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--t-fast), border-color var(--t-fast);
}

.comites-fil .comite-pastille:hover,
.comites-fil .comite-pastille:focus-visible {
  background: var(--c-hover-bg);
  border-color: var(--c-accent);
}

/* LE RESTE EN CREUX — « et 8 autres » ne nomme pas un comité, donc il ne doit
   pas se lire comme un quinzième nom. Le trait pointillé et l'encre sourde
   suffisent : la même cible, le même geste, un rang de moins. */
.panel .comites-fil .comite-pastille.is-reste {
  border-style: dashed;
  color: var(--c-text-mid);
}

/* LA GRILLE A QUATRE COLONNES, ET C'EST LE GABARIT QUI LES COMPOSE.

   Quatre depuis le 2026-08-18 — « Ce qui se passe » ouvre l'écran, les
   annonces glissent en deuxième. Rien ne change ici : les colonnes sont des
   `@include` dans le gabarit, et `auto-fit` replie la quatrième puis la
   troisième quand la largeur manque, dans l'ordre du document.

   ── CE QU'ELLE ÉTAIT, ET CE QUE LA MAQUETTE A TRANCHÉ ──────────────────────
   `auto-fit` sur les CARTES : six tuiles de même rang, chacune tombant d'elle-
   même quand la largeur manquait. La règle écrite ici s'y opposait — « deux
   colonnes déclarées demanderaient de choisir ce qui tombe dans laquelle, puis
   de réordonner sous le seuil, donc de tenir deux ordres » — et elle tenait
   tant qu'aucune carte n'en commandait une autre.

   Le dessin d'Eric du 2026-08-15 dit l'inverse : les cartes vont par PAIRES
   (ce que j'ai emprunté avec ce qui est cassé, le calendrier avec les
   courriels), et une paire est une colonne. Ce qui rendait la règle d'avant
   nécessaire n'a pas disparu, il a changé de porteur : `auto-fit` reste, sur
   les COLONNES au lieu des cartes. Une colonne descend entière quand la largeur
   manque, l'ordre du document reste l'ordre de lecture à toutes les largeurs,
   et il n'y a toujours qu'un seul ordre à tenir — sauf qu'il porte maintenant
   des groupes plutôt qu'un tas.

   ── ET LES GROUPES SONT DANS LE BLADE ──────────────────────────────────────
   Pas de `grid-column` posé carte par carte : déplacer une carte se ferait
   alors dans deux fichiers, et la CSS déciderait d'un groupement qu'on ne peut
   pas lire dans le gabarit. Une `<div class="dash-col">` par colonne coûte
   trois éléments et rend le regroupement évident là où on l'édite.

   320px est mesuré et non choisi : c'est la largeur sous laquelle une rangée de
   `.list-row` (titre + bouton « Ça m'intéresse ») se replie en deux lignes. Une
   colonne plus étroite ferait passer chaque annonce sur trois lignes, et le
   tableau perdrait ce pour quoi il existe — se lire d'un coup d'œil.

   `align-items: start` : sans lui, chaque colonne s'étire à la hauteur de la
   plus haute, et la colonne de deux cartes courtes se retrouve avec vingt
   centimètres de vide sous la seconde. */
.dash {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  align-items: start;
  gap: var(--space-4);
  margin: var(--space-5) 0;
}

.dash > * { min-width: 0; }

/* UNE COLONNE — une pile de cartes, à la même gouttière que la grille, sinon
   l'espace entre deux cartes d'une même colonne ne ressemble pas à l'espace
   entre deux colonnes et le groupement se voit deux fois.

   `align-content: start` : une grille implicite répartit ses rangées sur la
   hauteur disponible ; sans lui, deux cartes dans une colonne haute de trois
   s'écarteraient l'une de l'autre pour remplir le vide. */
.dash-col {
  display: grid;
  align-content: start;
  gap: var(--space-4);
}

.dash-col > * { min-width: 0; }

/* `.dash-wide` EST PARTIE AVEC SA SEULE UTILISATRICE (2026-08-13). Elle donnait
   la pleine largeur à la carte des engagements — les annonces l'avaient perdue
   la veille — et cette carte n'existe plus : le bilan est monté dans l'en-tête,
   puis en est reparti avec la maquette suivante. Une grille où toutes les
   colonnes ont le même rang n'a pas besoin d'une exception. */

/* `.card` pose une gouttière verticale pour les cartes empilées d'un écran ;
   dans une grille, c'est `gap` qui l'a déjà posée, et la garder donnerait deux
   gouttières superposées entre deux rangées de cartes. */
.dash-card { margin: 0; }

/* L'EN-TÊTE DE TUILE EST UNE BARRE, ET C'EST CE QUI FAIT LIRE LE TABLEAU.

   ── LE DÉFAUT QU'ELLE CORRIGE ──────────────────────────────────────────────
   Un titre de carte est à 18 px / 600 ; le titre d'une RANGÉE dedans est un
   `<strong>` à 14. Sur un écran unique, l'écart suffit — il n'y a qu'un titre.
   Sur sept tuiles côte à côte, l'œil compte vingt-cinq gras de tailles voisines
   et n'a plus de point d'entrée : on lit le tableau ligne à ligne au lieu de
   choisir la tuile qui nous concerne. C'est exactement ce qu'un tableau de bord
   ne doit pas demander.

   ── POURQUOI UNE BARRE ET PAS SEULEMENT DU PLUS GROS ───────────────────────
   Grossir le titre le met en concurrence avec le `h1` de la page ; le fond, lui,
   ne se compare à rien — il DÉLIMITE. Chaque tuile gagne un bord haut qui dit
   où elle commence, ce qu'une grille sans lignes ne dit pas. Le jeton employé
   s'appelle `--c-section-bar` et il existait déjà pour ça.

   Les marges négatives valent le remplissage de `.card` : la barre va d'un bord
   à l'autre, sinon c'est une bande flottante dans une carte, ce qui ajoute un
   troisième rectangle plutôt qu'un repère. Le rayon du haut suit celui de la
   carte, faute de quoi le coin dépasse de deux pixels — visible, et seulement
   sur les écrans de quelqu'un d'autre. */
.dash-card > .card-head {
  margin: calc(var(--space-4) * -1) calc(var(--space-4) * -1) var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--c-section-bar);
  border-bottom: 1px solid var(--c-border);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* 20 px / 700 : au-dessus des rangées (14) et du chiffre de réponse (18), sous
   le `h1` de la page (26). Trois crans, donc trois rangs — et le nom de la
   tuile est le premier des trois qu'on lit. */
.dash-card .card-title {
  font-size: var(--mil-fs-5);
  font-weight: 700;
}

/* Le dessin de la barre (2026-08-18) : au cran du texte (20 px), pas aux 22 du
   rail — ici l'icône est la vignette du mot, pas une cible. `currentColor`
   fait le reste : elle suit le titre, y compris dans la barre teintée. */
.dash-card .card-title svg {
  width: calc(20px * var(--mil-fs, 1));
  height: calc(20px * var(--mil-fs, 1));
  flex-shrink: 0;
}

/* LA RÉPONSE EN UN COUP D'ŒIL — le chiffre ou la phrase qui dit tout de la
   carte. Une seule par carte : deux « figures » de même taille et la carte
   n'a plus de sujet. Elle n'est pas en gras EN PLUS d'être grande, ce qui
   ferait crier trois cartes à la fois. */
.dash-figure {
  margin: 0 0 var(--space-2);
  font-size: var(--mil-fs-4);
  color: var(--c-text);
}

/* `.dash-meter` EST PARTIE AVEC LA TUILE DU PARCOURS (2026-08-15). C'était la
   jauge des sept sections de la fiche ; `/profil` a la sienne (`data-board-
   meter`), plus riche, et il ne reste ici qu'un bouton vers lui. */

/* UNE LISTE DE CARTE — plus serrée qu'une `.rows`, parce qu'elle ne porte pas
   de geste à droite : ce sont des choses à LIRE. Les puces partent (elles
   ajoutent une marge que la carte a déjà) mais chaque élément garde sa ligne
   séparée, sinon trois items se lisent comme un paragraphe. */
.dash-list {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.dash-list > li + li {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--c-border);
}

/* L'AGENDA — on lit un calendrier en balayant la marge gauche, donc la date
   est une COLONNE et pas le début d'une phrase. Largeur fixe pour que les
   quatre dates s'alignent : « Mar 14 » et « Ven 3 » n'ont pas la même largeur,
   et deux titres qui ne commencent pas au même endroit annulent le balayage. */
.dash-agenda {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.dash-agenda > li {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

.dash-agenda > li + li {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--c-border);
}

.dash-agenda > li > * { min-width: 0; }

.dash-when {
  flex: 0 0 auto;
  width: 5.5em;
  text-transform: capitalize;
}

.dash-when-day { display: block; font-weight: 600; }
.dash-when-hour { display: block; font-size: var(--mil-fs-0); }

/* `.dash-more` EST PARTIE LE 2026-08-13, avec la carte des engagements. Elle
   habillait le tiroir des rangées au-delà de la cinquième — « cette carte EST
   la liste, il n'y a pas d'écran où envoyer quelqu'un ». Il y a maintenant un
   endroit où l'envoyer, et ce n'est toujours pas un écran : la modale porte
   tout, donc il n'y a plus de reste à cacher. */

/* LA PASTILLE « MAQUETTE » — trois cartes affichent des données inventées, et
   c'est un AVERTISSEMENT, pas une étiquette neutre : quelqu'un qui croit une
   date d'assemblée générale se déplace un mardi soir pour rien. D'où les
   jetons d'alerte plutôt qu'un gris discret, qui se serait lu comme une
   décoration. */
.pill.is-maquette {
  border-color: var(--c-warning);
  color: var(--c-warning-text);
  background: var(--c-warning-soft);
}

/* `.parcours-*` EST PARTI AVEC LA TUILE DU PARCOURS (2026-08-15). Cinq règles
   dessinaient les sept sections de la fiche — le crochet, le nom, le lien qui
   reste — sur une carte que la maquette d'Eric remplace par un bouton vers
   `/profil`, où le même tableau vit en plus riche. Une classe qui survit à sa
   vue est un dessin que la prochaine maquette croira disponible. */

/* LE MENU DES PORTES — « Consulter », quand mes comités ont plusieurs comptes.

   ── LE MÊME REFUS DE FLOTTER QUE `.card-menu` ──────────────────────────────
   Aucun positionnement : ouvert, le panneau prend la largeur de la carte et
   pousse ce qui suit. Un panneau ancré sur un bouton qui se replie sort de
   l'écran d'un côté ou de l'autre selon la carte — c'est la panne que le rail
   a déjà payée, en plus difficile à borner ici, où la tuile fait 320 px.

   ── LE `<summary>` EST UN BOUTON, ET IL FAUT LE LUI DIRE TROIS FOIS ────────
   `.btn` lui donne la boîte, mais un `<summary>` garde son triangle par défaut
   et ne prend pas le curseur d'un bouton. Les trois lignes ci-dessous sont la
   même redéclaration que `.head-menu > summary` et `.card-menu > summary` — un
   `<summary>` n'est pas un `<button>`, et une classe ne gagne que sur ce
   qu'elle réécrit. `inline-flex` avec, sinon le bouton prend la largeur de la
   carte : c'est un geste, pas une bande. */
/* La MÊME gouttière que `.card-actions`, qui porte le bouton du cas à une
   seule porte : sans elle, le bas de la carte sauterait de trois pixels selon
   qu'une coop a branché un compte ou deux. */
.porte-menu { margin-top: var(--space-3); }

.porte-menu > summary {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  list-style: none;
  cursor: pointer;
}

.porte-menu > summary::-webkit-details-marker { display: none; }
.porte-menu > summary:hover { background: var(--c-hover-bg); }
.porte-menu[open] > summary { background: var(--c-hover-bg); }

/* LES LIGNES DU MENU — un logo, un comité, un compte.

   La cible fait 44 px de haut (règle 9 : c'est un geste membre, au pouce), et
   la marque est en tête comme sur les liens pratiques d'un comité : à cette
   taille, c'est le dessin qui fait reconnaître la porte avant qu'on lise le
   mot. `flex: 0 0 auto` sur le logo, sans quoi il se comprime quand un libellé
   de comité est long. */
.porte-liste {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.porte-liste > li + li { margin-top: var(--space-1); }

.porte-liste a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}

.porte-liste a:hover { background: var(--c-hover-bg); }
.porte-liste a > * { min-width: 0; }
.porte-liste .app-mark { flex: 0 0 auto; }

/* ------------------------------------------------------------------
   LE RAIL SUR FOND ACCENT (2026-08-12)
   ------------------------------------------------------------------ */

/* Demandé par Eric, et refusé deux fois avant lui pour une raison qui tenait :
   le bouton des DESTINATIONS, qui vit dans le rail, est dessiné en accent par
   l'actif partagé du Hub — halo `--mild-glow` au survol, fond `--mild-accent`
   à l'ouverture. Sur un rail accent, un halo accent ne se voit pas et un fond
   accent n'existe pas : le bouton reste cliquable et n'annonce plus rien.
   C'est la demande 6, déposée le 2026-08-10 et TOUJOURS SANS RÉPONSE.

   ── CE QUI DÉBLOQUE, ET POURQUOI ÇA NE TOUCHE PAS L'ACTIF PARTAGÉ ──────────
   La demande visait des valeurs qu'on croyait écrites en dur. Elles ne le sont
   pas : `--mild-accent` et `--mild-glow` sont des VARIABLES du composant, avec
   un repli. Les reposer sur le seul `.mil-dest-btn` change donc ses deux états
   et rien d'autre — le panneau qui s'ouvre garde sa palette, puisqu'il n'est
   pas un descendant du bouton. Le fichier partagé n'est pas modifié d'un octet,
   ce que `test_l_actif_partage_est_une_copie_conforme` continue de vérifier.

   La demande 6 devient donc facultative. Elle reste utile — un jeton nommé vaut
   mieux qu'un détournement de deux variables internes, et le Hub peut renommer
   les siennes sans nous prévenir — mais elle ne bloque plus rien.

   ── AUCUNE COULEUR N'EST ÉCRITE ICI, ET C'EST CE QUI FAIT SUIVRE LES THÈMES ─
   Tout dérive de `--c-rail` et `--c-on-rail` par `color-mix`. Les deux sont des
   jetons de marque, audités en paire par `BrandTokensTest`, et ils BASCULENT
   ensemble : la dalle fonce en sombre et l'encre posée dessus s'éclaircit. Le
   rail suit donc le thème et la palette daltonien sans une ligne de plus.

   ── ILS ONT REMPLACÉ `--c-accent` / `--c-on-accent` (2026-08-12) ────────────
   Ce pavé disait « les deux basculent ensemble : en sombre l'accent
   s'éclaircit et l'encre fonce », et c'était vrai — mais dans le mauvais sens
   pour une SURFACE. Un accent s'éclaircit en sombre pour rester lisible posé
   SUR du foncé ; en faire la matière de 84 px de large donnait une dalle bleu
   clair contre une page presque noire. Constaté par Eric.

   La substitution n'a coûté que deux noms : toute la construction en
   `color-mix` est restée telle quelle, et le rail en clair n'a pas bougé d'un
   pixel — `--c-rail` y vaut exactement l'accent. La raison du sombre vit dans
   `brand.css`, avec les valeurs. */
.rail {
  background: var(--c-rail);
  border-right-color: color-mix(in srgb, var(--c-on-rail) 20%, transparent);
}

/* Au repos, l'encre est atténuée ; au survol elle est pleine. C'est le même
   contraste relatif que sur fond clair (`--c-text-mid` puis `--c-text`), donc
   la hiérarchie du rail ne change pas — seule sa matière change. */
.rail .rail-item,
.rail .rail-menu > summary {
  color: color-mix(in srgb, var(--c-on-rail) 78%, transparent);
}

.rail .rail-item:hover,
.rail .rail-menu > summary:hover,
.rail .rail-menu[open] > summary,
.rail .rail-account:hover {
  background: color-mix(in srgb, var(--c-on-rail) 14%, transparent);
  color: var(--c-on-rail);
}

/* L'entrée courante : un voile plus dense, et l'encre pleine. Elle se
   distinguait par `--c-accent-soft` sur `--c-accent-dark`, c'est-à-dire par du
   bleu pâle sur du blanc — invisible sur un rail bleu. Le poids (600) et
   `aria-current` ne bougent pas : la couleur n'a jamais été seule (règle 6). */
.rail .rail-item.is-current,
.rail .rail-account.is-current {
  background: color-mix(in srgb, var(--c-on-rail) 22%, transparent);
  color: var(--c-on-rail);
}

/* Sur le rail, l'entrée d'administration prend l'encre des autres cibles du
   pied. Elle était éteinte d'un cran tant qu'elle était une place réservée ;
   depuis qu'elle mène quelque part (2026-08-14), la ranger en arrière
   dirait qu'elle ne marche pas. */
.rail .rail-admin {
  color: color-mix(in srgb, var(--c-on-rail) 78%, transparent);
}

.rail .rail-admin:hover {
  background: color-mix(in srgb, var(--c-on-rail) 14%, transparent);
  color: var(--c-on-rail);
}

.rail .rail-admin[aria-current="page"] {
  background: color-mix(in srgb, var(--c-on-rail) 22%, transparent);
  color: var(--c-on-rail);
}

/* La pastille des initiales : elle portait l'accent sur un fond pâle. Sur le
   rail, les deux sont du même bleu — donc on inverse, encre pleine sur voile. */
.rail .rail-avatar {
  background: color-mix(in srgb, var(--c-on-rail) 22%, transparent);
  color: var(--c-on-rail);
}

/* LES DEUX ÉTATS DU BOUTON DES DESTINATIONS, reposés pour ce fond. Voir le
   pavé du haut : ce sont les variables du composant, sur le bouton seul. */
.rail .mil-dest-btn {
  --mild-glow: color-mix(in srgb, var(--c-on-rail) 55%, transparent);
  --mild-accent: color-mix(in srgb, var(--c-on-rail) 22%, transparent);
}

/* ------------------------------------------------------------------
   LA RANGÉE DE BESOIN DEVIENT UNE CARTE CLIQUABLE (2026-08-12)
   ------------------------------------------------------------------ */

/* Constat d'Eric : « ça ressemble à Wikipédia plutôt qu'à une app moderne ».
   C'était juste — une rangée de texte avec un mot souligné en bleu dedans. La
   seule cible était le titre, et rien ne disait que la rangée entière menait
   au volet de droite.

   ── LA CARTE EST LA CIBLE, LE LIEN EST TOUJOURS UN LIEN ────────────────────
   `.row-card-title::after` couvre la carte, donc le clic porte partout ; le
   lien reste un vrai `<a>`, avec son URL au survol, son clic milieu et sa
   place unique dans l'ordre de tabulation. Ce que ni un `onclick` ni un `<a>`
   qui emballerait la rangée ne donnent — le second serait invalide, la rangée
   contenant un `<form>`.

   `align-items: stretch` défait le `center` de `.list-row` : les commandes se
   collent en haut de la carte plutôt que de flotter au milieu d'un bloc de
   trois lignes. */
.row-card {
  position: relative;
  align-items: stretch;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--c-border);
  border-bottom: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  transition: background-color .15s ease, border-color .15s ease;
}

.row-card + .row-card { margin-top: var(--space-2); }

/* La zone étirée. `z-index` explicite et non implicite : les commandes se
   relèvent d'un cran juste en dessous, et deux couches sans ordre déclaré se
   départagent par l'ordre du document — ce qui met le bouton DERRIÈRE. */
.row-card-title::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  z-index: 1;
}

/* Le survol porte sur la CARTE et pas sur le mot : c'est la carte qu'on clique.
   `:focus-within` fait la même chose au clavier, sans quoi la tabulation
   traverserait la liste sans qu'on voie où l'on est. */
.row-card:hover,
.row-card:focus-within {
  background: var(--c-hover-bg);
  border-color: var(--c-border-strong);
}

/* LE BESOIN OUVERT — celui auquel répond le volet de droite. Le repère est un
   BORD, pas un fond : un fond plein le mettrait au même niveau que le survol,
   et on ne saurait plus lequel des deux on regarde. */
.row-card.is-open {
  border-color: var(--c-accent);
  box-shadow: inset 3px 0 0 0 var(--c-accent);
}

/* Le titre cesse d'être bleu et souligné : c'est la carte qui annonce qu'on
   peut cliquer, et vingt liens bleus empilés sont précisément le mur de texte
   qu'on retire. Il garde sa graisse d'ouverture (règle 6 — l'état ne vit pas
   dans la seule couleur du bord).

   `.ecran` EST DANS LE SÉLECTEUR DEPUIS LE 2026-08-13, et c'est une réparation.
   Cette règle ne s'accrochait qu'à `.panel`, le panneau que les six écrans de
   la section Comités rendaient eux-mêmes. Le cadre plein écran les en a
   débarrassés le même jour — la règle a donc cessé de s'appliquer là où elle
   était née, et les quatorze besoins d'un comité sont redevenus quatorze liens
   bleus soulignés : exactement le mur de texte qu'elle existe pour retirer.

   Deux classes, donc (0,2,0), et c'est ce qu'il faut : `.panel a` en pèse
   (0,1,1) et l'emporterait sur `.row-card-title` seul. C'est le même piège que
   `.panel a.btn` deux mille lignes plus haut. */
.panel .row-card-title,
.ecran .row-card-title {
  color: var(--c-text);
  font-weight: 600;
  text-decoration: none;
}

/* Les commandes passent AU-DESSUS de la zone étirée, sinon « Ça m'intéresse »
   ouvrirait le besoin au lieu de lever la main. */
.row-card .list-row-actions,
.row-card form {
  position: relative;
  z-index: 2;
}

/* ---------------------------------------------------------------
   LE BESOIN ANNONCÉ — la rangée de « Ce que la coop cherche »
   ---------------------------------------------------------------
   Refaite le 2026-08-14. Ce qu'elle était, en une seule phrase et c'est le
   problème :

     Chargé·e d'administration · Finances
     Vous êtes une personne organisée… · Mensuel 1h · Annoncé le 5 août ·
     Tu as dit que ça t'intéressait.

   Un titre, puis QUATRE faits de natures différentes enfilés sur des points
   médians, en gris, dans une taille de note. Trois défauts, et aucun n'est une
   affaire de goût :

   · **Le point médian ne dit rien.** Il sépare « Finances » (un comité) de
     « Mensuel 1h » (un rythme) et de « Annoncé le 5 août » (une date) avec le
     même signe. Le lecteur doit deviner la catégorie de chaque fragment à
     partir de sa forme. C'est exactement ce que la règle 7 de la charte refuse
     — sauf qu'ici le vocabulaire d'initié est une PONCTUATION.
   · **L'état arrivait dernier.** « Tu as dit que ça t'intéressait » est ce qui
     a changé depuis la dernière visite, et c'était la fin d'une ligne grise. La
     règle 6 demande l'état en toutes lettres ; il l'était, illisiblement.
   · **À voix haute, la chaîne se lit d'un trait**, sans pause de phrase, parce
     qu'un point médian n'est pas une ponctuation forte.

   ── CE QUI REMPLACE ─────────────────────────────────────────────────────
   Un fait par ligne, chacun étiqueté par un mot. L'état monte en tête, en
   chip. Rien n'est retiré ni ajouté : c'est le MÊME contenu, rangé.

   Le coût est de la hauteur — mesuré sur les données de MIL, quatre annonces
   passent de 292 à 468 px. C'est acceptable ici et le serait rarement
   ailleurs : la liste est courte par construction (elle ne rend que les
   besoins annoncés, quatre chez MIL contre soixante et onze proposés), et
   c'est la seule chose que la page demande de LIRE. */
.besoin-corps {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

/* L'ÉTAT EN PREMIER, ET IL SORT DU GRIS. Une chip pleine plutôt que le
   `.pill` creux des rangs : celui-ci se lit comme une étiquette de catégorie,
   et ceci est une nouvelle — « c'est à toi », « tu as levé la main ». Le fond
   sémantique la distingue du reste de la carte sans qu'aucune couleur ne porte
   le sens seule : le mot est écrit dedans (règle 6). */
.besoin-etat {
  padding: 1px var(--space-2);
  border: 1px solid var(--c-success);
  border-radius: var(--radius-pill);
  background: var(--c-success-soft);
  color: var(--c-success-text);
  font-size: var(--mil-fs-0);
}

/* LE COMITÉ, ÉTIQUETÉ. Il était « · Finances » collé au titre, dans la même
   taille et la même graisse que lui — donc lisible comme la suite du titre du
   besoin. C'est l'adresse à qui parler ; elle mérite son mot. */
.besoin-comite {
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

/* CE QUE LE BESOIN DEMANDE — la seule vraie prose de la rangée, donc au cran
   de la prose et dans la couleur du texte, pas en note grise. C'est la phrase
   qu'un comité a écrite pour convaincre quelqu'un ; l'éteindre était une façon
   de la ranger, pas de la lire. */
.besoin-quoi {
  font-size: var(--mil-fs-2);
  color: var(--c-text);
}

/* LES FAITS COURTS — le rythme, la date. Ils se replient (`flex-wrap`) parce
   que deux étiquettes et leurs valeurs ne tiennent pas sur 375 px à 150 % de
   zoom texte. L'espacement les sépare : plus de point médian, donc plus de
   signe à interpréter. */
.besoin-faits {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}

/* ---------------------------------------------------------------
   LES MODALES — un formulaire posé par-dessus l'écran
   ---------------------------------------------------------------
   Demandées par Eric le 2026-08-12 pour les deux formulaires de
   besoin. Ils vivaient dans le flux : six champs sous la liste
   pour « déclarer », six autres au fond d'un volet de deux écrans
   pour « corriger ». La liste que quatre-vingt-douze personnes
   viennent lire passait sous la ligne de flottaison pour un
   formulaire que quatorze utilisent.

   ── DEUX ÉTATS, ET LE PREMIER EST CELUI DU SERVEUR ──────────────
   `.modale` habille la surface LÀ OÙ ELLE EST RENDUE : dans le
   flux, en carte, avec son titre. C'est la page sans JavaScript,
   et le déclencheur est un `<a href="#son-id">` qui y saute
   (§0, règle 10 — pas d'étape de construction, donc pas de
   surface qui n'existe qu'une fois un script chargé).

   `modale.js` la DÉPLACE ensuite dans un `<dialog>`, et les
   règles de `.modale-boite > .modale` défont l'habillage de
   carte : le cadre est alors celui de la boîte, et deux cadres
   emboîtés se liraient comme une carte dans une carte.
   --------------------------------------------------------------- */
.modale {
  margin: var(--space-5) 0;
  padding: var(--space-4);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
}

/* Le voile. Comme la visionneuse du Cahier, il est le fond du DIALOGUE
   lui-même et non son `::backdrop` : celui-ci n'hérite des propriétés
   personnalisées que depuis Safari 17.4 et Chrome 122, et là où il n'hérite
   pas, `var(--c-overlay)` ne rend rien — donc un formulaire posé sur la page
   en clair, sans que rien ne signale la panne.

   Les trois `none` défont le dialogue centré du navigateur : sa feuille par
   défaut le borne à `calc(100% - 6px - 2em)` et le pose en `fit-content`. */
.modale-dialogue {
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: var(--space-5);
  border: 0;
  background: var(--c-overlay);
  overflow: auto;
}

/* ── `[open]` PORTE LE `display`, ET CE N'EST PAS UN DÉTAIL ─────────────────
   Ce qui cache un dialogue fermé est `dialog:not([open]) { display: none }`,
   une règle de la feuille du NAVIGATEUR — donc battue par n'importe quelle
   règle d'auteur. Un `display: flex` posé sur `.modale-dialogue` tout court
   affichait le dialogue en permanence : voile sur toute la page dès le
   chargement, formulaire au milieu, et personne pour l'avoir demandé.

   Le symptôme trompe, parce qu'il RESSEMBLE à une modale ouverte : ce qui le
   distingue est que le contenu de la page passe PAR-DESSUS le voile. Un
   dialogue ouvert par `showModal()` vit dans la couche supérieure, où rien du
   document ne peut le recouvrir ; ce dialogue-ci n'y était pas, donc les
   boutons « Ça m'intéresse » — que `.row-card` monte en `z-index: 2` — se
   posaient sur le formulaire. Vu à l'écran par Eric le 2026-08-12.

   `.cahier-visio` échappait au piège en ne déclarant aucun `display` : elle
   centre sa photo par une scène en flex à l'intérieur. Ici c'est `[open]` qui
   tranche, ce qui garde le centrage sur le dialogue lui-même. */
.modale-dialogue[open] {
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

/* La boîte. `position: relative` porte le bouton de fermeture ; l'ombre est
   réservée aux menus et aux modales (§0, règle 2), et c'en est une. */
/* PAS DE `margin: auto` ICI, et c'est mesuré. Il centrerait la boîte dans le
   dialogue — ce qu'on veut tant qu'elle est plus courte que l'écran, et qui la
   fait DÉBORDER PAR LE HAUT dès qu'elle est plus haute : le débordement d'un
   conteneur qui défile n'est pas atteignable vers le négatif, donc le titre et
   le premier champ se retrouvent coupés sans moyen de remonter. Mesuré à
   `top: -4px` sur ce formulaire-ci à 900 px de haut, et ça empire à mesure que
   l'écran raccourcit. `align-items: flex-start` sur le dialogue s'en charge, et
   le rembourrage du dialogue donne l'air au-dessus. */
.modale-boite {
  position: relative;
  width: min(40rem, 100%);
  padding: var(--space-5);
  border-radius: var(--radius);
  background: var(--c-surface);
  box-shadow: 0 4px 16px var(--c-shadow-strong);
}

/* La surface levée n'est plus une carte : c'est la boîte qui encadre. */
.modale-boite > .modale {
  margin: 0;
  padding: 0;
  border: 0;
}

.modale-boite > .modale > :first-child { margin-top: 0; }
.modale-boite > .modale > :last-child { margin-bottom: 0; }

/* LE TITRE D'UNE MODALE EST UN TITRE, PAS UN EN-TÊTE DE SECTION. `h2` porte les
   petites capitales grises qui découpent une page en morceaux ; ici il n'y a
   rien à découper — c'est le sujet de tout ce que la boîte contient, et le seul
   texte qui dise ce qu'on est en train de faire. Même partage que
   `.card-title` et `.detail-title`, et il faut le REDÉCLARER pour la même
   raison : une classe ne gagne que sur ce qu'elle réécrit.

   Il garde sa marge de droite pour le bouton de fermeture, qui est en position
   absolue au-dessus de lui : sans elle, un titre long passe dessous. */
.modale > h2 {
  margin: 0 44px var(--space-4) 0;
  font-size: var(--mil-fs-4);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text);
}

/* 44 px : la cible tactile minimale, et la seule mesure qui décide de sa
   taille. L'anneau de focus vient de la règle qui habille déjà tout
   `button:focus-visible`. */
.modale-fermer {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--c-text-muted);
  font-size: var(--mil-fs-3);
  line-height: 1;
  cursor: pointer;
}

.modale-fermer:hover { background: var(--c-hover-bg); color: var(--c-text); }

/* LA PAGE NE DÉFILE PAS DERRIÈRE, et c'est le seul trou que `<dialog>` ne
   bouche pas : il rend la page inerte au clic et au clavier, jamais à la
   molette. La classe est posée sur `<html>` par `modale.js`, le temps de
   l'ouverture — même remède que `.cahier-visio-ouverte`. */
.modale-ouverte { overflow: hidden; }

/* L'EN-TÊTE D'UNE LISTE QUI PORTE UNE COMMANDE — le titre à gauche, le geste
   poussé à droite. `flex-wrap` parce qu'à 375 px « Ce que le comité propose »
   et « Nouveau à faire » ne tiennent pas sur une ligne, et que le bouton doit
   alors passer dessous plutôt que se comprimer sous sa cible tactile. */
.liste-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

.liste-head > h2 { margin-bottom: var(--space-2); }

/* Le déclencheur est un `<a>` et pas un `<button>` : sans JavaScript il saute
   au formulaire, qui est dans le flux. Il n'hérite donc ni de la cible tactile
   ni du centrage que `.card-actions` donne à ses liens — même redéclaration
   que `.head-menu > summary`, et pour la même raison.

   `.tabs-actions` est dans le sélecteur depuis le 2026-08-13 : « Nouveau à
   faire » a quitté l'en-tête de la liste pour la ligne des onglets, et la
   règle doit suivre le bouton. Sans elle il ressort SOULIGNÉ — un `<a>` dans
   un panneau l'est par défaut — au milieu de molettes qui ne le sont pas, ce
   qui se voit tout de suite et ne se devine pas en relisant le gabarit. */
/* La cible de rangée, et RIEN QUE elle depuis le 2026-08-16. Cette règle
   portait aussi `display: inline-flex`, `align-items: center` et
   `text-decoration: none` — c'est-à-dire le dessin même du bouton-lien, rendu
   ici à `a.btn`, qui l'applique partout au lieu de ces deux endroits. */
.liste-head > .btn,
.tabs-actions > .btn {
  min-height: 44px;
}

/* Le + rond de l'en-tête se CENTRE au lieu de suivre la ligne de base : un
   rond sans texte n'a pas de ligne de base à offrir, et `baseline` le pose
   alors sur son bord bas — le bouton flotte au-dessus du titre. Vaut pour
   tout `.outil-rond` posé dans un `.liste-head`. */
.liste-head > .outil-rond { align-self: center; }

/* ET LA MOLETTE MONTE À LA MÊME HAUTEUR, sinon la rangée a deux étages : un
   bouton de 44 px à côté d'un de 36 se lit comme un défaut d'alignement, et
   c'est la règle 9 (cibles ≥ 44 px) qui décide lequel des deux cède. */
.tabs-actions button { min-height: 44px; }

/* SUR UN TÉLÉPHONE, LES COMMANDES TIENNENT SUR UNE LIGNE (2026-08-13).

   Mesuré à 375 px, cadre figé posé : « Libre-service : ouvert » et « Nouveau à
   faire » font 340 px à deux, pour 327 px de large. Treize pixels de trop, donc
   `flex-wrap` les empilait — et la rangée des onglets passait à 163 px sur les
   ~590 que le cadre laisse au contenu. Un quart de l'écran pour deux boutons.

   C'est le LIBELLÉ qui rétrécit, pas la cible : `--mil-fs-1` et la gouttière
   étroite les font tenir côte à côte, et `min-height: 44px` tient la règle 9
   par ailleurs. Un bouton plus court se vise aussi bien ; un bouton plus BAS,
   non. */
@media (max-width: 700px) {
  .tabs-actions { font-size: var(--mil-fs-1); }

  .tabs-actions > .btn,
  .tabs-actions button {
    font-size: inherit;
    padding-left: var(--space-2);
    padding-right: var(--space-2);
  }
}

/* CE QUE LA VALIDATION A REFUSÉ. `auth.css` en a sa version depuis les écrans
   d'avant-connexion, et le chrome de l'app n'en avait aucune : un refus
   renvoyait la page avec la saisie intacte (`old()`) et sans un mot pour dire
   ce qui n'allait pas. Visible surtout dans une modale, qui se rouvre alors
   sans raison apparente. */
.field-errors {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--c-danger);
  border-radius: var(--radius);
  background: var(--c-danger-soft);
  color: var(--c-danger-text);
  list-style: none;
}

.field-errors li + li { margin-top: var(--space-1); }

/* ---------------------------------------------------------------
   LA LIGNE DES ONGLETS PORTE LES MOLETTES D'ÉCRAN (2026-08-12)
   ---------------------------------------------------------------
   Demande d'Eric : « les toggles généraux comme le libre-service
   devraient se trouver à droite, à la même hauteur que les onglets,
   en outlined buttons ».

   Le trait du bas passe de `.tabs` à la RANGÉE : sur `.tabs` seule,
   il s'arrêtait sous le dernier onglet et laissait les molettes
   flotter au-dessus de rien. Il court maintenant sur toute la
   largeur, et c'est lui qui fait la ligne.

   `align-items: flex-end` : les onglets sont hauts de 44 px et un
   bouton de 36 px doit s'asseoir sur la même ligne de base, pas se
   centrer dans leur hauteur.
   --------------------------------------------------------------- */
.tabs-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-4) 0 var(--space-5);
  border-bottom: 1px solid var(--c-border);
}

/* Le trait appartient à la rangée ; `.tabs` le rendrait deux fois. */
.tabs-row > .tabs {
  margin: 0;
  border-bottom: 0;
}

/* Les molettes se collent au trait, avec l'air qu'un bouton demande sous lui.
   `flex-wrap` parce qu'à 375 px trois onglets et une molette ne tiennent pas
   sur une ligne — elle passe dessous plutôt que de comprimer sa cible. */
.tabs-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-2);
}

/* Sans barre d'onglets — une personne partie de la coop — la rangée n'a plus
   de trait à porter : il n'y a rien à séparer d'une navigation absente. */
.tabs-row-seule {
  justify-content: flex-end;
  border-bottom: 0;
}

/* L'interrupteur ENFONCÉ. Le libellé porte déjà l'état en toutes lettres
   (« Libre-service : ouvert »), donc ce fond ne l'annonce pas — il le
   confirme, ce qui est le seul emploi honnête d'une couleur d'état
   (règle 6). */
.tabs-actions [aria-pressed="true"] {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-text);
}

/* ---------------------------------------------------------------
   LES LIENS PRATIQUES D'UN COMITÉ (2026-08-12)
   ---------------------------------------------------------------
   Le dossier OneDrive, la boîte Outlook et le courriel du comité.
   Ce sont des PORTES vers ailleurs, pas des commandes : d'où une
   puce discrète plutôt qu'un bouton, et un dessin de marque qui se
   reconnaît avant qu'on lise.

   Ils ne sont pas dans `.tabs-actions` — ce ne sont pas des
   molettes d'écran. Ils appartiennent au comité, pas à l'onglet
   qu'on regarde.

   `.liens-pratiques`, la rangée qui les portait SOUS le nom, est
   partie le 2026-08-13 : ils vivent dans `.committee-head-actions`,
   sur la ligne du nom, à côté de « Je démissionne ». Une rangée
   pleine largeur pour au plus trois pastilles était de la hauteur
   dépensée à ne rien dire.
   --------------------------------------------------------------- */
.lien-pratique {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 36px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--mil-fs-1);
  text-decoration: none;
}

.lien-pratique:hover {
  background: var(--c-hover-bg);
  border-color: var(--c-border-strong);
}

/* Le logo garde sa taille quoi que la puce fasse : c'est lui qu'on reconnaît,
   et un dessin comprimé par un libellé long cesse d'être reconnaissable. */
.lien-pratique .app-mark { flex: 0 0 auto; width: 18px; height: 18px; }
/* ═══════════════════════════════════════════════════════════════════════════
   MA FICHE — `/profil`, un seul écran qui se lit ET se remplit
   ═══════════════════════════════════════════════════════════════════════════

   Décision d'Eric, 2026-08-12, la maquette de l'app actuelle à l'appui : « il
   faut que ce soit aussi simple que ça, pas de page de modification à part ».
   Ce que ça change tient en une phrase — la fiche n'est plus un écran qu'on
   LIT avant d'aller sur un second pour ÉCRIRE. Les réponses sont sur place, et
   on les change là où on les voit.

   D'où ces règles-ci : elles n'existent que pour rendre un choix aussi facile à
   viser qu'à lire. Rien ici ne s'applique au volet de l'annuaire, qui montre la
   fiche de quelqu'un d'autre et n'a rien à modifier. */

/* ───────────────────────────────────────────────────────────────────────────
   LA FICHE PREND LA FENÊTRE (Eric, 2026-08-16)
   ───────────────────────────────────────────────────────────────────────────
   Deux phrases, sur une capture au téléphone : « better use of space — things
   are not very tight, unnecessary box and padding » et « viewport doesn't
   stretch to the bottom ». C'est un seul défaut : la fiche était une CARTE
   posée dans une page qui défile. D'où un bord et un rayon autour d'un écran
   qui n'a rien à côté de lui, une gouttière qui reprenait 16 px de chaque côté
   d'un téléphone de 375, et une bande morte sous la carte — le document
   s'arrêtait où le contenu s'arrêtait, pas où la fenêtre finit.

   Elle rejoint donc le patron du README (« la fenêtre est la fenêtre, les zones
   défilent »), déjà tenu par `/membres` et par la section Comités.

   ── LE REMBOURRAGE VIT DANS UNE VARIABLE, ET C'EST STRUCTUREL ──────────────
   Deux meubles de cet écran DÉBORDENT volontairement de la colonne de texte
   pour aller d'un bord à l'autre : le tableau de bord collant et la barre
   d'enregistrement. Ils le font par marge négative, donc ils doivent connaître
   le rembourrage exact de la zone. Écrit trois fois, ce nombre finit par ne
   plus être le même aux trois endroits — et le symptôme est un couloir clair de
   4 px par où le contenu remonte derrière une barre, ce qu'on ne voit qu'en
   défilant. `--fiche-pad` est lu par les trois. */
/* IL N'Y A PLUS DE GOUTTIÈRE DE BUREAU À GARDER — Eric a tranché le 2026-08-16,
   quelques heures après le commit qui écrivait ici l'inverse : « the padding on
   .wrap is superfluous », à toutes les largeurs. Ce bloc disait « la carte de
   bureau reste, là elle SÉPARE l'écran du bureau autour » ; c'était une lecture
   défendable du « unnecessary box » de la capture — la capture était un
   téléphone — et ce n'est pas celle qui a été retenue.

   Mesuré sur l'accueil avant la décision : le rail s'arrête à 84 px, le panneau
   commençait à 108, le texte à 132. Quarante-huit pixels pour une bordure,
   parce que `.wrap` rembourrait autour d'un panneau qui rembourre dedans.

   CE QUI NE CHANGE RIEN À CET ÉCRAN-CI, et c'est le point : la chaîne flexible
   posée ici (`.shell-main-ecran > .wrap` en `flex: 1 1 auto`, panneau à
   `height: 100%`) n'avait déjà rien à retrancher. Elle marche à l'identique
   sans gouttière — mieux, même, puisqu'il n'y a plus de nombre à connaître.

   SOUS 700 PX LE CADRE S'EFFACE ENCORE, et ça, ça reste vrai : le bord et le
   rayon d'une carte qui touche les deux bords d'un téléphone ne dessinent rien
   qu'on puisse voir. La ligne `.wrap-fiche` en dessous ne retranche donc plus
   rien — `.wrap` est déjà à zéro partout — mais elle reste la SEULE définition
   de cette classe, et `DesignSystemRulesTest` exige qu'une classe posée dans un
   gabarit existe dans une feuille. La retirer demanderait de retirer aussi
   `wrap-fiche` du gabarit ; c'est l'écran d'une autre tranche, et ce n'est pas
   à cette tranche-ci de le remanier. */
@media (max-width: 700px) {
  .wrap-fiche { padding: 0; }

  .panel.fiche-plein {
    border-left: 0;
    border-right: 0;
    border-radius: 0;
  }
}

/* LE PANNEAU EST LA ZONE QUI DÉFILE, et il n'y en a qu'une.

   Les deux autres écrans à fenêtre figée séparent le cadre des zones parce
   qu'ils en ont plusieurs — trois volets sur `/membres`, une colonne et un
   contenu dans la section Comités. Ici il n'y a qu'une pile de sections : une
   boîte de plus pour l'envelopper serait un niveau d'imbrication qui ne décide
   de rien, et deux endroits où lire le rembourrage.

   Le tableau de bord et la barre d'enregistrement collent DANS ce conteneur —
   c'est ce qui les libère des deux barres fixes de la coquille, dont ils
   lisaient la hauteur jusqu'ici (voir leurs règles).

   `overflow-y` seul, et `overflow-x` suit : la spec calcule en `auto` un axe
   `visible` en face d'un axe qui ne l'est pas. C'est voulu — la grille des
   disponibilités est plus large que la colonne au téléphone, et elle a déjà son
   propre défilement. */
/* ⚠️ AUCUN REMBOURRAGE VERTICAL, ET CE SONT LES DEUX COLLANTS QUI L'EXIGENT.

   Un élément collant se pose dans la fenêtre de défilement RÉDUITE DU
   REMBOURRAGE du conteneur : mesuré sur jam, 24 px en haut et en bas donnaient
   un tableau de bord épinglé à 25 px du bord et une barre d'enregistrement à
   24 px du fond — deux couloirs par lesquels les sections qui défilent se
   voient passer, en haut ET en bas. C'est exactement le défaut que les marges
   négatives de ces deux meubles existent pour éviter sur les côtés.

   L'air vertical est donc DANS le contenu : l'en-tête porte celui du haut (il
   défile avec lui, ce qui est juste), et les deux barres ont le leur. */
.fiche-plein {
  --fiche-pad: var(--space-5);
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  height: 100%;
  min-height: 0;
  padding: 0 var(--fiche-pad);
  overflow-y: auto;
}

@media (max-width: 700px) {
  .fiche-plein { --fiche-pad: var(--space-4); }
}

/* SUR PAPIER, LA FENÊTRE N'EXISTE PAS.

   Une zone haute d'un écran avec un `overflow` n'imprime QUE ce qu'elle montre :
   la fiche sortirait coupée à la première question, et les six autres sections
   n'existeraient pas sur la feuille. Le gabarit promet l'inverse — « l'écran
   reste imprimable » est l'argument qui a fait choisir des liens d'ancre plutôt
   que des onglets — donc la promesse se tient ici plutôt qu'elle ne se retire
   là-bas.

   Les deux collants redeviennent du contenu ordinaire : sur une feuille, un
   meuble qui suit le regard est un meuble répété à chaque page. */
@media print {
  .shell-main-ecran { height: auto; }

  .fiche-plein {
    height: auto;
    overflow: visible;
  }

  .fiche-board,
  .fiche-save { position: static; }
}

/* L'EN-TÊTE — le visage, le nom, et de quoi se reconnaître.

   Il porte l'air du haut de la zone (voir `.fiche-plein`, qui n'a pas de
   rembourrage vertical) : posé sur le conteneur, cet air-là deviendrait un
   couloir où le contenu défile au-dessus du tableau de bord épinglé. Ici il
   défile avec l'en-tête, ce qui est ce qu'on veut de toute façon. */
.fiche-head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding-top: var(--fiche-pad);
}

.fiche-ident { min-width: 0; }

.fiche-ident h1 { margin: 0; }

/* LE CADENAS DE LA FICHE, dans le coin de son en-tête. Il n'entre pas dans
   `.fiche-ident` : ce bloc-là porte le nom, l'appartement et les comptes, et un
   signe posé dedans se lirait comme un fait de plus sur la personne. Il est le
   troisième enfant de la RANGÉE, poussé au bout — même raisonnement, et même
   repli à 700 px, que `.titre-ligne`. */
.fiche-head > .infobulle { margin-left: auto; }

@media (max-width: 700px) {
  .fiche-head > .infobulle { margin-left: 0; }
}

.fiche-sub {
  margin: var(--space-1) 0 0;
  color: var(--c-text-mid);
  font-size: var(--mil-fs-2);
}

/* L'ADRESSE EST UNE PORTE (2026-08-19) — vers les autres adresses de la
   personne, celles qu'elle a prouvées.

   ── ELLE NE PREND PAS LA COULEUR D'UN LIEN, ET C'EST DÉLIBÉRÉ ──────────────
   `.panel a` la peindrait en accent : la ligne d'identité se mettrait à crier
   sous le nom, là où elle est une mention. Elle garde donc le gris du
   sous-titre et annonce autrement qu'on peut la toucher — un soulignement en
   pointillé, qui se voit sans peser. C'est le piège `.panel a` du magasin :
   une classe de composant qui n'écrit pas sa couleur perd contre le lien
   générique, en silence, et seulement à l'écran.

   `text-underline-offset` : sans lui le pointillé traverse les jambages des
   « p » et des « g » d'une adresse de courriel, qui en est pleine. */
.panel a.fiche-courriels,
.ecran a.fiche-courriels {
  color: inherit;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

.panel a.fiche-courriels:hover,
.ecran a.fiche-courriels:hover { color: var(--c-text); }

.fiche-courriels .pill { vertical-align: baseline; }

/* LA LISTE DES ADRESSES, dans la modale.

   Une ligne par adresse, ses outils poussés au bout par `margin-left: auto` —
   la même mécanique que les barres d'outils des comités, pour que les croix
   s'alignent quelles que soient les longueurs d'adresse. Elles s'alignent, donc
   on vise la bonne. */
.courriels {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.courriel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
}

/* CE QUI ATTEND SE VOIT SANS LIRE. Le trait de gauche fait la différence en
   diagonale, là où la pastille « à confirmer » demande de lire deux mots. */
.courriel.is-attente { border-left: 3px solid var(--c-warning); }

/* `min-width: 0` + `overflow-wrap` : une adresse longue casse plutôt que de
   pousser les outils hors de la carte — sur un téléphone, une seule adresse de
   travail suffit à les faire disparaître à droite. */
.courriel-adresse {
  min-width: 0;
  overflow-wrap: anywhere;
}

.courriel-outils {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-left: auto;
}

/* LE FORMULAIRE D'AJOUT. En colonne — le champ de courriel a besoin de toute
   la largeur sur un téléphone, et un bouton à côté de lui le réduirait à un
   tiers d'écran pour la chaîne la plus longue du formulaire. */
.courriel-neuf {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* Le compte de ce à quoi on participe, en pastille : c'est la seule ligne de
   l'en-tête qui bouge d'une semaine à l'autre, et la seule qu'on vient
   vérifier. Elle ne se modifie pas ici — on la gagne ailleurs, en levant la
   main — donc elle est du texte, pas un bouton. */
.fiche-counts {
  display: inline-block;
  margin: var(--space-2) 0 0;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-size: var(--mil-fs-1);
}

/* ───────────────────────────────────────────────────────────────────────────
   LE TABLEAU DE BORD DE LA FICHE (2026-08-14)
   ───────────────────────────────────────────────────────────────────────────
   Demande d'Eric : « make /profil easy to understand, thumb friendly and
   gamified ». Les trois se règlent au même endroit — l'écran fait plusieurs
   hauteurs de téléphone et n'avait aucun repère, donc on ne savait ni où on en
   était, ni ce qu'il restait, ni où aller. */

/* OÙ J'EN SUIS, EN UNE PHRASE. Elle ne colle pas : on la lit en arrivant, pas
   en cochant. Le compte est en gras et la suite ne l'est pas — « 5 sur 7 » est
   ce qu'on retient, « la prochaine : … » est ce qu'on suit. */
.fiche-quete {
  margin: var(--space-4) 0 var(--space-3);
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

.fiche-quete strong { color: var(--c-text); }

/* LES TROIS DESTINATIONS, COLLÉES EN HAUT DE LA ZONE.

   Les marges négatives lui rendent la largeur du panneau, dont il est bordé
   (`--fiche-pad`) : une barre collante qui s'arrête aux marges laisse deux
   couloirs par où le contenu remonte, et ça se voit. Même patron que
   `.cnav-head`, pour la même raison.

   `top: 0` VAUT MAINTENANT À TOUTES LES LARGEURS — c'est ce que la fenêtre
   figée a simplifié le 2026-08-16. Il a existé ici un `top: var(--rail-bar-h)`
   sous 700 px : la page défilait, la barre du rail était `position: fixed`
   par-dessus, et un `0` glissait DESSOUS. La zone qui défile commence
   aujourd'hui après cette barre, donc il n'y a plus rien à esquiver.

   Le FOND EST OBLIGATOIRE, et ce n'est pas de la décoration : sans lui, les
   pastilles de disponibilités défileraient à travers les mots du tableau de
   bord. */
.fiche-board {
  position: sticky;
  top: 0;
  z-index: 2;
  margin: 0 calc(var(--fiche-pad) * -1);
  padding: var(--space-3) var(--fiche-pad);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
}

/* LA JAUGE EN FILET — les sept sections, sans un mot. Le compte est déjà écrit
   au-dessus et dans les trois pastilles ; ce trait-ci sert à l'œil qui balaye,
   pas à qui lit. Sa hauteur est en `em` pour la même raison que celle de
   l'accueil : une barre de 6 px reste de 6 px quand quelqu'un monte le texte à
   150 %, et devient un cheveu. */
.board-meter {
  display: block;
  width: 100%;
  height: 0.35em;
  margin: 0 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--c-surface-2);
  appearance: none;
}

.board-meter::-webkit-progress-bar {
  border-radius: var(--radius-pill);
  background: var(--c-surface-2);
}

.board-meter::-webkit-progress-value {
  border-radius: var(--radius-pill);
  background: var(--c-accent);
  transition: width var(--t-slow);
}

.board-meter::-moz-progress-bar {
  border-radius: var(--radius-pill);
  background: var(--c-accent);
}

/* Trois colonnes ÉGALES, et elles le restent à 375 px : `flex: 1 1 0` plutôt
   qu'une base en `rem`, sinon la pastille au libellé le plus long décide de la
   largeur des trois et la rangée passe à la ligne pour un mot. */
.quests {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.quests > li { flex: 1 1 0; min-width: 0; }

/* LA PASTILLE — un lien d'ancre de 56 px, c'est-à-dire une vraie cible au
   pouce (§0, règle 9 : 44 px au minimum).

   Elle est GRISE tant qu'on n'a pas répondu, et c'est l'état par défaut : rien
   n'est obligatoire sur cette fiche, donc une pastille vide ne peut pas porter
   la couleur d'un avertissement. */
.quest {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  text-align: center;
  text-decoration: none;
}

/* `.panel .quest` ET PAS `.quest` POUR LA SEULE COULEUR — même piège que
   `.comite-card` plus haut, et il ne se voit qu'à l'écran : `.panel a` vaut
   (0,1,1) et bat `.quest` (0,1,0), donc la pastille au repos sortait en BLEU de
   lien au lieu du gris « pas encore répondu ». Les états, eux, portent deux
   classes et gagnent tout seuls. */
.panel .quest { color: var(--c-text-mid); }

/* Le nom se serre plutôt que de passer à trois lignes : à 375 px chaque
   pastille a 92 px, et « Préférences » y tient sur une.

   `position: relative` est la règle de la maison sur toute zone qui écrête
   (§0) : un `overflow` ne retient que ce qu'il CONTIENT, et un descendant en
   `position: absolute` — toute `.visually-hidden` en est un — s'échapperait
   vers le premier ancêtre positionné. Ici il n'y en a pas encore ; la ligne est
   là pour qu'il n'y en ait jamais un qui parte sans qu'on s'en aperçoive. */
.quest-what {
  position: relative;
  font-size: var(--mil-fs-1);
  font-weight: 600;
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* Le compte (« 1 sur 2 ») dit l'état SANS la couleur (règle 6) — c'est lui qui
   reste quand le vert et le gris se ressemblent. */
.quest-score {
  display: flex;
  align-items: center;
  gap: 2px;
  font-size: var(--mil-fs-0);
}

/* Le crochet n'apparaît qu'une fois la quête FINIE. Réservé en largeur nulle
   plutôt qu'absent : posé sur `display: none`, son arrivée décalerait le compte
   d'un caractère à chaque quête terminée. */
.quest-mark {
  opacity: 0;
  font-weight: 700;
}

.quest.is-done .quest-mark { opacity: 1; }

/* COMMENCÉE — au moins une réponse. C'est exactement ce qu'allume le point au
   bout du titre de section : les deux lisent `answeredSections()`, donc l'un ne
   peut pas dire « répondu » pendant que l'autre dit « pas encore ». */
.quest.is-on {
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
}

/* FINIE — le bloc entier est rempli. Le fond en plus du bord, comme `.choice`
   et `.seg-opt` : c'est le vocabulaire de « c'est celui-là » dans toute l'app,
   et une deuxième façon de peindre le même fait serait un deuxième vocabulaire
   à apprendre. */
.quest.is-done {
  background: var(--c-accent-soft);
  font-weight: 600;
}

/* LA PROCHAINE, ET SEULEMENT ELLE — la première qui manque, dans l'ordre du
   formulaire. Trois pastilles également accentuées ne donnent aucun point de
   départ ; c'est aussi celle que nomme la phrase juste au-dessus, donc le
   trait et la phrase désignent la même chose. */
.quest.is-next {
  border-color: var(--c-accent);
  border-style: dashed;
  color: var(--c-text);
}

.quest:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* CE QU'ON VIENT DE GAGNER BAT UNE FOIS — posé par `profil.js` sur la pastille
   qui change d'état, et retiré à la fin de l'animation.

   C'est le seul mouvement de l'écran, et il est là pour une raison précise :
   la réponse s'enregistre en arrière-plan, à trois hauteurs d'écran de la
   pastille qui la compte. Sans ce battement, la seule preuve que la fiche a
   avancé est un avis qui passe en bas — et personne ne regarde en haut au bon
   moment. Il ne remplace rien : l'avis, la ligne d'état et le compte écrit
   disent déjà tout à qui ne voit pas l'animation. */
@keyframes quest-gagnee {
  0% { transform: scale(1); }
  40% { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* `--t-slow` — le cran des toasts et des panneaux, c'est-à-dire de ce qui
   ANNONCE. Le battement dit la même chose que l'avis qui passe en bas, donc il
   dure le même temps ; sur `--t-fast`, qui est le cran du survol, il ressemble
   à un défaut de rendu plutôt qu'à une réponse. */
.quest.is-gagnee { animation: quest-gagnee var(--t-slow) ease; }

/* `prefers-reduced-motion` COUPE LE BATTEMENT, jamais l'information : la
   pastille change quand même de bord, de fond et de compte. Un mouvement est un
   supplément, donc il se retire sans rien emporter. */
@media (prefers-reduced-motion: reduce) {
  .quest.is-gagnee { animation: none; }
  .board-meter::-webkit-progress-value { transition: none; }
}

/* LA SECTION — son titre, et le point qui dit si on a répondu.
   Le point n'est pas décoratif : sur un écran qui n'a plus de bouton
   « Modifier », c'est la seule chose qui distingue « je n'ai rien à dire ici »
   de « je n'ai pas encore vu cette question ». Il double le mot `is-on` par sa
   couleur ET par son titre accessible, jamais par la couleur seule (règle 6). */
/* LE FILET ET SON AIR, MESURÉS ENSEMBLE (2026-08-16). Le titre de section porte
   la marge de tous les `h2` de l'app — `--space-6` — et la section ajoutait la
   sienne par-dessus : 64 px de vide entre un trait et le mot qu'il annonce, six
   fois sur une page qui en fait cinq de haut. C'est le « things are not very
   tight » de la capture. Les deux valeurs sont maintenant écrites ICI, l'air
   au-dessus du filet et l'air en dessous, et le `h2` ne décide plus de rien. */
.fiche-section {
  border-top: 1px solid var(--c-border);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

.fiche-section > .section-head > h2,
.fiche-section > h2 { margin-top: 0; }

/* LE TITRE VISÉ S'ARRÊTE SOUS LE TABLEAU DE BORD, PAS DESSOUS.

   Un lien d'ancre pose sa cible en haut de la ZONE QUI DÉFILE, et ce haut-là est
   occupé par le tableau de bord collant. Sans cette marge, les trois pastilles
   font exactement ce qu'on leur demande et l'on atterrit sur la question qui
   suit celle qu'on visait, en croyant que le lien a raté.

   C'est la panne silencieuse de tout en-tête collant. Les 7 rem sont la hauteur
   du tableau de bord plus un peu d'air ; ils ne suivent pas le zoom texte parce
   que le tableau de bord non plus — ses trois pastilles ont une hauteur en
   pixels.

   UNE SEULE VALEUR DEPUIS LE 2026-08-16 : il a existé ici un second calcul sous
   700 px, qui ajoutait `--rail-bar-h`, parce que la PAGE défilait sous une barre
   fixe. La cible se pose désormais dans une zone qui commence après cette barre.
   Le doublon ne pouvait que dériver. */
.fiche-section h2[id] { scroll-margin-top: 7rem; }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

/* LA RÈGLE EN DEUX MOTS, SUR LA LIGNE DU TITRE — « Jusqu'à 5 » (2026-08-18,
   « as little text to read as possible » : le paragraphe qui l'expliquait est
   parti, le refus du serveur explique encore à qui dépasse). `flex: 1` la
   colle au titre et repousse le point d'état au bout, comme avant elle. */
.section-note {
  flex: 1;
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
  white-space: nowrap;
}

.section-dot {
  flex: none;
  width: .625rem;
  height: .625rem;
  border-radius: var(--radius-pill);
  background: var(--c-border-strong);
}

.section-dot.is-on { background: var(--c-success); }

/* LE CHOIX EN CARTE — la case à cocher devient une cible.

   ── POURQUOI PAS `.check`, QUI EXISTE DÉJÀ ────────────────────────────────
   `.check` est une case de 18 px suivie d'un mot : parfaite dans une liste de
   quarante talents, où ce qu'on lit compte plus que ce qu'on vise. Ici il y a
   trois réponses à une question, et la réponse EST le geste : la carte entière
   se touche, ce qui vaut sur un téléphone tenu d'une main et pour quiconque ne
   vise pas 18 px du premier coup.

   La case reste un vrai `<input>`, seulement invisible : le clavier la trouve,
   le lecteur d'écran l'annonce « bouton radio, coché », et la page marche sans
   une ligne de JavaScript. Un `<div>` avec un `aria-pressed` posé à la main
   perdrait les trois. */
.choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.choice { flex: 1 1 7rem; margin: 0; }

/* Hors flux plutôt que `display: none` : une case masquée par `none` sort de
   l'ordre de tabulation, et le clavier ne peut plus répondre à la question. */
.choice input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.choice-face {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text-mid);
  text-align: center;
  font-size: var(--mil-fs-2);
  cursor: pointer;
}

/* L'objet d'une réponse (2026-08-18) — ramené au cran du mot qu'il précède :
   les 22 px du magasin sont la taille du rail, à côté d'un libellé de pilule
   ils font une image avec une légende. `currentColor` suit l'état tout seul :
   gris au repos, l'accent une fois choisi. */
.choice-face svg {
  flex: none;
  width: 18px;
  height: 18px;
}

/* L'état choisi porte le bord, le fond ET la graisse — trois signaux pour un,
   parce qu'un seul serait la couleur (règle 6). Mêmes jetons que `.seg-opt`,
   qui dit la même chose dans le menu des réglages : deux façons de peindre
   « c'est celui-là » seraient deux vocabulaires à apprendre. */
.choice input:checked + .choice-face {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 600;
}

.choice input:focus-visible + .choice-face {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* LES RÉPONSES COULENT EN PILULES DÈS 700 px (2026-08-18, « improved chip
   flow » de la maquette en colonnes — étendu au catalogue des talents sur le
   mot d'Eric, en direct sur jam : « Intérêts buttons should be content
   width »).

   En cartes étirées (`flex: 1 1 7rem`), quatre réponses se partageaient la
   largeur en un 2×2 inégal où « Sans préférence » pesait autant que la
   réponse ; en pilules à largeur de mot, « Courriel · Slack · Texto ·
   Téléphone » se lit comme une phrase et la rangée dit d'elle-même « cochez
   ce qui vous parle ». Le catalogue suit le même dessin : sa grille de
   colonnes égales rendait quelques rangées de moins (la mesure est sur son
   bloc, plus bas), et Eric a préféré payer cette hauteur pour des boutons au
   format de leur mot. Le rayon pilule est LE jeton des chips (tokens.css) ;
   un libellé qui passerait à deux lignes y perdrait ses coins — aucun de MIL
   ne le fait, c'est le pari, et il se relit ici.

   SOUS 700 px, RIEN NE BOUGE : la carte pleine largeur et la grille du
   catalogue restent la cible du téléphone (la mesure du 375 tient toujours).

   `.choices .choice` ET PAS `.choice` : le statut de dispo (`.dispo-seg`)
   porte les mêmes cartes et doit les GARDER — trois réponses qui se comparent
   restent trois colonnes égales. Le conteneur décide du dessin. */
@media (min-width: 700px) {
  .choices .choice,
  .tags .tag { flex: 0 0 auto; }

  .choices .choice-face,
  .tags .tag-face {
    min-height: 48px;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-pill);
  }

  /* PAS DE CROCHET SUR LES PASTILLES NI SUR LES PILULES — Eric, 2026-08-18,
     sur jam, deux fois : « remove the check on active — selected color is
     enough », puis « same for Ce que j'aime faire ». La règle 6 tient sans
     lui : l'état choisi porte le bord, le fond ET la graisse, et la graisse
     n'est pas une couleur. La grille des dispos, elle, GARDE son crochet —
     une tuile n'a pas de mot à engraisser, le crochet y est le seul signal
     qui ne soit pas une couleur, et il y REMPLACE la case au lieu de la
     doubler. */
}

/* LE TALENT EN PASTILLE — le même patron que `.choice`, à une différence près :
   la pastille se range À CÔTÉ de sa voisine au lieu de prendre la ligne.

   ── POURQUOI CE N'ÉTAIT PAS DÉJÀ ÇA ────────────────────────────────────────
   Le catalogue était une colonne de `.check` — une case de 18 px suivie d'un
   mot. L'argument tenu jusqu'ici était qu'une CARTE par talent ferait six
   écrans ; il visait `.choice`, pleine largeur, et il tenait pour elle. Une
   pastille qui s'apparie coûte moins que la case qu'elle remplace : mesuré à
   375 px sur les neuf talents de MIL, 352 px de haut en cases contre 288 en
   pastilles, avec des cibles qui passent de 18 px à toute la pastille.

   ── UNE GRILLE, ET C'EST CE QUI PAYE LA HAUTEUR ────────────────────────────
   En file (`flex-wrap`), la pastille prend la largeur de son mot : « 🔨 Réparer
   & bricoler » fait 202 px, deux d'affilée en demandent 412, et le panneau d'un
   téléphone en offre 293. Résultat mesuré, neuf talents à 375 px : une par
   ligne, 408 px de haut — PLUS que les 352 px des cases qu'elles remplacent.

   En grille de colonnes égales, la pastille prend la largeur de sa COLONNE et
   son étiquette passe à la ligne dedans : deux colonnes de 142 px, cinq
   rangées, 288 px — soit 64 px de MOINS que les cases qu'elle remplace, avec
   toute la pastille pour cible.

   LA GRILLE NE VAUT PLUS QUE SOUS 700 px depuis le 2026-08-18 : refaite avec
   les libellés de MIL, la mesure lui donnait toujours raison (le flux libre
   rend sept rangées inégales là où elle en rend cinq, à 375 comme dans une
   colonne de bureau de ~350), mais Eric, la comparaison sous les yeux sur
   jam, a tranché pour des boutons au format de leur mot dès 700
   (« Intérêts buttons should be content width ») — la hauteur est le prix
   du dessin, et c'est sa fiche. La règle du flux vit avec les pilules de
   Préférences, plus haut ; celle-ci reste la vérité du téléphone.

   Le `minmax` est en `rem` et non en px : à 150 % de zoom texte, une colonne
   fixe de 136 px couperait « Bidouiller le » au milieu. */
.tags {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* LE PASSAGE AU FLUX EST ICI, APRÈS LA GRILLE, ET L'ORDRE EST LA CORRECTION :
   posé plus haut dans la feuille (avec les pilules de Préférences), il perdait
   contre la déclaration ci-dessus À POIDS ÉGAL — et le symptôme, vu sur jam,
   était une pastille au rayon pilule ENCORE rangée en colonnes égales, parce
   que ses règles à deux classes gagnaient pendant que le `display` perdait.
   Le reste du dessin (largeur au mot, rayon, hauteur) vit là-bas, avec sa
   raison ; ici il n'y a que le `display`, à l'endroit où il gagne. */
@media (min-width: 700px) {
  .tags { display: flex; flex-wrap: wrap; }
}

.tag { margin: 0; min-width: 0; }

/* Hors flux plutôt que `display: none`, comme `.choice` : une case masquée par
   `none` sort de l'ordre de tabulation, et le clavier ne peut plus répondre. */
.tag input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* `height: 100%` remplit la rangée de la grille : sans lui, une pastille à une
   ligne posée à côté d'une pastille à deux lignes flotte au milieu de sa
   cellule, et la grille se lit comme un damier mal aligné.

   `--radius-sm` et non la pilule : à deux lignes de texte, un bord entièrement
   arrondi mange les coins du mot. C'est le même rayon que `.choice-face`, qui
   porte le même genre de contenu. */
.tag-face {
  display: flex;
  align-items: center;
  height: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--mil-fs-2);
  line-height: 1.25;
  cursor: pointer;
}

/* Coché : le bord, le fond ET la graisse — trois signaux pour un, parce qu'un
   seul serait la couleur (règle 6). Mêmes jetons que `.choice-face` et
   `.seg-opt` : une seule façon de peindre « c'est celui-là » dans toute l'app.

   Le talent au repos garde `--c-text` et non le gris de `.choice-face` : ici on
   LIT la liste entière pour en cocher deux, donc les sept neuvièmes qui restent
   doivent rester lisibles. Sur une question à trois réponses, au contraire, ce
   qui n'est pas choisi doit reculer. */
.tag input:checked + .tag-face {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
  font-weight: 600;
}

.tag input:focus-visible + .tag-face {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* LE BOUTON D'ENREGISTREMENT COLLE AU BAS DE LA ZONE.

   L'écran fait cinq hauteurs de téléphone : sans ça, changer une préférence en
   haut demande de traverser la grille des disponibilités pour trouver le
   bouton, et c'est le trajet qui fait abandonner. Il ne flotte pas au-dessus du
   contenu — il a son fond et son filet, donc rien ne se lit dessous.

   ── `bottom: 0` EST REDEVENU JUSTE, ET IL FAUT SAVOIR POURQUOI ─────────────
   Il a été faux du 2026-08-12 au 2026-08-16, et personne ne l'a vu : la page
   défilait, la barre des destinations était `position: fixed` au bas de la
   FENÊTRE sous 700 px, et cette barre-ci se collait INTÉGRALEMENT dessous —
   mesuré à 375 px, 743→812 px sous une barre qui va de 736 à 812. Le bouton de
   qui n'a pas JavaScript et la ligne « C'est enregistré » de tous les autres
   vivaient hors de l'écran, sur tous les téléphones, sans que rien ne le
   signale. Le correctif du 2026-08-14 lisait `--rail-tabbar-h` pour l'esquiver.

   La zone qui défile s'arrête aujourd'hui AU-DESSUS de cette barre : son bas est
   le bas de la zone, et il n'y a plus de meuble à esquiver. C'est la deuxième
   raison de figer la fenêtre — un `bottom: 0` qui ment selon la largeur est le
   mode de défaillance de tout collant dans une page qui défile sous des meubles
   fixes.

   Les marges négatives lui rendent la largeur du panneau, comme au tableau de
   bord et pour la même raison : un filet qui s'arrête aux marges laisse deux
   couloirs par où le contenu remonte. */
.fiche-save {
  position: sticky;
  bottom: 0;
  margin: var(--space-5) calc(var(--fiche-pad) * -1) 0;
  padding: var(--space-3) var(--fiche-pad);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface);
}

/* ET ELLE NE PREND RIEN QUAND ELLE N'A RIEN À DIRE (2026-08-16).

   `profil.js` cache le bouton et laisse la ligne d'état VIDE tant que rien n'a
   été tapé : la barre réservait quand même 69 px — un filet et une bande
   blanche épinglés au bas de l'écran, en permanence, sur l'écran dont la
   demande était « better use of space ». Le défaut ne se voyait pas avant
   aujourd'hui, et pour une mauvaise raison : au téléphone, la barre entière
   vivait cachée sous la barre des destinations.

   LES DEUX CONDITIONS SONT NÉCESSAIRES, et c'est ce qui garde la règle 4 : sans
   JavaScript le bouton n'est pas caché, donc la barre reste — c'est le seul
   moyen d'enregistrer. Depuis le 2026-08-18 la barre est même l'EXCEPTION :
   l'envoi est silencieux et le succès passe par l'avis (« the toast is
   enough »), donc elle ne se rouvre que sur une panne — la ligne parle, et le
   bouton revient à côté d'elle.

   SA MARGE DU HAUT RESTE : c'est l'air sous la dernière question, que la zone
   ne porte plus (elle n'a pas de rembourrage vertical, voir `.fiche-plein`). */
.fiche-save:has(button[hidden]):has(.fiche-status:empty) {
  padding: 0;
  border-top: 0;
}

/* Sous 700 px la carte prend toute la largeur : deux boutons de 7 rem côte à
   côte sur un téléphone donnent deux colonnes de texte cassé. */
@media (max-width: 699px) {
  .choice { flex-basis: 100%; }
}

/* LA LIGNE D'ÉTAT DE L'ENREGISTREMENT AUTOMATIQUE — là où était le bouton.

   Elle prend sa place exacte (2026-08-12) : le bouton se cache dès que
   `profil.js` démarre, et deux façons d'enregistrer côte à côte poseraient la
   question « et si je clique quand même ? ». Elle revient au gris dès que
   l'avis qui passe a disparu, ce qui est le bon partage — l'avis dit « ta tape
   a pris », cette ligne dit « où en est cette page », dix minutes plus tard.

   `is-error` ne change QUE la couleur, et c'est volontaire : le mot change
   aussi (`autosave_failed`), et le bouton d'envoi réapparaît à côté. Trois
   signaux pour un, dont deux se lisent sans distinguer le rouge du gris. */
.fiche-status {
  margin: 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  color: var(--c-text-mid);
  font-size: var(--mil-fs-1);
}

.fiche-status.is-error { color: var(--c-danger-text); }

/* VIDE, ELLE NE RÉSERVE PAS SES 44 PX. Les 44 px sont la cible tactile de la
   croix des avis, qui n'existe que quand il y a un mot ; sans cette règle, la
   barre ci-dessus ne peut pas se replier — un `padding: 0` sur une boîte dont
   l'enfant fait 44 px de haut ne rend rien. */
.fiche-status:empty { min-height: 0; }

/* ───────────────────────────────────────────────────────────────────────────
   LA FICHE EN COLONNES (2026-08-18) — l'option A de la maquette d'Eric.
   ───────────────────────────────────────────────────────────────────────────
   « Columns on desktop » : dès que la fenêtre offre la largeur, les trois
   sections du formulaire se posent côte à côte — talents, disponibilités,
   préférences — et l'écran de cinq hauteurs de téléphone tient dans UNE
   hauteur de bureau. C'est du CSS seulement : l'ordre du gabarit ne bouge
   pas, l'impression non plus (une feuille fait moins de 1200 px, donc ce
   bloc ne s'y applique jamais).

   1200 ET NON 999 : le seuil des comités mesure le CHROME — la largeur où le
   rail change de forme. Celui-ci mesure le CONTENU : la grille des dispos
   veut ~330 px sans se comprimer, les talents autant, les préférences ~300,
   plus deux gouttières de 32 et le rembourrage du panneau — la première
   fenêtre qui offre tout ça est ~1200. En dessous, RIEN ne change : la
   colonne unique livrée au téléphone continue de valoir. Un seuil se
   justifie par ce qu'il mesure.

   LE TABLEAU DE BORD SE RETIRE EN COLONNES, et ce n'est pas une perte : ses
   trois pastilles sont des ANCRES vers ce qu'on ne voit pas, sur un écran
   qu'on traverse. En colonnes, les trois destinations sont sous les yeux, et
   leur état vit déjà au bout de chaque titre — le point de section, qui lit
   le même `answeredSections()`, donc les deux ne peuvent pas se contredire.
   La jauge et la phrase restent : « où j'en suis » ne dépend pas de la
   largeur. `profil.js` continue de mettre à jour des pastilles cachées — un
   `display: none` n'arrête pas un `classList.toggle` — et elles reviennent
   telles quelles au premier rétrécissement. */
@media (min-width: 1200px) {
  form[data-autosave] {
    display: grid;
    /* Pesées sur le contenu : la grille des dispos ne se comprime pas, donc
       sa colonne est la plus large ; les préférences coulent en pilules,
       donc la leur est la plus étroite. */
    grid-template-columns: 20fr 23fr 19fr;
    column-gap: var(--space-6);
    /* `start` et non l'étirement par défaut : trois sections de hauteurs
       différentes étirées au même bas dessineraient trois filets de tête
       alignés sur un vide inégal. */
    align-items: start;
  }

  /* La barre d'enregistrement reprend toute la largeur SOUS les colonnes :
     elle parle du formulaire entier, pas d'une colonne. Elle reste collante —
     trois colonnes hautes défilent encore sur un portable bas. */
  form[data-autosave] > .fiche-save { grid-column: 1 / -1; }

  /* Le tableau de bord rend son épinglage et ses pastilles (voir le chapeau
     du bloc). Le fond et le filet partent avec : ils existaient pour que le
     contenu ne défile pas À TRAVERS un meuble épinglé, et il n'y a plus de
     meuble. */
  .fiche-board {
    position: static;
    margin: 0;
    padding: 0 0 var(--space-2);
    border-bottom: 0;
    background: none;
  }

  .fiche-board .quests { display: none; }

  .board-meter { margin-bottom: 0; }

  /* Plus rien d'épinglé au-dessus des titres : la cible d'une ancre n'a plus
     que son propre air à garder. Les 7 rem du cas courant esquivent un
     tableau de bord qui, ici, ne colle plus. */
  .fiche-section h2[id] { scroll-margin-top: var(--space-4); }
}

/* ════════════════════════════════════════════════════════════════════════════
   INVITER — la barre collante et la recherche (Eric, 2026-08-19 : « The header
   should be sticky », les deux commandes en haut)

   La page défile — cet écran n'a pas rejoint les fenêtres figées — et la barre
   d'écran reste en vue pour que « Inviter les personnes cochées » soit sous le
   pouce à la centième rangée. Le FOND est déjà dans `.apphead` ; le `z-index`
   la fait passer au-dessus des cases à cocher, et reste sous le rail (30).

   `top: 0` n'est juste qu'aux largeurs où le rail est une colonne. Sous
   700 px, la barre du rail est `position: fixed` PAR-DESSUS la page qui
   défile : un zéro glisserait dessous — le piège payé par `/profil`, où la
   barre d'enregistrement a vécu cachée sur tous les téléphones. On lit le
   jeton, jamais un nombre. */
.invitations .apphead {
  position: sticky;
  top: 0;
  z-index: 5;
}

@media (max-width: 700px) {
  .invitations .apphead { top: var(--rail-bar-h); }
}

/* UNE BARRE COLLANTE CASSE LES LIENS D'ANCRE (le troisième piège de la même
   liste) : sans script, « Faire entrer quelqu'un » saute à la surface de
   création, et la cible se poserait sous la barre. La marge dit à l'ancre de
   s'arrêter avant. */
.invitations .modale {
  scroll-margin-top: calc(var(--apphead-h) + var(--space-4));
}

@media (max-width: 700px) {
  .invitations .modale {
    scroll-margin-top: calc(var(--rail-bar-h) + var(--apphead-h) + var(--space-4));
  }
}

/* La recherche vit dans la ZONE DES ÉTATS de la barre (Eric, 2026-08-19) et
   emprunte le champ et la croix de l'annuaire — mêmes classes, même dessin,
   même place réservée pour la croix, et leurs tailles de flex travaillent
   puisque la zone est une rangée flex. Rien à ajouter ici : c'est le point. */

/* UN `.btn` RÉDUIT À SON PICTOGRAMME (Eric, 2026-08-19, sur la barre des
   invitations). Deuxième demandeur après le journal du Cahier
   (`.cahier-org-journal`), donc la classe devient commune — sans elle, le
   bouton garde le rembourrage d'un mot absent et le dessin flotte au milieu
   d'un bouton vide. Le dessin ne se nomme jamais (contrat du magasin
   d'icônes) : l'`aria-label` du bouton est OBLIGÉ.

   `display: block` sur le dessin : en ligne, il s'assoit sur une ligne de
   base et le bouton gagne quatre pixels de vide sous l'icône. */
.btn-picto {
  padding-inline: var(--space-2);
  line-height: 1;
}

.btn-picto svg { display: block; }

/* UNE ACTION DE BARRE INACTIVE (Eric, 2026-08-19 : l'enveloppe ne s'allume
   qu'avec au moins une personne cochée). `aria-disabled` et pas `disabled` :
   le déclencheur est un LIEN — c'est ce qui garde la surface de confirmation
   joignable sans script — et l'attribut est aussi ce que le lecteur d'écran
   annonce. C'est le script qui le pose et qui BLOQUE le clic ; cette règle ne
   fait que le montrer. Pas de `pointer-events: none` : il éteindrait le
   curseur sans éteindre le clavier, et le blocage aurait l'air fait quand il
   ne le serait qu'à moitié. */
a.btn[aria-disabled='true'],
a.btn-primary[aria-disabled='true'] {
  opacity: .45;
  filter: grayscale(1);
  cursor: default;
}

/* ===============================================================
   APPRÉCIER LA PARTICIPATION — la page (2026-08-19)
   ===============================================================
   Eric : « une page séparée de Comités », « pas de nav à gauche,
   juste une longue liste avec des sections pour chaque comité dont
   je suis coordo ou sous-coordo », et « mes comités se défilent un
   après l'autre ».

   Deux surfaces sont mortes avec cette page : l'écran par comité
   (`/comites/{c}/appreciations`) et la colonne du roster. Ce qui
   est dessiné ici est ce qu'elles ne pouvaient pas faire — trois
   comités et cent personnes dans une seule descente.
   --------------------------------------------------------------- */

/* L'EN-TÊTE FIGÉ D'UN ÉCRAN QUI N'EST PAS UN COMITÉ.
   `.comite-tete` fait ce travail dans la section Comités ; le
   reprendre ici mentirait sur ce qu'on regarde, et un vocabulaire
   qui ment est la première divergence — celle qu'on ne voit
   jamais. Mêmes valeurs, à dessein : c'est la même mécanique. */
.ecran-contenu > .appr-entete {
  flex: 0 0 auto;
  padding: var(--space-4) var(--space-5) 0;
  background: var(--c-surface);
}

.ecran-contenu > .appr-entete > .tabs-row { margin: 0; }

/* Le compte du bouton se colle au mot ; le dessin garde sa taille
   d'icône de barre plutôt que celle du rail. */
.appreciations-porte { gap: var(--space-2); }
.appreciations-porte svg { width: calc(18px * var(--mil-fs, 1)); height: calc(18px * var(--mil-fs, 1)); }

/* ---------------------------------------------------------------
   UN BLOC = UN COMITÉ
   --------------------------------------------------------------- */

/* La hauteur VISIBLE de la barre du comité une fois collée. C'est
   une variable et pas un nombre recopié parce que DEUX règles en
   dépendent — la barre elle-même et la ligne des crans qui se cale
   dessous. Recopié, un décalage d'un pixel laisse passer du texte
   entre les deux étages, et ça ne se voit qu'en défilant vite. */
.appr-bloc { --appr-tete-h: 44px; }

.appr-bloc + .appr-bloc { margin-top: var(--space-6); }

/* PREMIER ÉTAGE COLLANT — quel comité j'apprécie, et combien il
   en reste.

   Le décalage négatif affleure le HAUT de la zone qui défile, qui
   a son propre rembourrage de 24 px : sans lui, une bande de
   contenu passerait au-dessus de la barre. Le rembourrage haut le
   rattrape pour que le titre ne colle pas au bord une fois collé,
   et `box-sizing` fait que `--appr-tete-h` reste ce qu'on VOIT. */
.appr-tete {
  position: sticky;
  top: calc(-1 * var(--space-5));
  z-index: 3;
  box-sizing: border-box;
  min-height: calc(var(--appr-tete-h) + var(--space-5));
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 calc(-1 * var(--space-5));
  padding: calc(var(--space-5) + var(--space-2)) var(--space-5) var(--space-2);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
}

.appr-tete h2 {
  margin: 0;
  font-size: var(--mil-fs-4);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--c-text);
}

.appr-tete .comite-flag { color: var(--c-text-mid); }

.appr-reste {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* La jauge est celle du parcours (`.board-meter`), rétrécie et
   remise en ligne : ici elle est À CÔTÉ du compte, pas au-dessus
   d'une liste. Un `<progress>` et pas un `<div>` à largeur
   calculée — la largeur d'un `<div>` s'écrit en style en ligne,
   que la règle 6 interdit, et l'élément natif dit « 37 sur 43 »
   tout seul à voix haute. */
.appr-jauge { width: 96px; margin: 0; }

/* « Tout est fait » — le seul endroit de l'écran où une couleur
   d'état paraît, et elle CONFIRME un mot au lieu de le remplacer
   (règle 6). */
.appr-fini {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--c-success-text);
}

.appr-fini svg { width: 16px; height: 16px; }

/* ---------------------------------------------------------------
   LE TABLEAU — style B : le cran est nommé EN TÊTE DE COLONNE
   ---------------------------------------------------------------
   Quatre boutons libellés par rangée coûtent 700 px et 172 mots à
   relire pour apprécier quarante-trois personnes ; quatre colonnes
   nommées coûtent 370 px et quatre mots, écrits une fois. C'est
   cette largeur-là qui laisse la place au statut et au retrait.

   La règle 6 — « le cran est porté par son nom, jamais par sa
   couleur » — tient à DEUX conditions, et les deux sont écrites
   dans le code : la ligne d'en-tête reste collée (voir juste en
   dessous), et chaque cible porte un `aria-label` qui nomme la
   personne ET le cran. Le nom ne dépend donc jamais de l'œil.
   --------------------------------------------------------------- */
.appr-table { width: 100%; border-collapse: collapse; }

/* SECOND ÉTAGE COLLANT. Sans lui, l'en-tête sort de l'écran à la
   septième rangée et le style B redevient quatre ronds anonymes —
   c'est-à-dire exactement ce que la règle 6 interdit. Le décalage
   lit la variable du bloc : les deux étages bougent ensemble. */
.appr-table thead th {
  position: sticky;
  top: var(--appr-tete-h);
  z-index: 2;
  padding: var(--space-2) var(--space-3);
  font-size: var(--mil-fs-0);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--c-text-muted);
  text-align: left;
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
  white-space: nowrap;
}

/* L'en-tête d'un cran est le SEUL endroit où son nom est écrit :
   il a donc le droit de passer à la ligne, et pas celui d'être
   coupé. `hyphens: none` défait le `hyphens: auto` du `body`, qui
   rendait « AU REN-DEZ-VOUS » sur trois lignes. */
/* 96 px et non 108 : cinq colonnes au lieu de quatre depuis le 2026-08-19, et
   le libellé le plus long est passé de « Au rendez-vous » à « Au poste ». Le
   tableau est donc PLUS étroit qu'avant malgré le bouton de plus. */
.appr-table th.appr-cran-tete {
  width: 96px;
  text-align: center;
  white-space: normal;
  line-height: 1.25;
  hyphens: none;
  -webkit-hyphens: none;
}

.appr-table td {
  padding: var(--space-1) var(--space-3);
  border-bottom: 1px solid var(--c-border);
  vertical-align: middle;
}

.appr-table tbody tr:hover td { background: var(--c-hover-bg); }

.appr-nom { font-size: var(--mil-fs-2); }

/* Le rappel du mois dernier. Discret parce qu'il ne se lit qu'en
   cas de doute — mais présent sur chaque rangée, parce que c'est
   lui qui transforme une décision en confirmation. */
.appr-sous {
  display: block;
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
}

/* Une rangée déjà faite RECULE D'UN TON, elle ne bouge pas et elle
   ne disparaît pas : on perdrait sa place à chaque clic sur une
   liste de quarante-trois noms. */
.appr-faite .appr-nom { color: var(--c-text-mid); }
.appr-partie .appr-nom { color: var(--c-text-mid); }

/* LA CIBLE D'UN CRAN — 44 px de haut, la règle de l'app pour tout
   ce qui se presse dans une rangée. Large de toute la colonne :
   c'est ce qui permet de descendre une colonne au doigt sans viser
   un rond de 20 px. */
.appr-cran-cell { text-align: center; }

.appr-cran {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 44px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--c-border-strong);
}

.appr-cran svg { width: 20px; height: 20px; }

.appr-cran:hover { background: var(--c-accent-soft); color: var(--c-accent-dark); }

.appr-cran[aria-pressed='true'] {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-accent-dark);
}

/* Le mot du cran ne paraît qu'au téléphone — voir le média plus bas. */
.appr-cran-mot { display: none; }

/* ---------------------------------------------------------------
   LE TÉLÉPHONE : LA RANGÉE SE REPLIE
   ---------------------------------------------------------------
   Six colonnes ne tiennent pas dans 375 px. Mesuré à l'écran le
   2026-08-19 : « Absent·e » et la croix sortaient du cadre, et le
   tableau emportait la page en défilement latéral.

   ── POURQUOI PAS UNE BOÎTE QUI DÉFILE, COMME LE ROSTER ──────────
   Le roster en a une, et elle est justifiée là-bas : son `<select>`
   de rang ne se coupe nulle part. Ici elle coûterait deux choses
   qu'on ne peut pas payer. Un `overflow-x` crée un contexte de
   défilement sur LES DEUX axes : les deux étages collants — la
   barre du comité et la ligne qui NOMME les crans — cesseraient de
   tenir, et le style B redeviendrait quatre ronds anonymes. Et un
   second axe de défilement à l'intérieur du premier, sur l'écran
   qu'on parcourt le plus vite, est exactement ce que la directive
   « une app au doigt » refuse.

   ── CE QU'ON FAIT À LA PLACE ────────────────────────────────────
   La rangée devient une grille de deux étages : le nom et la croix
   en haut, les quatre crans en dessous, chacun sur un quart de la
   largeur. Le statut part — le rang se lit ailleurs (la fiche, le
   roster), un cran ne se pose qu'ici. L'en-tête part aussi, et le
   nom du cran revient DANS le bouton (`.appr-cran-mot`) : c'est la
   condition pour que la règle 6 tienne à toutes les largeurs.
   --------------------------------------------------------------- */
@media (max-width: 700px) {
  .appr-table,
  .appr-table tbody,
  .appr-table tr,
  .appr-table td { display: block; }

  .appr-table thead { display: none; }

  /* CINQ colonnes depuis le cinquième bouton (2026-08-19) : les crans
     occupent la seconde rangée, un par colonne. « Au poste » est plus court
     que « Au rendez-vous » qu'il remplace, donc les cinq tiennent dans 375 px
     là où quatre tenaient à peine. */
  .appr-table tr {
    display: grid;
    /* `minmax(0, 1fr)` et PAS `1fr` : une piste `1fr` a un plancher implicite
       de `min-content`, donc « Absent·e » — un mot que rien ne coupe — élargit
       sa colonne, les cinq débordent, et c'est le DOCUMENT entier qui part en
       défilement latéral. Mesuré à 390 px le 2026-08-19 : le cinquième bouton
       sortait de l'écran et la barre du bas était coupée avec lui. Le `0`
       autorise la piste à descendre sous son contenu ; le texte se replie. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--space-1);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--c-border);
  }

  .appr-table td {
    padding: 0;
    border: 0;
  }

  .appr-table td:first-child { grid-column: 1 / 5; grid-row: 1; }

  .appr-table td.cell-actions {
    grid-column: 5;
    grid-row: 1;
    justify-self: end;
  }

  .appr-table td.appr-col-statut { display: none; }

  .appr-table td.appr-cran-cell { grid-row: 2; }

  /* Le bouton porte son mot : dessin au-dessus, nom en dessous, et
     la cible garde ses 44 px de haut minimum. */
  .appr-cran {
    flex-direction: column;
    gap: 2px;
    padding: var(--space-1) 0;
    border-color: var(--c-border);
  }

  .appr-cran-mot {
    display: block;
    font-size: var(--mil-fs-0);
    line-height: 1.15;
    text-align: center;
    /* Il A le droit de se couper ici, contrairement à l'en-tête de colonne du
       bureau : à cinq crans dans 375 px, « Absent·e » sur deux lignes vaut
       mieux qu'un mot qui pousse la page hors de l'écran. */
    overflow-wrap: anywhere;
  }
}

/* ---------------------------------------------------------------
   LA COUVERTURE — qui apprécie, et qui n'apprécie pas du tout
   ---------------------------------------------------------------
   L'onglet Métriques, depuis le 2026-08-24. Un comité par rangée,
   six mois en colonnes, et un NOMBRE dans chaque case plutôt
   qu'une coche : « 9 » à côté de « 42 personnes » dit que le plus
   gros comité de la coop est couvert au cinquième, là où une coche
   l'aurait rendu vert.

   Pas d'étage collant ici, contrairement au tableau des crans : il
   n'y a rien à retenir en haut. Le nom du mois est dans la case du
   HAUT et une rangée fait une ligne, donc l'œil ne perd pas la
   colonne — et au téléphone l'en-tête part de toute façon, les
   mois revenant sous chaque comité.
   --------------------------------------------------------------- */
.couv-table { width: 100%; border-collapse: collapse; }

.couv-table thead th {
  padding: var(--space-2) var(--space-3);
  font-size: var(--mil-fs-0);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--c-text-muted);
  text-align: left;
  border-bottom: 1px solid var(--c-border);
  white-space: nowrap;
}

.couv-table tbody th,
.couv-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--c-border);
  text-align: left;
  font-weight: 400;
  vertical-align: middle;
}

/* LE NOM DU COMITÉ EST UN `<th scope="row">`, ET IL FAUT LE DÉSHABILLER.
   Le balisage est juste — c'est lui qui apparie « Entretien » et « août »
   pour qui écoute, sans quoi les six cases s'annoncent comme six nombres
   nus. Mais la feuille habille TOUT `th` en étiquette de colonne : petites
   capitales, gris, interlettrage, fond de barre. « ENTRETIEN » en 12 px
   gris n'est plus un nom de comité, c'est un en-tête — et le tableau se lit
   alors comme s'il n'avait pas de contenu. */
.couv-table tbody th {
  font-size: inherit;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--c-text);
  background: none;
}

.couv-table tbody tr:hover th,
.couv-table tbody tr:hover td { background: var(--c-hover-bg); }

.couv-nom { font-size: var(--mil-fs-2); }

/* La taille du comité, sous son nom : c'est le DÉNOMINATEUR, donc
   la moitié qui décide si « 9 » est beaucoup. Discrète, mais sur
   chaque rangée — un dénominateur qu'il faut aller chercher n'est
   pas un dénominateur. */
.couv-sous {
  display: block;
  font-size: var(--mil-fs-0);
  color: var(--c-text-muted);
}

/* 3,5 rem : « 12 » tient, et six colonnes de cette largeur laissent
   la moitié de la ligne au nom du comité jusque vers 700 px, où la
   rangée se replie.

   ⚠️ Le sélecteur PORTE SON ÉLÉMENT, et ce n'est pas du style :
   `.couv-table tbody th, .couv-table td` (0,2,1) bat `.couv-mois`
   (0,1,0), donc un `text-align: center` écrit sur la classe seule
   est ANNULÉ par l'alignement à gauche du tableau — sans erreur,
   sans rien de rouge, et la colonne se lit juste « un peu de
   travers ». Vu à l'écran le 2026-08-24 : les nombres pendaient à
   gauche sous des en-têtes centrés. */
.couv-table th.couv-mois,
.couv-table td.couv-mois { width: 3.5rem; text-align: center; }

/* Un point médian et pas un zéro. Six zéros à la file font une
   ligne qui a l'air PLEINE, et c'est précisément la ligne qu'on
   veut lire comme vide. */
.couv-rien { color: var(--c-text-muted); }

.couv-qui { white-space: nowrap; }

.couv-jamais { color: var(--c-text-muted); }

/* ---------------------------------------------------------------
   LA COUVERTURE AU TÉLÉPHONE : LA RANGÉE SE REPLIE
   ---------------------------------------------------------------
   Huit colonnes ne tiennent pas dans 375 px. Comme pour le tableau
   des crans, la rangée devient deux étages plutôt qu'une boîte à
   défilement latéral : le nom et le « qui » en haut, les six mois
   en dessous, un par colonne.

   L'en-tête part avec le repli, donc les mois perdraient leur nom
   — d'où `data-mois` en `::before` sur chaque case, qui le remet
   DANS la case. Ce n'est pas de la prose échappée dans le CSS :
   l'attribut est rendu par le serveur, la feuille ne fait que le
   poser (même mécanique que le mot du cran, à l'envers).
   --------------------------------------------------------------- */
@media (max-width: 700px) {
  .couv-table,
  .couv-table tbody,
  .couv-table tr,
  .couv-table tbody th,
  .couv-table td { display: block; }

  .couv-table thead { display: none; }

  .couv-table tr {
    display: grid;
    /* `minmax(0, 1fr)` et pas `1fr` : une piste `1fr` a un plancher de
       `min-content`, donc un nom de mois un peu long élargit sa colonne, les
       six débordent, et c'est le document entier qui part en défilement
       latéral. Le `0` autorise la piste à descendre sous son contenu. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: var(--space-1);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--c-border);
  }

  .couv-table tbody th,
  .couv-table td {
    padding: 0;
    border: 0;
  }

  .couv-table tbody th {
    grid-column: 1 / 5;
    grid-row: 1;
  }

  .couv-table td.couv-qui {
    grid-column: 5 / 7;
    grid-row: 1;
    text-align: right;
    align-self: center;
    font-size: var(--mil-fs-0);
    white-space: normal;
  }

  .couv-table td.couv-mois {
    grid-row: 2;
    width: auto;
    padding-top: var(--space-1);
  }

  .couv-table td.couv-mois::before {
    content: attr(data-mois);
    display: block;
    font-size: var(--mil-fs-0);
    color: var(--c-text-muted);
  }
}

/* ---------------------------------------------------------------
   UNE ZONE QUI N'A RIEN À MONTRER
   ---------------------------------------------------------------
   La page fermée par le réglage de la coop s'en sert : elle dit
   POURQUOI il n'y a rien — une zone vide sans phrase se lit comme
   une panne. Les métriques l'ont employée jusqu'au 2026-08-24,
   date à laquelle elles ont eu quelque chose à montrer.
   --------------------------------------------------------------- */
.appr-vide {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-7) var(--space-5);
  text-align: center;
  color: var(--c-text-mid);
}

.appr-vide svg { width: 32px; height: 32px; color: var(--c-text-muted); }

.appr-vide p { max-width: 52ch; margin: 0; }

.appr-vide > p:first-of-type { font-size: var(--mil-fs-4); color: var(--c-text); }

/* ===============================================================
   MES APPAREILS (2026-08-21)
   ===============================================================
   L'envers de la case « reste connecté·e » : une ligne par
   navigateur, et la croix qui la retire.

   Le patron est celui des adresses de courriel (`.courriels`) et
   ce n'est pas une économie de lignes — c'est la même GRAMMAIRE :
   une liste d'objets qui m'appartiennent, chacun avec son outil au
   bout de la rangée. Deux dessins différents pour la même idée
   obligeraient à réapprendre où se trouve la croix.
   =============================================================== */
.appareils {
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.appareil {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
}

/* CELUI D'OÙ L'ON REGARDE se voit sans lire, comme `.courriel.is-attente` :
   le trait de gauche fait la différence en diagonale, là où la pastille
   demande de lire deux mots. C'est la ligne qu'il ne faut pas retirer par
   distraction, donc c'est celle qui doit se distinguer de loin. */
.appareil.is-ici { border-left: 3px solid var(--c-accent); }

.appareil.is-vide {
  justify-content: center;
  color: var(--c-text-mid);
}

.appareil-forme { display: flex; color: var(--c-text-mid); }
.appareil-forme svg { width: 20px; height: 20px; }

/* `min-width: 0` : sans lui, un nom long pousse la croix hors de la carte sur
   un téléphone — le contenu d'un enfant flex refuse de rétrécir sous sa taille
   intrinsèque tant qu'on ne le lui a pas permis. */
.appareil-corps {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.appareil-nom {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  overflow-wrap: anywhere;
}

/* La seconde ligne : quand, et d'où. Elle se LIT en second, donc elle est plus
   petite et plus pâle — c'est un indice de reconnaissance, pas une donnée. */
.appareil-faits {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
}

/* L'IP ne dit rien à la plupart des gens et tout à quelques-uns : gardée, et
   au dernier rang de la hiérarchie visuelle. En chiffres tabulaires, parce
   qu'une colonne d'adresses qui ne s'alignent pas se compare mal. */
.appareil-ip {
  font-variant-numeric: tabular-nums;
  color: var(--c-text-muted);
}

/* La croix est poussée au bout, comme dans toutes les rangées à outils de
   l'app : elles s'alignent, donc on vise la bonne. */
.appareil > form.inline { margin-left: auto; }

/* « Cet appareil » : la seule pastille de l'app qui marque un LIEU plutôt
   qu'un état ou un rang. L'accent, parce que c'est le repère depuis lequel on
   lit tout le reste de la liste. */
.pill.is-ici {
  border-color: var(--c-accent);
  color: var(--c-accent-dark);
  background: var(--c-accent-soft);
}

/* ─── Le jumelage (T1 §4) ─────────────────────────────────────────────────
   Le QR ne se thème PAS : son fond blanc voyage DANS le SVG (`QrSvg` porte le
   raisonnement — une caméra lit mal l'inversé, la zone de silence fait partie
   du code). Ici on ne fait que le poser : centré, coins apprivoisés. */
.jumelage-qr {
  display: flex;
  justify-content: center;
  margin: var(--space-4) 0 var(--space-3);
}

.jumelage-qr svg { border-radius: var(--radius); }

.jumelage-toucher {
  text-align: center;
  margin: 0 0 var(--space-2);
}

/* LES DEUX PORTES DU CARNET — la même rangée sur `/jumeler`, sous le QR de
   jumelage, et dans « À propos ». Elle s'appelait `.jumelage-magasins` tant
   qu'une seule page la portait ; le nom nommait alors l'endroit, ce qui est
   exactement la dette que le troisième appelant fait payer.

   `flex-wrap` PARCE QUE LE LIBELLÉ DE TEST EST LONG : « TestFlight » et
   « Google Play (test) » côte à côte dépassent la largeur d'un téléphone, et
   deux boutons qui débordent sont pires qu'un bouton par ligne. */
.magasins {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
}

/* ─── « À propos » (2026-08-25) ───────────────────────────────────────────
   La modale du menu des réglages. Deux blocs : le LOGICIEL — sa version, son
   crédit — puis LE CARNET, qui n'est là que s'il y a une porte où l'aller
   prendre. */
.a-propos-version { margin: 0 0 var(--space-2); }

/* Le crédit ferme le bloc du logiciel : en retrait d'encre, et c'est sa marge
   du bas qui écarte la section du carnet. */
.a-propos-credit {
  margin: 0;
  color: var(--c-text-mid);
}

/* Le titre de la section du carnet. C'est un `h3` — le `h2` de la boîte est
   déjà pris par « À propos », et le plan du document ne se déduit pas de ce
   qui est joli. Il porte donc à la main l'en-tête de section que `h2` dessine
   ailleurs, plus le trait qui sépare les deux blocs. */
.a-propos-titre {
  margin: var(--space-5) 0 var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--c-border);
  font-size: var(--mil-fs-1);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--c-text-mid);
}

/* LE QR, ET LA CONDITION QUI DÉCIDE DE SON EXISTENCE.

   Il ne sert que là où l'écran qu'on LIT et l'appareil où l'on INSTALLE sont
   deux objets différents. Dans la main, ils n'en font qu'un : les deux boutons
   du dessus font le travail, et le code devient un carré qu'on ne peut pas
   scanner avec l'appareil qui l'affiche. Même raisonnement que le lien
   « Ouvrir sur ce téléphone » d'`/appareils`, pris dans l'autre sens.

   `display: none` et non `visually-hidden` : il n'y a rien à annoncer non plus
   — le bloc entier est déjà `aria-hidden`, un QR ne se lit pas à voix haute. */
.a-propos-qr {
  margin-top: var(--space-4);
  text-align: center;
  font-size: var(--mil-fs-0);
  color: var(--c-text-mid);
}

/* Le QR ne se thème PAS : son fond blanc voyage DANS le SVG (`QrSvg` porte le
   raisonnement). Ici on ne fait qu'apprivoiser ses coins. */
.a-propos-qr svg { border-radius: var(--radius); }

.a-propos-qr span { display: block; margin-top: var(--space-2); }

@media (max-width: 700px) {
  .a-propos-qr { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LA MESSAGERIE (T3)
   ═══════════════════════════════════════════════════════════════════════════
   La mise en page vient de `.work-split` / `.work-list` / `.work-detail` —
   ceux d'« À faire » et des bris. Rien à redéclarer : ce bloc ne traite que ce
   qui est propre à une conversation, c'est-à-dire trois choses.

   ── ① L'EXTRAIT SE COUPE EN CSS, JAMAIS EN PHP ─────────────────────────────
   Tronquer à quatre-vingts caractères côté serveur coupe au milieu d'un mot et
   pose des points de suspension DANS le texte de quelqu'un — au demi-mot près
   selon la langue. Le débordement est une affaire de largeur, donc c'est le
   rendu qui le règle, et la phrase entière reste dans le HTML pour qui lit à
   voix haute. */
.fil-extrait {
  display: block;
  /* `position` PARCE QU'IL Y A `overflow` — la règle du dépôt, tenue par
     `DesignSystemRulesTest` : une zone qui écrête doit CONTENIR ce qu'elle
     écrête, sinon un descendant absolu (toute `.visually-hidden` en est un) se
     place dans le document entier et échappe au découpage. */
  position: relative;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--mil-fs-1);
  margin-top: var(--space-1);
}

/* ── ② LA CONVERSATION — une colonne, le champ à la fin ────────────────────
   Le plus ancien en haut : c'est l'ordre d'une conversation et pas celui d'une
   boîte de réception. Conséquence utile — la dernière chose de la zone est le
   champ, donc il n'y a rien à faire défiler tout seul à l'arrivée. */
.fil-conversation {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Une bulle sobre, pas un phylactère : ce n'est pas une app de clavardage, et
   la forme le dit avant le texte. Le message de quelqu'un d'autre porte le
   fond de surface ; le sien, la teinte d'accent — la seule différence, et elle
   suffit à balayer la colonne sans lire les noms. */
.fil-message {
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  background: var(--c-surface);
}

.fil-message.est-moi {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}

.fil-message-tete {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  margin: 0 0 var(--space-2);
  font-size: var(--mil-fs-1);
}

/* `pre-wrap` : les retours à la ligne sont de la PONCTUATION quand quelqu'un
   fait une liste dans un message. `overflow-wrap` casse l'URL collée sans
   espace, qui autrement élargit le volet et fait défiler la page en travers. */
.fil-message-corps {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.fil-ecrire { margin-top: var(--space-2); }

/* `max-width: none` EN PLUS de la largeur, et c'est ce qui manquait : les
   champs du dépôt portent un plafond de lisibilité (~388 px) qui est juste
   pour un formulaire de fiche et faux ici — un champ de réponse trois fois
   plus étroit que les messages au-dessus de lui se lit comme un champ cassé.
   Mesuré à l'écran, pas deviné : `width: 100%` seul ne gagnait pas contre le
   plafond. */
.fil-ecrire textarea {
  width: 100%;
  max-width: none;
}

/* ── ③ LA LISTE DES PERSONNES DÉFILE ───────────────────────────────────────
   Même raison que `.check-list` juste au-dessus, et le même chiffre : MIL a
   cent six membres, et un formulaire qui pousse son bouton d'envoi hors de
   l'écran se fait abandonner au milieu. */
.check-scroll {
  position: relative;  /* écrête ⇒ contient, cf. `.ecran` */
  max-height: 15rem;
  overflow-y: auto;
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  padding: var(--space-2);
}

/* ── ④ « ÉCRIRE À » NE MONTRE QUE LA MOITIÉ CHOISIE ────────────────────────
   Le patron est celui de l'aide du statut de dispo : `:has` sur le choix coché,
   rien avant. Sans lui, quelqu'un qui vient de cocher « un comité » lit quand
   même cent six cases à cocher — la définition même du texte qu'on ne devrait
   pas avoir à parcourir (directive du 2026-08-18).

   Le repli est le bon, et c'est pour ça que c'est écrit ainsi plutôt qu'en
   masquant par défaut : un navigateur sans `:has` affiche LES DEUX moitiés. Le
   formulaire est plus long, il n'est pas cassé — jamais un champ qu'on ne peut
   plus atteindre. */
.fil-cible:has(input[value="gens"]:checked) .fil-choix-comite,
.fil-cible:has(input[value="comite"]:checked) .fil-choix-gens {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LA PROMOTION (F10)
   ═══════════════════════════════════════════════════════════════════════════
   « Un message se promeut […] et le fil garde le lien vers l'objet qu'il a
   produit. » Deux surfaces, et la seconde est celle qui doit rester lisible
   dans six mois.

   ── ① LA COMMANDE EST DISCRÈTE, ET C'EST UNE DÉCISION ─────────────────────
   Elle est sous CHAQUE message. Un bouton plein répété cinquante fois ferait
   d'une conversation un tableau de bord, et le geste rare paraîtrait le geste
   attendu. Un lien, à la taille du dessous — visible à qui le cherche, muet
   pour qui lit. */
.fil-message-promo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  margin: var(--space-2) 0 0;
  font-size: var(--mil-fs-1);
}

/* La commande passe APRÈS ce qui a déjà été produit (l'ordre du HTML), et se
   pousse à droite : ce qu'on lit est le résultat, pas le bouton. */
.fil-promouvoir { margin-left: auto; }

/* L'ÉTIQUETTE DE CE QU'UN MESSAGE A PRODUIT — « Bris », « À faire ». Un seul
   mot, et il ne se coupe pas : `nowrap`, parce que la ligne peut porter deux
   étiquettes et la commande, et qu'un « À » seul en bout de ligne au-dessus
   d'un « faire » se lit comme un défaut d'affichage plutôt que comme un lien.
   C'est le repli du `flex-wrap` au-dessus qui décide où ça passe à la ligne —
   entre les étiquettes, jamais dedans. */
.promo-lien { white-space: nowrap; }

/* ── ② CE QUE LE FIL A PRODUIT — L'INDEX, EN HAUT ──────────────────────────
   La ligne sous un message est l'ancre ; celle-ci est ce qui tient la promesse
   du plan. Sur un fil de trois cents messages, une flèche perdue au milieu
   n'est pas un endroit où l'on retrouve une décision — il faut que ça se lise
   sans défiler.

   Encadrée plutôt que simplement colorée : c'est un objet DURABLE au-dessus
   d'une conversation qui, elle, s'efface à vingt-quatre mois. La bordure dit
   cette différence-là sans un mot. */
.fil-produits {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--c-border);
  border-radius: var(--radius);
  background: var(--c-surface);
  font-size: var(--mil-fs-1);
}

/* ── ③ L'ÉCRAN DU GESTE ────────────────────────────────────────────────────
   Une colonne étroite : c'est un formulaire, pas un tableau. Le message cité
   en haut est le sujet de la page, et il porte le filet de gauche des
   citations — la seule chose qui le distingue d'un champ de plus. */
.promo-ecran {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 44rem;
}

.promo-source {
  margin: 0;
  padding: var(--space-3);
  border-left: 3px solid var(--c-accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--c-surface);
}

.promo-deja { margin: 0; }

/* LE BLOC DE CHAMPS D'UNE CIBLE — ce que le titre partagé ne suffit pas à dire :
   où est le bris, quel comité, quelle rubrique. La colonne est ici, une seule
   fois ; les quatre classes `-bris` / `-a_faire` / `-cahier` / `-appel` ne
   servent qu'au repli plus bas, et n'ont donc aucune règle à elles. */
.promo-champs {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ── ④ LA CIBLE CHOISIE MONTRE SES CHAMPS, LES AUTRES SE REPLIENT ──────────
   Le patron d'« Écrire à », à quatre branches. Écrit en MASQUANT ce qui n'est
   pas choisi plutôt qu'en montrant ce qui l'est : un navigateur sans `:has`
   affiche les quatre blocs, donc un formulaire plus long — jamais un champ
   qu'on ne peut plus atteindre (règle 10).

   Le titre disparaît sur la seule cible qui n'en a pas : le Cahier reçoit une
   ligne dans une rubrique qui porte déjà son nom. */
form:has(.promo-cibles input[value="a_faire"]:checked) .promo-champs-bris,
form:has(.promo-cibles input[value="cahier"]:checked) .promo-champs-bris,
form:has(.promo-cibles input[value="appel"]:checked) .promo-champs-bris,
form:has(.promo-cibles input[value="bris"]:checked) .promo-champs-a_faire,
form:has(.promo-cibles input[value="cahier"]:checked) .promo-champs-a_faire,
form:has(.promo-cibles input[value="appel"]:checked) .promo-champs-a_faire,
form:has(.promo-cibles input[value="bris"]:checked) .promo-champs-cahier,
form:has(.promo-cibles input[value="a_faire"]:checked) .promo-champs-cahier,
form:has(.promo-cibles input[value="appel"]:checked) .promo-champs-cahier,
form:has(.promo-cibles input[value="bris"]:checked) .promo-champs-appel,
form:has(.promo-cibles input[value="a_faire"]:checked) .promo-champs-appel,
form:has(.promo-cibles input[value="cahier"]:checked) .promo-champs-appel,
form:has(.promo-cibles input[value="cahier"]:checked) .promo-champ-titre {
  display: none;
}

/* ── ⑤ LA PROVENANCE, SUR L'OBJET ──────────────────────────────────────────
   La ligne « promu depuis une conversation » vit sur le bris, le besoin et
   l'appel. Discrète comme une date, parce que c'en est une — mais jamais
   masquée : c'est elle qui répond à « d'où sort cette décision ? ». */
.promo-origine {
  margin: 0 0 var(--space-2);
  font-size: var(--mil-fs-1);
}

/* ---------------------------------------------------------------
   L'implication lue par la coordination (PLAN-IMPLICATION, tranche 2)
   ---------------------------------------------------------------

   Le troisième onglet de l'écran des appréciations. Il réutilise le bloc de
   comité (`.appr-bloc`, `.appr-tete`, `.appr-nom`) : c'est le même écran, la
   même pile « un comité après l'autre », et lui refaire une coquille aurait
   donné deux en-têtes de comité qui dérivent l'une de l'autre.

   Ce qui est écrit ici est ce que l'onglet des crans n'a pas : une ligne par
   personne qui n'est pas une rangée de tableau, parce qu'elle porte un nombre,
   trois crans et parfois un paragraphe. */

.impl-personne {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--c-border);
}

.impl-personne:first-of-type { border-top: 0; }

.impl-qui {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

/* « N'a pas répondu » n'est pas un reproche et ne doit pas se lire comme un
   état d'alerte : c'est le cas le plus fréquent du premier trimestre. Encre
   moyenne, aucun fond. */
.impl-rien {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

/* LA PORTE. Elle est un LIEN sur toute sa phrase — le geste qui débloque est
   la seule chose à faire ici, donc toute la ligne est la cible. */
.impl-ferme {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-2);
}

.impl-chiffre {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-2) 0 0;
}

.impl-satisfaction {
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

/* LES TROIS CRANS DU TRIMESTRE — l'autre moitié de l'écart, posée sous les
   heures et non à côté : à 480 px, « à côté » veut dire que le troisième mois
   passe à la ligne tout seul et se lit comme s'il appartenait à autre chose. */
.impl-crans {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  font-size: var(--mil-fs-2);
}

.impl-cran-mois {
  display: block;
  color: var(--c-text-muted);
}

/* Le cran est porté par son NOM (charte, règle 6) — ici il n'y a même pas de
   couleur à porter : ce serait une seconde échelle, à côté de celle de
   l'onglet des crans, et deux vocabulaires visuels pour un même fait. */
.impl-cran-mot { color: var(--c-text); }

.impl-resume {
  margin: var(--space-2) 0 0;
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

/* CE QUI EST ARRIVÉ SANS NOM — détaché du reste par un filet, parce que ce
   bloc-ci ne parle de personne et que le précédent ne parle que de gens. */
.impl-mots {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--c-border);
}

.impl-mots h3 {
  font-size: var(--mil-fs-2);
  margin: 0 0 var(--space-2);
}

.impl-mot {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--c-border-strong);
  background: var(--c-surface-2);
  border-radius: var(--radius-sm);
}

.impl-mot p { margin: 0; }

/* Sélecteur descendant VOULU, pas une habitude : `.impl-mot p` ci-dessus est
   plus spécifique qu'une classe seule, donc un `.impl-mot-genre` nu verrait sa
   marge écrasée par le `margin: 0` du raccourci — silencieusement, et seulement
   sur la marge du bas. */
.impl-mot .impl-mot-genre {
  font-size: var(--mil-fs-1);
  color: var(--c-text-muted);
  margin-bottom: var(--space-1);
}

/* ---------------------------------------------------------------
   L'implication en chiffres, sur l'onglet Info d'un comité
   ---------------------------------------------------------------

   Le premier des trois cercles de lecture : tout le monde voit ça. Trois ou
   quatre nombres, aucun nom, et aucune phrase d'explication — le lecteur n'est
   pas venu pour les chiffres, il est venu lire ce que fait le comité. Ce bloc
   est ce qu'on peut lui apprendre en passant, donc il doit se lire en un coup
   d'œil ou pas du tout. */

.carte-implication {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--c-border);
}

.carte-implication h2 {
  font-size: var(--mil-fs-3);
  margin: 0;
}

.impl-periode {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-2);
  color: var(--c-text-muted);
}

.impl-reponses {
  margin: var(--space-1) 0 0;
  font-size: var(--mil-fs-2);
  color: var(--c-text-mid);
}

/* LES NOMBRES. `flex-wrap` et pas une grille à colonnes fixes : il y en a deux,
   trois ou cinq selon ce que les gens ont rempli, et une grille de cinq
   laisserait deux trous quand personne n'a chiffré ses heures. */
.impl-chiffres {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-5);
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
}

/* Le nombre au-dessus de ce qu'il compte, et pas à côté : c'est ce qui laisse
   la légende passer à la ligne sans désaligner la rangée de nombres. */
.impl-nombre {
  display: block;
  font-size: var(--mil-fs-5);
  font-weight: 700;
  line-height: 1.1;
}

.impl-quoi {
  display: block;
  max-width: 14ch;
  font-size: var(--mil-fs-1);
  color: var(--c-text-mid);
}
